@colabcommerce/elements 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/QuoteForm.js +24 -0
  2. package/dist/Store.js +37 -0
  3. package/dist/StoreLocator.js +34 -0
  4. package/dist/StoreLocatorProvider.js +1 -0
  5. package/dist/elements.css +1 -0
  6. package/dist/index-DvX0QvFh.js +31 -0
  7. package/dist/navigation-DpGLbcKb.js +1 -0
  8. package/dist/store-locator-Bto20jHS.js +1 -0
  9. package/dist/styles.js +1 -0
  10. package/dist/translations-6mspyPRw.js +1 -0
  11. package/dist/useStoreLocator.js +1 -0
  12. package/dist/useStoreLocatorConfig-r4Y_2t-M.js +1 -0
  13. package/package.json +5 -1
  14. package/.pnp.cjs +0 -16484
  15. package/.pnp.loader.mjs +0 -2126
  16. package/.yarn/install-state.gz +0 -0
  17. package/.yarn/releases/yarn-4.12.0.cjs +0 -942
  18. package/.yarnrc.yml +0 -1
  19. package/cypress/fixtures/example.json +0 -5
  20. package/cypress/support/commands.js +0 -25
  21. package/cypress/support/component-index.html +0 -15
  22. package/cypress/support/component.js +0 -26
  23. package/eslint.config.js +0 -32
  24. package/index.html +0 -13
  25. package/playground/index.html +0 -14
  26. package/playground/main.jsx +0 -36
  27. package/src/App.css +0 -0
  28. package/src/App.jsx +0 -65
  29. package/src/components/CollapsibleStoreHours/index.jsx +0 -269
  30. package/src/components/HoursList/index.jsx +0 -225
  31. package/src/components/LeadForm/index.jsx +0 -241
  32. package/src/components/MessageDialog/index.jsx +0 -169
  33. package/src/components/QuoteForm/index.jsx +0 -82
  34. package/src/components/QuoteFormSearch/index.jsx +0 -276
  35. package/src/components/QuoteFormStoreList/index.jsx +0 -65
  36. package/src/components/QuoteFormStoreListItem/index.jsx +0 -134
  37. package/src/components/QuoteLeadForm/index.jsx +0 -16
  38. package/src/components/QuoteMap/index.jsx +0 -96
  39. package/src/components/QuoteMapMarker/index.jsx +0 -56
  40. package/src/components/StaticMap/index.jsx +0 -24
  41. package/src/components/Store/index.jsx +0 -44
  42. package/src/components/StoreContact/index.jsx +0 -96
  43. package/src/components/StoreInfo/index.jsx +0 -50
  44. package/src/components/StoreList/index.jsx +0 -59
  45. package/src/components/StoreListItem/index.jsx +0 -99
  46. package/src/components/StoreListItem/indexStoreListItem.cy.jsx +0 -30
  47. package/src/components/StoreListNoneFound/index.jsx +0 -16
  48. package/src/components/StoreLocator/index.jsx +0 -43
  49. package/src/components/StoreLocatorMap/index.jsx +0 -93
  50. package/src/components/StoreLocatorMapMarker/index.jsx +0 -55
  51. package/src/components/StoreLocatorMessageDialog/index.jsx +0 -20
  52. package/src/components/StoreLocatorSearch/index.jsx +0 -316
  53. package/src/components/StoreMap/index.jsx +0 -30
  54. package/src/components/StoreMeta/index.jsx +0 -7
  55. package/src/components/StoreProducts/index.jsx +0 -112
  56. package/src/components/ui/Badge/index.jsx +0 -46
  57. package/src/components/ui/Button/index.jsx +0 -56
  58. package/src/components/ui/Button/indexButton.cy.jsx +0 -9
  59. package/src/components/ui/Card/index.jsx +0 -90
  60. package/src/components/ui/Input/index.jsx +0 -19
  61. package/src/components/ui/Input/indexInput.cy.jsx +0 -9
  62. package/src/components/ui/LoadingPuff/index.jsx +0 -10
  63. package/src/components/ui/Panel/index.jsx +0 -23
  64. package/src/components/ui/PhoneNumberInput/index.jsx +0 -17
  65. package/src/contexts/quote-form.jsx +0 -94
  66. package/src/contexts/store-locator.jsx +0 -83
  67. package/src/contexts/store.jsx +0 -59
  68. package/src/contexts/translations.jsx +0 -11
  69. package/src/dist.css +0 -229
  70. package/src/entries/QuoteForm.js +0 -2
  71. package/src/entries/Store.js +0 -2
  72. package/src/entries/StoreLocator.js +0 -2
  73. package/src/entries/StoreLocatorProvider.js +0 -2
  74. package/src/entries/styles.js +0 -2
  75. package/src/entries/useStoreLocator.js +0 -2
  76. package/src/i18n/defaultResources.js +0 -19
  77. package/src/i18n/index.js +0 -44
  78. package/src/i18n/mergeResources.js +0 -22
  79. package/src/index.css +0 -214
  80. package/src/lib/addressComponentsToAddress.js +0 -43
  81. package/src/lib/productSchema.js +0 -6
  82. package/src/lib/useGeolocation.js +0 -266
  83. package/src/lib/useHours.js +0 -205
  84. package/src/lib/usePlacesAutocomplete.js +0 -288
  85. package/src/lib/useProductAvailability.js +0 -38
  86. package/src/lib/useRudderAnalytics.js +0 -50
  87. package/src/lib/useSearchResults.js +0 -102
  88. package/src/lib/useStoreLocatorConfig.js +0 -50
  89. package/src/lib/utils/cn.js +0 -6
  90. package/src/lib/utils/measure.js +0 -31
  91. package/src/locales/en/default.json +0 -58
  92. package/src/locales/es/default.json +0 -58
  93. package/src/locales/fr/default.json +0 -58
  94. package/src/locales/it/default.json +0 -58
  95. package/src/main.jsx +0 -10
  96. package/vite.config.js +0 -67
  97. /package/{public → dist}/vite.svg +0 -0
package/.yarnrc.yml DELETED
@@ -1 +0,0 @@
1
- yarnPath: .yarn/releases/yarn-4.12.0.cjs
@@ -1,5 +0,0 @@
1
- {
2
- "name": "Using fixtures to represent data",
3
- "email": "hello@cypress.io",
4
- "body": "Fixtures are a great way to mock data for responses to routes"
5
- }
@@ -1,25 +0,0 @@
1
- // ***********************************************
2
- // This example commands.js shows you how to
3
- // create various custom commands and overwrite
4
- // existing commands.
5
- //
6
- // For more comprehensive examples of custom
7
- // commands please read more here:
8
- // https://on.cypress.io/custom-commands
9
- // ***********************************************
10
- //
11
- //
12
- // -- This is a parent command --
13
- // Cypress.Commands.add('login', (email, password) => { ... })
14
- //
15
- //
16
- // -- This is a child command --
17
- // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18
- //
19
- //
20
- // -- This is a dual command --
21
- // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22
- //
23
- //
24
- // -- This will overwrite an existing command --
25
- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
@@ -1,15 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
-
4
- <head>
5
- <meta charset="utf-8">
6
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
7
- <meta name="viewport" content="width=device-width,initial-scale=1.0">
8
- <title>Components App</title>
9
- </head>
10
-
11
- <body>
12
- <div data-cy-root class="cc"></div>
13
- </body>
14
-
15
- </html>
@@ -1,26 +0,0 @@
1
- // ***********************************************************
2
- // This example support/component.js is processed and
3
- // loaded automatically before your test files.
4
- //
5
- // This is a great place to put global configuration and
6
- // behavior that modifies Cypress.
7
- //
8
- // You can change the location of this file or turn off
9
- // automatically serving support files with the
10
- // 'supportFile' configuration option.
11
- //
12
- // You can read more here:
13
- // https://on.cypress.io/configuration
14
- // ***********************************************************
15
-
16
- // Import commands.js using ES2015 syntax:
17
- import './commands'
18
-
19
- import { mount } from 'cypress/react'
20
-
21
- import '@fontsource/plus-jakarta-sans/latin.css'
22
- import '@/index.css'
23
- Cypress.Commands.add('mount', mount)
24
-
25
- // Example use:
26
- // cy.mount(<MyComponent />)
package/eslint.config.js DELETED
@@ -1,32 +0,0 @@
1
- // For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
2
- import storybook from "eslint-plugin-storybook";
3
-
4
- import js from '@eslint/js'
5
- import globals from 'globals'
6
- import reactHooks from 'eslint-plugin-react-hooks'
7
- import reactRefresh from 'eslint-plugin-react-refresh'
8
- import { defineConfig, globalIgnores } from 'eslint/config'
9
-
10
- export default defineConfig([
11
- globalIgnores(['dist']),
12
- {
13
- files: ['**/*.{js,jsx}'],
14
- extends: [
15
- js.configs.recommended,
16
- reactHooks.configs.flat.recommended,
17
- reactRefresh.configs.vite,
18
- ],
19
- languageOptions: {
20
- ecmaVersion: 2020,
21
- globals: globals.browser,
22
- parserOptions: {
23
- ecmaVersion: 'latest',
24
- ecmaFeatures: { jsx: true },
25
- sourceType: 'module',
26
- },
27
- },
28
- rules: {
29
- 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
30
- },
31
- },
32
- ])
package/index.html DELETED
@@ -1,13 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>elements</title>
8
- </head>
9
- <body>
10
- <div id="root"></div>
11
- <script type="module" src="/src/main.jsx"></script>
12
- </body>
13
- </html>
@@ -1,14 +0,0 @@
1
- <html style="font-size: 10px;">
2
-
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width,initial-scale=1" />
6
- <title>Playground</title>
7
- </head>
8
-
9
- <body>
10
- <div id="root"></div>
11
- <script type="module" src="/main.jsx"></script>
12
- </body>
13
-
14
- </html>
@@ -1,36 +0,0 @@
1
- import React from "react";
2
- import ReactDOM from "react-dom/client";
3
- import CartForm from "../../src/components/CartForm"; // import from source, not dist
4
-
5
- ReactDOM.createRoot(document.getElementById("root")).render(
6
- <React.StrictMode>
7
- <CartForm
8
- id="b594100c-7a81-4114-a2fe-043ce7e69ae9"
9
- products={[
10
- {
11
- "collection_id": "41080",
12
- "options": [
13
- {
14
- "key": "Configuration",
15
- "value": "41080-67/10/09/60/66"
16
- },
17
- {
18
- "key": "Cover",
19
- "value": "Classic Buff"
20
- },
21
- {
22
- "key": "Category"
23
- },
24
- {
25
- "key": "Quantity",
26
- "value": 1
27
- }
28
- ],
29
- "name": "Acacia",
30
- "url": "https://www.palliser.com/acacia-41080-09",
31
- "image": "https://www.palliser.com/media/catalog/product/a/c/acacia_sectionals_67_6h_09_10_66_41080_front_angle_plp.jpg"
32
- }
33
- ]}
34
- locale="en-US" />
35
- </React.StrictMode>
36
- );
package/src/App.css DELETED
File without changes
package/src/App.jsx DELETED
@@ -1,65 +0,0 @@
1
- import { useState } from 'react'
2
- import { Button } from '@/components/ui/Button'
3
- import StoreLocatorSearch from './components/StoreLocatorSearch'
4
- import StoreLocatorMap from './components/StoreLocatorMap'
5
- import Input from '@/components/ui/Input'
6
- import useGeolocation from '@/lib/useGeolocation'
7
- import StoreList from './components/StoreList'
8
- import useSearchResults from '@/lib/useSearchResults'
9
- import { APIProvider } from '@vis.gl/react-google-maps'
10
- import StoreInfo from '@/components/StoreInfo'
11
- import StoreContact from '@/components/StoreContact'
12
- import Store from '@/components/Store'
13
- import StoreLocator from '@/components/StoreLocator'
14
- import QuoteForm from '@/components/QuoteForm'
15
- import '@fontsource/plus-jakarta-sans/latin.css'
16
- import './App.css'
17
-
18
- function App() {
19
- const [showQuoteForm, setShowQuoteForm] = useState(true)
20
-
21
- const products = [{
22
- name: 'Juno',
23
- sku: 'juno',
24
- quantity: 2
25
- },
26
- {
27
- name: 'Braxton',
28
- sku: 'braxton',
29
- quantity: 1
30
- }
31
- ]
32
-
33
- const handleClose = () => {
34
- console.log('Quote form closed')
35
- // Super hack, hide the form after close to simulate unmount
36
- setShowQuoteForm(false)
37
- // Timeout to show the form again
38
- setTimeout(() => {
39
- setShowQuoteForm(true)
40
- }, 2000)
41
- }
42
-
43
- const handleSuccess = (values) => {
44
- console.log('Quote form submitted successfully with values:', values)
45
- }
46
-
47
- return (
48
- <div className="cc">
49
- {/* <StoreLocator organizationId="d58bcec2-0769-48ba-9c82-a234dcdc7073" locale="fr" /> */}
50
- {/* <Store storeId="d10ede1f-0f2f-45e2-94d1-c5b387365302" /> */}
51
- <div style={{ maxWidth: '600px', height: '100vh', margin: '0 auto' }}>
52
- {showQuoteForm &&
53
- <QuoteForm
54
- organizationId="4f155976-8631-445a-b1b7-c8ccac28cca1"
55
- products={products}
56
- onClose={handleClose}
57
- onSuccess={handleSuccess}
58
- />
59
- }
60
- </div>
61
- </div>
62
- )
63
- }
64
-
65
- export default App
@@ -1,269 +0,0 @@
1
- import * as React from "react";
2
- import * as Collapsible from "@radix-ui/react-collapsible";
3
- import { ChevronDown } from "lucide-react";
4
- import { useTranslation } from "react-i18next";
5
-
6
- const DAY_LABELS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
7
-
8
- export default function StoreHoursCollapsible({
9
- hours,
10
- openingSoonMinutes = 60,
11
- closingSoonMinutes = 60,
12
- className = "",
13
- }) {
14
- const { t } = useTranslation();
15
- const [open, setOpen] = React.useState(false);
16
-
17
- const computed = React.useMemo(() => {
18
- const safeHours = Array.isArray(hours) ? hours : [];
19
- const storeTz =
20
- safeHours.find((h) => h?.timezone)?.timezone ||
21
- Intl.DateTimeFormat().resolvedOptions().timeZone;
22
-
23
- const now = new Date();
24
- const storeWeekdayIndex = getWeekdayIndexInTimeZone(now, storeTz); // 0..6
25
- const today = safeHours.find((h) => h?.day === storeWeekdayIndex);
26
-
27
- const status = computeStatus({
28
- now,
29
- today,
30
- openingSoonMinutes,
31
- closingSoonMinutes,
32
- t,
33
- });
34
-
35
- // Display Monday..Sunday
36
- const displayOrder = [1, 2, 3, 4, 5, 6, 0];
37
- const byDay = new Map(safeHours.map((h) => [h.day, h]));
38
- const rows = displayOrder.map((dayIdx) => {
39
- const h = byDay.get(dayIdx);
40
- const label = t(`default:${DAY_LABELS[dayIdx].toLowerCase()}`);
41
- const isToday = dayIdx === storeWeekdayIndex;
42
-
43
- let text = t("default:closed");
44
- if (h?.open) {
45
- const openDate = buildOpenDateForDisplay(h);
46
- const closeDate = buildCloseDateForDisplay(h, openDate);
47
- if (openDate && closeDate) {
48
- text = `${formatTimeLocal(openDate)} - ${formatTimeLocal(closeDate)}`;
49
- } else {
50
- text = t("default:open");
51
- }
52
- }
53
-
54
- return { dayIdx, label, text, isToday };
55
- });
56
-
57
- return {
58
- statusLabel: t(`default:${status.label.toLowerCase().replace(" ", "_")}`), // "Closed" | "Opening Soon" | "Open" | "Closing Soon"
59
- statusDetail: status.detail, // "Opens at 9am" | "Closes at 4:30pm" | ""
60
- rows,
61
- };
62
- }, [hours, openingSoonMinutes, closingSoonMinutes]);
63
-
64
- return (
65
- <div className={className}>
66
- <Collapsible.Root open={open} onOpenChange={setOpen}>
67
- <Collapsible.Trigger asChild>
68
- <button
69
- type="button"
70
- className={[
71
- "group w-full flex items-center justify-between gap-3",
72
- "rounded-lg bg-white py-2 px-3",
73
- "text-sm text-muted-foreground",
74
- "hover:bg-black/[0.02] active:bg-black/[0.04]",
75
- "focus:outline-none",
76
- ].join(" ")}
77
- aria-expanded={open}
78
- >
79
- <div className="flex flex-col items-start">
80
- <span className="text-muted-foreground leading-tight">{computed.statusLabel}</span>
81
- {computed.statusDetail ? (
82
- <span className="text-xs text-muted-foreground/80 leading-tight">
83
- {computed.statusDetail}
84
- </span>
85
- ) : null}
86
- </div>
87
-
88
- <ChevronDown
89
- className={[
90
- "h-4 w-4 text-black/60",
91
- "transition-transform duration-200 ease-out",
92
- open ? "rotate-180" : "rotate-0",
93
- ].join(" ")}
94
- aria-hidden="true"
95
- />
96
- </button>
97
- </Collapsible.Trigger>
98
-
99
- <Collapsible.Content
100
- className={[
101
- "mt-2 bg-white p-2",
102
- "data-[state=open]:animate-in data-[state=closed]:animate-out",
103
- "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
104
- "data-[state=closed]:slide-out-to-top-1 data-[state=open]:slide-in-from-top-1",
105
- ].join(" ")}
106
- >
107
- <div className="grid gap-1.5">
108
- {computed.rows.map((row) => (
109
- <div
110
- key={row.dayIdx}
111
- className={[
112
- "flex items-center justify-between gap-3",
113
- "px-2.5 py-1 text-sm",
114
- row.isToday ? "text-primary font-semibold" : "border-black/5 bg-transparent",
115
- ].join(" ")}
116
- aria-current={row.isToday ? "date" : undefined}
117
- >
118
- <span>{row.label}</span>
119
- <span>{row.text}</span>
120
- </div>
121
- ))}
122
- </div>
123
- </Collapsible.Content>
124
- </Collapsible.Root>
125
- </div>
126
- );
127
- }
128
-
129
- /**
130
- * Toggle label logic based on today's schedule (store timezone for "today",
131
- * comparisons against the user's current instant).
132
- *
133
- * Adds a detail line:
134
- * - Closed / Opening Soon -> "Opens at X"
135
- * - Open / Closing Soon -> "Closes at Y"
136
- */
137
- function computeStatus({ now, today, openingSoonMinutes, closingSoonMinutes, t }) {
138
- if (!today || !today.open) return { label: t('default:closed'), detail: "" };
139
-
140
- const openDate = buildOpenDateForDisplay(today);
141
- const closeDate = buildCloseDateForDisplay(today, openDate);
142
-
143
- // If schedule incomplete, still show basic label
144
- if (!openDate || !closeDate) return { label: t('default:open'), detail: "" };
145
-
146
- const nowMs = now.getTime();
147
- const openMs = openDate.getTime();
148
- const closeMs = closeDate.getTime();
149
-
150
- const openingSoonStart = openMs - openingSoonMinutes * 60 * 1000;
151
- const closingSoonStart = closeMs - closingSoonMinutes * 60 * 1000;
152
-
153
- const opensDetail = t('default:opens_at', { time: formatTimeLocal(openDate) });
154
- const closesDetail = t('default:closes_at', { time: formatTimeLocal(closeDate) });
155
-
156
- if (nowMs < openMs) {
157
- if (nowMs >= openingSoonStart) return { label: t('default:opening_soon'), detail: opensDetail };
158
- return { label: t('default:closed'), detail: opensDetail };
159
- }
160
-
161
- if (nowMs >= openMs && nowMs < closeMs) {
162
- if (nowMs >= closingSoonStart) return { label: t('default:closing_soon'), detail: closesDetail };
163
- return { label: t('default:open'), detail: closesDetail };
164
- }
165
-
166
- return { label: t('default:closed'), detail: opensDetail };
167
- }
168
-
169
- /** Returns weekday index (0=Sun..6=Sat) for a Date, as seen in `timeZone`. */
170
- function getWeekdayIndexInTimeZone(date, timeZone) {
171
- const weekday = new Intl.DateTimeFormat("en-US", { weekday: "short", timeZone }).format(date);
172
- switch (weekday) {
173
- case "Sun":
174
- return 0;
175
- case "Mon":
176
- return 1;
177
- case "Tue":
178
- return 2;
179
- case "Wed":
180
- return 3;
181
- case "Thu":
182
- return 4;
183
- case "Fri":
184
- return 5;
185
- case "Sat":
186
- return 6;
187
- default:
188
- return new Date(date).getDay();
189
- }
190
- }
191
-
192
- /**
193
- * Build "open" Date (instant) for display in user's local time.
194
- * Prefer `open_at` because it encodes offset and is DST-correct for that date.
195
- */
196
- function buildOpenDateForDisplay(h) {
197
- if (!h) return null;
198
-
199
- if (h.open_at) {
200
- const d = new Date(h.open_at);
201
- return isValidDate(d) ? d : null;
202
- }
203
-
204
- if (typeof h.open_at_hour === "number" && typeof h.open_at_minute === "number") {
205
- return buildDateFromLocalStoreTime({
206
- storeHour: h.open_at_hour,
207
- storeMinute: h.open_at_minute,
208
- utcOffsetMinute: h.utc_offset_minute,
209
- });
210
- }
211
-
212
- return null;
213
- }
214
-
215
- /**
216
- * Build "close" Date (instant). If close <= open, assume next day.
217
- */
218
- function buildCloseDateForDisplay(h, openDate) {
219
- if (!h) return null;
220
-
221
- if (typeof h.close_at_hour !== "number" || typeof h.close_at_minute !== "number") return null;
222
-
223
- let closeDate = buildDateFromLocalStoreTime({
224
- storeHour: h.close_at_hour,
225
- storeMinute: h.close_at_minute,
226
- utcOffsetMinute: h.utc_offset_minute,
227
- anchorDate: openDate || undefined,
228
- });
229
-
230
- if (!closeDate || !openDate) return closeDate;
231
-
232
- if (closeDate.getTime() <= openDate.getTime()) {
233
- closeDate = new Date(closeDate.getTime() + 24 * 60 * 60 * 1000);
234
- }
235
-
236
- return closeDate;
237
- }
238
-
239
- /**
240
- * Build an instant from a store-local time-of-day plus a fixed UTC offset.
241
- * storeLocal = UTC + offset => UTC = storeLocal - offset
242
- */
243
- function buildDateFromLocalStoreTime({ storeHour, storeMinute, utcOffsetMinute, anchorDate }) {
244
- const base = anchorDate ? new Date(anchorDate) : new Date();
245
- const y = base.getFullYear();
246
- const m = base.getMonth();
247
- const d = base.getDate();
248
-
249
- const utcMillis = Date.UTC(y, m, d, storeHour, storeMinute, 0) - utcOffsetMinute * 60 * 1000;
250
- const out = new Date(utcMillis);
251
-
252
- return isValidDate(out) ? out : null;
253
- }
254
-
255
- function isValidDate(d) {
256
- return d instanceof Date && !Number.isNaN(d.getTime());
257
- }
258
-
259
- /** Format in user's local timezone as "9am" or "4:30pm". */
260
- function formatTimeLocal(date) {
261
- return new Intl.DateTimeFormat("en-US", {
262
- hour: "numeric",
263
- minute: "2-digit",
264
- })
265
- .format(date)
266
- .replace(":00", "")
267
- .replace(" AM", "am")
268
- .replace(" PM", "pm");
269
- }