@akinon/projectzero 2.0.0-beta.1 → 2.0.0-beta.11

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 (107) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/app-template/.env.example +5 -0
  3. package/app-template/.gitignore +2 -0
  4. package/app-template/CHANGELOG.md +251 -0
  5. package/app-template/README.md +6 -0
  6. package/app-template/config/prebuild-tests.json +5 -0
  7. package/app-template/docs/plugins.md +60 -25
  8. package/app-template/jest.config.ts +2 -2
  9. package/app-template/{next.config.mjs → next.config.ts} +6 -3
  10. package/app-template/package.json +30 -27
  11. package/app-template/postcss.config.mjs +8 -0
  12. package/app-template/public/locales/en/account.json +4 -0
  13. package/app-template/public/locales/en/common.json +10 -0
  14. package/app-template/public/locales/tr/account.json +4 -0
  15. package/app-template/public/locales/tr/common.json +10 -0
  16. package/app-template/src/__tests__/middleware-matcher.test.ts +135 -0
  17. package/app-template/src/app/[commerce]/[locale]/[currency]/account/orders/[id]/cancellation/page.tsx +99 -7
  18. package/app-template/src/app/[commerce]/[locale]/[currency]/account/orders/[id]/page.tsx +112 -47
  19. package/app-template/src/app/[commerce]/[locale]/[currency]/account/page.tsx +1 -1
  20. package/app-template/src/app/[commerce]/[locale]/[currency]/address/stores/page.tsx +2 -2
  21. package/app-template/src/app/[commerce]/[locale]/[currency]/auth/page.tsx +1 -1
  22. package/app-template/src/app/[commerce]/[locale]/[currency]/basket/page.tsx +2 -2
  23. package/app-template/src/app/[commerce]/[locale]/[currency]/error.tsx +12 -15
  24. package/app-template/src/app/[commerce]/[locale]/[currency]/forms/[pk]/generate/page.tsx +1 -1
  25. package/app-template/src/app/[commerce]/[locale]/[currency]/{pz-not-found/page.tsx → not-found.tsx} +2 -2
  26. package/app-template/src/app/[commerce]/[locale]/[currency]/orders/checkout/page.tsx +7 -4
  27. package/app-template/src/app/[commerce]/[locale]/[currency]/xml-sitemap/[node]/route.ts +47 -1
  28. package/app-template/src/assets/globals.scss +162 -34
  29. package/app-template/src/components/__tests__/badge.test.tsx +2 -2
  30. package/app-template/src/components/accordion.tsx +1 -1
  31. package/app-template/src/components/button.tsx +50 -35
  32. package/app-template/src/components/checkbox.tsx +1 -0
  33. package/app-template/src/components/file-input.tsx +44 -2
  34. package/app-template/src/components/input.tsx +3 -3
  35. package/app-template/src/components/modal.tsx +1 -1
  36. package/app-template/src/components/select.tsx +2 -2
  37. package/app-template/src/components/shimmer.tsx +1 -1
  38. package/app-template/src/components/tabs.tsx +2 -2
  39. package/app-template/src/components/types/index.ts +4 -1
  40. package/app-template/src/middleware.ts +1 -0
  41. package/app-template/src/plugins.js +2 -1
  42. package/app-template/src/redux/middlewares/category.ts +1 -1
  43. package/app-template/src/redux/reducers/category.ts +1 -1
  44. package/app-template/src/redux/store.ts +4 -3
  45. package/app-template/src/utils/convert-facet-search-params.ts +1 -1
  46. package/app-template/src/views/account/contact-form.tsx +3 -8
  47. package/app-template/src/views/account/content-header.tsx +2 -3
  48. package/app-template/src/views/account/order.tsx +11 -9
  49. package/app-template/src/views/account/orders/order-cancellation-item.tsx +5 -4
  50. package/app-template/src/views/anonymous-tracking/order-detail/index.tsx +45 -38
  51. package/app-template/src/views/basket/basket-item.tsx +1 -0
  52. package/app-template/src/views/category/category-active-filters.tsx +1 -1
  53. package/app-template/src/views/category/category-header.tsx +12 -6
  54. package/app-template/src/views/category/category-info.tsx +4 -4
  55. package/app-template/src/views/category/filters/index.tsx +2 -2
  56. package/app-template/src/views/checkout/auth.tsx +1 -1
  57. package/app-template/src/views/checkout/layout/header.tsx +1 -1
  58. package/app-template/src/views/checkout/steps/payment/index.tsx +1 -1
  59. package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +1 -1
  60. package/app-template/src/views/checkout/steps/payment/options/redirection.tsx +5 -1
  61. package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +4 -4
  62. package/app-template/src/views/checkout/steps/shipping/address-box.tsx +3 -3
  63. package/app-template/src/views/checkout/steps/shipping/addresses.tsx +1 -1
  64. package/app-template/src/views/checkout/summary.tsx +2 -2
  65. package/app-template/src/views/header/action-menu.tsx +11 -4
  66. package/app-template/src/views/header/band.tsx +2 -2
  67. package/app-template/src/views/header/mini-basket.tsx +15 -4
  68. package/app-template/src/views/header/mobile-menu.tsx +6 -6
  69. package/app-template/src/views/header/navbar.tsx +1 -1
  70. package/app-template/src/views/header/pwa-back-button.tsx +1 -1
  71. package/app-template/src/views/header/search/index.tsx +16 -4
  72. package/app-template/src/views/header/search/results.tsx +1 -1
  73. package/app-template/src/views/header/user-menu.tsx +3 -1
  74. package/app-template/src/views/installment-options/index.tsx +1 -1
  75. package/app-template/src/views/login/index.tsx +30 -6
  76. package/app-template/src/views/otp-login/index.tsx +12 -14
  77. package/app-template/src/views/product/price-wrapper.tsx +7 -2
  78. package/app-template/src/views/product/product-info.tsx +35 -5
  79. package/app-template/src/views/product/slider.tsx +1 -1
  80. package/app-template/src/views/product-pointer-banner-item.tsx +1 -1
  81. package/app-template/src/views/register/index.tsx +29 -4
  82. package/app-template/src/views/sales-contract-modal/index.tsx +17 -17
  83. package/app-template/src/widgets/footer-info.tsx +1 -1
  84. package/app-template/src/widgets/footer-menu.tsx +1 -1
  85. package/app-template/src/widgets/footer-subscription/index.tsx +1 -1
  86. package/app-template/src/widgets/home-stories-eng.tsx +1 -1
  87. package/app-template/tailwind.config.js +2 -134
  88. package/codemods/sentry-9/index.js +30 -0
  89. package/codemods/sentry-9/remove-sentry-configs.js +14 -0
  90. package/codemods/sentry-9/remove-sentry-dependency.js +25 -0
  91. package/codemods/sentry-9/replace-error-page.js +32 -0
  92. package/codemods/update-tailwind-config/index.js +30 -0
  93. package/codemods/update-tailwind-config/transform.js +102 -0
  94. package/commands/codemod.ts +17 -0
  95. package/commands/index.ts +3 -1
  96. package/commands/plugins.ts +24 -30
  97. package/dist/codemods/sentry-9/templates/error.js +14 -0
  98. package/dist/commands/codemod.js +15 -0
  99. package/dist/commands/index.js +3 -1
  100. package/dist/commands/plugins.js +23 -20
  101. package/package.json +3 -2
  102. package/app-template/postcss.config.js +0 -6
  103. package/app-template/sentry.client.config.ts +0 -16
  104. package/app-template/sentry.edge.config.ts +0 -3
  105. package/app-template/sentry.properties +0 -4
  106. package/app-template/sentry.server.config.ts +0 -3
  107. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/loading.tsx +0 -67
@@ -51,7 +51,7 @@ export default function Auth() {
51
51
  {t('auth.register.mobile_title')}
52
52
  </Button>
53
53
  </div>
54
- <div className="w-full flex flex-wrap border md:border-0">
54
+ <div className="w-full flex flex-wrap border border-gray-200 md:border-0">
55
55
  <div
56
56
  className={twMerge(
57
57
  clsx('w-full md:block md:w-1/2', {
@@ -25,7 +25,7 @@ export default function Page() {
25
25
  }, [basket, isSuccess]);
26
26
 
27
27
  return (
28
- <div className="max-w-screen-xl p-4 flex flex-col text-primary-800 lg:p-8 xl:flex-row xl:mx-auto">
28
+ <div className="max-w-(--breakpoint-xl) p-4 flex flex-col text-primary-800 lg:p-8 xl:flex-row xl:mx-auto">
29
29
  {isLoading && (
30
30
  <div className="flex justify-center w-full">
31
31
  <LoaderSpinner />
@@ -62,7 +62,7 @@ export default function Page() {
62
62
  <Summary basket={basket} />
63
63
  </>
64
64
  ) : (
65
- <div className="flex flex-col items-center container max-w-screen-sm py-4 px-4 xs:py-6 xs:px-6 sm:py-8 sm:px-8 lg:max-w-screen-xl">
65
+ <div className="flex flex-col items-center container max-w-(--breakpoint-sm) py-4 px-4 xs:py-6 xs:px-6 sm:py-8 sm:px-8 lg:max-w-(--breakpoint-xl)">
66
66
  <h1
67
67
  className="w-full text-xl font-light text-secondary text-center sm:text-2xl"
68
68
  data-testid="basket-empty"
@@ -1,20 +1,17 @@
1
1
  'use client';
2
2
 
3
- import { useLocalization } from '@akinon/next/hooks';
4
- import { Link } from '@theme/components';
5
- import { ROUTES } from '@theme/routes';
3
+ import { useSentryUncaughtErrors } from '@akinon/next/hooks';
4
+ import PzErrorPage from '@akinon/next/views/error-page';
6
5
 
7
- export default function Error() {
8
- const { t } = useLocalization();
6
+ export default function ErrorPage({
7
+ error,
8
+ reset
9
+ }: {
10
+ error: Error & { digest?: string; isServerError?: boolean };
11
+ reset: () => void;
12
+ }) {
13
+ // DO NOT REMOVE THIS LINE TO REPORT UNCAUGHT ERRORS TO SENTRY
14
+ useSentryUncaughtErrors(error);
9
15
 
10
- return (
11
- <section className="text-center px-6 my-14 md:px-0 md:m-14">
12
- <div className="text-7xl font-bold md:text-8xl">500</div>
13
- <h1 className="text-lg md:text-xl"> {t('common.page_500.title')} </h1>
14
- <p className="text-lg md:text-xl"> {t('common.page_500.description')} </p>
15
- <Link href={ROUTES.HOME} className="text-lg underline">
16
- {t('common.page_500.link_text')}
17
- </Link>
18
- </section>
19
- );
16
+ return <PzErrorPage error={error} reset={reset} />;
20
17
  }
@@ -21,7 +21,7 @@ export default async function Page(props) {
21
21
  schema={schema}
22
22
  allFieldClasses={{
23
23
  className:
24
- 'border border-[#d4d4d4] text-[#4a4f54] mt-1.5 h-[38px] p-2.5 text-xs outline-none focus:border-black',
24
+ 'border border-[#d4d4d4] text-[#4a4f54] mt-1.5 h-[38px] p-2.5 text-xs outline-hidden focus:border-black',
25
25
  labelClassName: 'text-[#4a4f54] text-xs',
26
26
  wrapperClassName: 'flex flex-col mb-6'
27
27
  }}
@@ -8,10 +8,10 @@ const NotFound = () => {
8
8
  const { t } = useLocalization();
9
9
 
10
10
  return (
11
- <div className="py-6 flex flex-col items-center justify-center">
11
+ <div className="py-10 lg:py-8 flex flex-col items-center justify-center px-4 lg:px-0">
12
12
  <div className="text-8xl font-bold">404</div>
13
13
  <h1 className="text-4xl font-bold mb-4">{t('not_found.title')}</h1>
14
- <p className="text-lg mb-6">{t('not_found.sub_title')}</p>
14
+ <p className="text-lg mb-6 text-center">{t('not_found.sub_title')}</p>
15
15
  <Link href={'/'}>
16
16
  <Button className="h-auto mt-4 text-base py-3 px-6">
17
17
  {t('not_found.button')}
@@ -11,7 +11,7 @@ import {
11
11
  } from '@akinon/next/redux/reducers/checkout';
12
12
  import { RootState } from '@theme/redux/store';
13
13
  import { ROUTES } from '@theme/routes';
14
- import { useFetchCheckoutQuery } from '@akinon/next/data/client/checkout';
14
+ import { useFetchCheckoutQuery, useResetCheckoutStateQuery } from '@akinon/next/data/client/checkout';
15
15
  import { Button, LoaderSpinner } from '@theme/components';
16
16
  import { pushAddPaymentInfo, pushAddShippingInfo } from '@theme/utils/gtm';
17
17
  import { CheckoutStep } from '@akinon/next/types';
@@ -25,6 +25,8 @@ const Checkout = () => {
25
25
  (state: RootState) => state.checkout
26
26
  );
27
27
 
28
+ const { data: indexData, isLoading: isResetStateLoading } = useResetCheckoutStateQuery(null);
29
+
28
30
  const {
29
31
  data: checkoutData,
30
32
  isFetching,
@@ -32,7 +34,8 @@ const Checkout = () => {
32
34
  isSuccess,
33
35
  refetch: refetchCheckout
34
36
  } = useFetchCheckoutQuery(null, {
35
- refetchOnMountOrArgChange: true
37
+ refetchOnMountOrArgChange: true,
38
+ skip: isResetStateLoading || !indexData
36
39
  });
37
40
  const initialStepChanged = useRef<boolean>(false);
38
41
  const router = useRouter();
@@ -94,10 +97,10 @@ const Checkout = () => {
94
97
  );
95
98
  }
96
99
 
97
- if (isFetching || isError) {
100
+ if (isResetStateLoading || isFetching || isError) {
98
101
  return (
99
102
  <div className="flex flex-col items-center justify-center h-80">
100
- {isFetching ? (
103
+ {isResetStateLoading || isFetching ? (
101
104
  <LoaderSpinner />
102
105
  ) : (
103
106
  <>
@@ -1,5 +1,18 @@
1
1
  import { urlLocaleMatcherRegex } from '@akinon/next/utils';
2
2
 
3
+ /**
4
+ * XML Sitemap Route
5
+ *
6
+ * This route serves XML sitemaps from an S3 bucket.
7
+ *
8
+ * Required environment variables:
9
+ * - SITEMAP_S3_BUCKET_NAME: The name of the S3 bucket containing the sitemaps
10
+ * Example: "0fb534"
11
+ *
12
+ * If the environment variable is not set, the route will return a 503 Service Unavailable
13
+ * response with a JSON error message.
14
+ */
15
+
3
16
  export const dynamic = 'force-dynamic';
4
17
 
5
18
  export async function GET(request: Request, context: { params }) {
@@ -7,9 +20,42 @@ export async function GET(request: Request, context: { params }) {
7
20
  const url = new URL(request.url);
8
21
  const matchedLocale = url.pathname.match(urlLocaleMatcherRegex);
9
22
 
23
+ const s3BucketName = process.env.SITEMAP_S3_BUCKET_NAME;
24
+
25
+ if (!s3BucketName) {
26
+ return new Response(
27
+ JSON.stringify({
28
+ error: 'Configuration error',
29
+ message: 'Please set the SITEMAP_S3_BUCKET_NAME environment variable'
30
+ }),
31
+ {
32
+ status: 503,
33
+ headers: {
34
+ 'Content-Type': 'application/json'
35
+ }
36
+ }
37
+ );
38
+ }
39
+
10
40
  const sitemap = await fetch(
11
- `https://s3.eu-central-1.amazonaws.com/0fb534/sitemaps/sitemaps/sitemap-${node}.xml.gz`
41
+ `https://s3.eu-central-1.amazonaws.com/${s3BucketName}/sitemaps/sitemaps/sitemap-${node}.xml.gz`
12
42
  );
43
+
44
+ if (!sitemap.ok) {
45
+ return new Response(
46
+ JSON.stringify({
47
+ error: 'Sitemap not found',
48
+ message: `Failed to fetch sitemap for node: ${node}`
49
+ }),
50
+ {
51
+ status: 503,
52
+ headers: {
53
+ 'Content-Type': 'application/json'
54
+ }
55
+ }
56
+ );
57
+ }
58
+
13
59
  let sitemapContent = await sitemap.text();
14
60
 
15
61
  sitemapContent = sitemapContent.replace(
@@ -2,48 +2,176 @@
2
2
  @use './fonts/index.scss' as fonts;
3
3
  @use './fonts/pz-icon.css' as icons;
4
4
 
5
- @tailwind base;
6
- @tailwind components;
7
- @tailwind utilities;
8
-
9
- @layer utilities {
10
- .header-grid-template-areas {
11
- grid-template-areas:
12
- 'band-l band-m band-r'
13
- 'main-l main-m main-r'
14
- 'nav nav nav';
15
- }
5
+ @import 'tailwindcss';
16
6
 
17
- .header-grid-area-band-l {
18
- grid-area: band-l;
19
- }
7
+ @plugin '@tailwindcss/typography';
20
8
 
21
- .header-grid-area-band-m {
22
- grid-area: band-m;
23
- }
9
+ @config '../../tailwind.config.js';
24
10
 
25
- .header-grid-area-band-r {
26
- grid-area: band-r;
27
- }
11
+ @source '../app/**/*.{js,ts,jsx,tsx}';
12
+ @source '../pages/**/*.{js,ts,jsx,tsx}';
13
+ @source '../components/**/*.{js,ts,jsx,tsx}';
14
+ @source '../views/**/*.{js,ts,jsx,tsx}';
15
+ @source '../widgets/**/*.{js,ts,jsx,tsx}';
16
+ @source '../hooks/**/*.{js,ts,jsx,tsx}';
17
+ @source '../utils/**/*.{js,ts,jsx,tsx}';
28
18
 
29
- .header-grid-area-main-l {
30
- grid-area: main-l;
31
- }
19
+ @theme {
20
+ --font-sans: 'Jost', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
21
+ 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
22
+ --font-size-2xs: 0.5rem;
23
+ --outline-off: none;
32
24
 
33
- .header-grid-area-main-m {
34
- grid-area: main-m;
35
- }
25
+ --width-1-10: 10%;
26
+ --width-2-10: 20%;
27
+ --width-3-10: 30%;
28
+ --width-4-10: 40%;
29
+ --width-5-10: 50%;
30
+ --width-6-10: 60%;
31
+ --width-7-10: 70%;
32
+ --width-8-10: 80%;
33
+ --width-9-10: 90%;
36
34
 
37
- .header-grid-area-main-r {
38
- grid-area: main-r;
39
- }
35
+ --transition-property-max-width: max-width;
36
+
37
+ --background-image-skeleton-shimmer: linear-gradient(
38
+ 90deg,
39
+ #d7d7d7 0%,
40
+ #ebebeb 40%,
41
+ #eeeeee 60%,
42
+ #d7d7d7
43
+ );
40
44
 
41
- .header-grid-area-nav {
42
- grid-area: nav;
45
+ --animate-skeleton-shimmer: skeleton-shimmer 2s linear infinite;
46
+
47
+ @keyframes skeleton-shimmer {
48
+ to {
49
+ transform: translateX(100%);
50
+ }
43
51
  }
44
52
 
45
- .header-m-template-cols {
46
- grid-template-columns: auto 1fr 1fr;
53
+ --color-transparent: transparent;
54
+ --color-white: #ffffff;
55
+ --color-primary: #000000;
56
+ --color-primary-hover: #181818;
57
+ --color-primary-foreground: #ffffff;
58
+ --color-primary-100: #525252;
59
+ --color-primary-200: #404040;
60
+ --color-primary-300: #3d3d3d;
61
+ --color-primary-400: #333333;
62
+ --color-primary-500: #2d2d2d;
63
+ --color-primary-600: #292929;
64
+ --color-primary-700: #2b2b2b;
65
+ --color-primary-800: #181818;
66
+ --color-primary-900: #000000;
67
+
68
+ --color-secondary: #e95151;
69
+ --color-secondary-hover: #d03838;
70
+ --color-secondary-foreground: #ffffff;
71
+ --color-secondary-100: #ffb7b7;
72
+ --color-secondary-200: #ff9e9e;
73
+ --color-secondary-300: #ff8484;
74
+ --color-secondary-400: #ff6b6b;
75
+ --color-secondary-500: #e95151;
76
+ --color-secondary-600: #d72b01;
77
+ --color-secondary-700: #b61e1e;
78
+ --color-secondary-800: #9d0505;
79
+ --color-secondary-900: #830000;
80
+
81
+ --color-black: #000000;
82
+ --color-black-100: #525252;
83
+ --color-black-200: #404040;
84
+ --color-black-300: #3d3d3d;
85
+ --color-black-400: #333333;
86
+ --color-black-500: #2d2d2d;
87
+ --color-black-600: #292929;
88
+ --color-black-700: #2b2b2b;
89
+ --color-black-800: #181818;
90
+ --color-black-900: #000000;
91
+
92
+ --color-gray: #ebebeb;
93
+ --color-gray-25: #fdfdfd;
94
+ --color-gray-50: #f7f7f7;
95
+ --color-gray-100: #f5f5f5;
96
+ --color-gray-150: #f4f4f4;
97
+ --color-gray-200: #eeeeee;
98
+ --color-gray-300: #ebebeb;
99
+ --color-gray-400: #d7d7d7;
100
+ --color-gray-450: #d4d4d4;
101
+ --color-gray-500: #c9c9c9;
102
+ --color-gray-600: #9d9d9d;
103
+ --color-gray-700: #686868;
104
+ --color-gray-800: #615f62;
105
+ --color-gray-850: #58585a;
106
+ --color-gray-900: #4a4f54;
107
+ --color-gray-950: #424242;
108
+
109
+ --color-error: #d72b01;
110
+ --color-error-100: #e20008;
111
+
112
+ --color-success: #7b9d76;
113
+ --color-success-100: #7b9d76;
114
+
115
+ --container-center: true;
116
+ --container-padding: 0rem;
117
+ --container-padding-sm: 2rem;
118
+ --container-padding-2xl: 0rem;
119
+
120
+ --breakpoint-xs: 575px;
121
+ --breakpoint-sm: 640px;
122
+ --breakpoint-md: 768px;
123
+ --breakpoint-lg: 1024px;
124
+ --breakpoint-xl: 1170px;
125
+ --breakpoint-2xl: 1370px;
126
+ }
127
+
128
+ @utility header-grid-template-areas {
129
+ grid-template-areas:
130
+ 'band-l band-m band-r'
131
+ 'main-l main-m main-r'
132
+ 'nav nav nav';
133
+ }
134
+
135
+ @utility header-grid-area-band-l {
136
+ grid-area: band-l;
137
+ }
138
+
139
+ @utility header-grid-area-band-m {
140
+ grid-area: band-m;
141
+ }
142
+
143
+ @utility header-grid-area-band-r {
144
+ grid-area: band-r;
145
+ }
146
+
147
+ @utility header-grid-area-main-l {
148
+ grid-area: main-l;
149
+ }
150
+
151
+ @utility header-grid-area-main-m {
152
+ grid-area: main-m;
153
+ }
154
+
155
+ @utility header-grid-area-main-r {
156
+ grid-area: main-r;
157
+ }
158
+
159
+ @utility header-grid-area-nav {
160
+ grid-area: nav;
161
+ }
162
+
163
+ @utility header-m-template-cols {
164
+ grid-template-columns: auto 1fr 1fr;
165
+ }
166
+
167
+ @utility container {
168
+ margin-inline: auto;
169
+ }
170
+
171
+ @layer base {
172
+ input::placeholder,
173
+ textarea::placeholder {
174
+ color: theme(--color-gray-400);
47
175
  }
48
176
 
49
177
  input {
@@ -51,7 +179,7 @@
51
179
  &:not(:placeholder-shown),
52
180
  &:autofill {
53
181
  & + label {
54
- @apply top-1/3;
182
+ top: calc(1 / 3 * 100%);
55
183
  }
56
184
  }
57
185
  }
@@ -5,7 +5,7 @@ describe('Badge Component', () => {
5
5
  let badge;
6
6
 
7
7
  beforeEach(() => {
8
- render(<Badge className="text-md rounded-sm">%90 off</Badge>);
8
+ render(<Badge className="text-md rounded-xs">%90 off</Badge>);
9
9
 
10
10
  badge = screen.getByText('%90 off');
11
11
  });
@@ -15,6 +15,6 @@ describe('Badge Component', () => {
15
15
  });
16
16
 
17
17
  it('should be classname checked', () => {
18
- expect(badge).toHaveClass('text-md rounded-sm');
18
+ expect(badge).toHaveClass('text-md rounded-xs');
19
19
  });
20
20
  });
@@ -34,7 +34,7 @@ export const Accordion = ({
34
34
  return (
35
35
  <div
36
36
  className={twMerge(
37
- 'flex flex-col justify-center border-b pb-4 mb-4 last:border-none',
37
+ 'flex flex-col justify-center border-b border-gray-200 pb-4 mb-4 last:border-none',
38
38
  className
39
39
  )}
40
40
  >
@@ -1,46 +1,61 @@
1
1
  'use client';
2
2
 
3
+ import { Link } from '@theme/components';
3
4
  import { ButtonProps } from '@theme/components/types';
4
5
  import clsx from 'clsx';
5
6
  import { twMerge } from 'tailwind-merge';
6
7
 
7
8
  export const Button = (props: ButtonProps) => {
8
- return (
9
- <button
10
- {...props}
11
- className={twMerge(
12
- clsx(
13
- [
14
- 'px-4',
15
- 'h-10',
16
- 'text-xs',
17
- 'bg-primary',
18
- 'text-primary-foreground',
19
- 'border',
20
- 'border-primary',
21
- 'transition-all',
22
- 'hover:bg-white',
23
- 'hover:border-primary',
24
- 'hover:text-primary'
25
- ],
26
- props.appearance === 'outlined' && [
27
- 'bg-transparent ',
28
- 'text-primary ',
29
- 'hover:bg-primary ',
30
- 'hover:text-primary-foreground'
31
- ],
32
- props.appearance === 'ghost' && [
33
- 'bg-transparent',
34
- 'border-transparent',
35
- 'text-primary',
36
- 'hover:bg-primary',
37
- 'hover:text-primary-foreground'
38
- ]
39
- ),
40
- props.className
41
- )}
9
+ const {
10
+ appearance = 'filled',
11
+ size = 'md',
12
+ href,
13
+ target,
14
+ children,
15
+ className,
16
+ ...rest
17
+ } = props;
18
+
19
+ const variants = {
20
+ filled:
21
+ 'bg-primary text-primary-foreground border border-primary hover:bg-white hover:border-primary hover:text-primary',
22
+ outlined:
23
+ 'bg-transparent text-primary hover:bg-primary hover:text-primary-foreground',
24
+ ghost:
25
+ 'bg-transparent border-transparent text-primary hover:bg-primary hover:text-primary-foreground',
26
+ link: 'px-0 h-auto underline underline-offset-2'
27
+ };
28
+
29
+ const sizes = {
30
+ sm: 'h-8',
31
+ md: 'h-10',
32
+ lg: 'h-12',
33
+ xl: 'h-14'
34
+ };
35
+
36
+ const buttonClasses = twMerge(
37
+ clsx(
38
+ 'px-4 text-xs transition-all duration-200 cursor-pointer',
39
+ 'inline-flex gap-2 justify-center items-center',
40
+ variants[appearance],
41
+ sizes[size],
42
+ className
43
+ ),
44
+ className
45
+ );
46
+
47
+ return props.href ? (
48
+ <Link
49
+ prefetch={false}
50
+ target={target}
51
+ href={href}
52
+ className={buttonClasses}
42
53
  >
43
- {props.children}
54
+ {children}
55
+ </Link>
56
+ ) : (
57
+ <button data-testid="button" {...rest} className={buttonClasses}>
58
+ {children}
44
59
  </button>
45
60
  );
46
61
  };
@@ -9,6 +9,7 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>((props, ref) => {
9
9
  <label className={twMerge('flex flex-col text-xs', props.className)}>
10
10
  <div className="flex items-center">
11
11
  <input
12
+ data-testid="checkbox"
12
13
  type="checkbox"
13
14
  {...rest}
14
15
  ref={ref}
@@ -1,8 +1,50 @@
1
+ import { useState } from 'react';
1
2
  import { forwardRef } from 'react';
2
3
  import { FileInputProps } from '@theme/components/types';
4
+ import clsx from 'clsx';
5
+ import { useLocalization } from '@akinon/next/hooks';
3
6
 
4
7
  export const FileInput = forwardRef<HTMLInputElement, FileInputProps>(
5
- function fileInput(props, ref) {
6
- return <input type="file" {...props} ref={ref} />;
8
+ function FileInput({ className, onChange, ...props }, ref) {
9
+ const { t } = useLocalization();
10
+ const [fileNames, setFileNames] = useState<string[]>([]);
11
+
12
+ const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
13
+ const files = Array.from(event.target.files || []);
14
+ setFileNames(files.map((file) => file.name));
15
+
16
+ if (onChange) {
17
+ onChange(event);
18
+ }
19
+ };
20
+
21
+ return (
22
+ <div className="relative">
23
+ <input
24
+ type="file"
25
+ {...props}
26
+ ref={ref}
27
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
28
+ onChange={handleFileChange}
29
+ />
30
+ <button
31
+ type="button"
32
+ className={clsx('bg-primary text-white py-2 px-4 text-sm', className)}
33
+ >
34
+ {t('common.file_input.select_file')}
35
+ </button>
36
+ <div className="mt-1 text-gray-500">
37
+ {fileNames.length > 0 ? (
38
+ <ul className="list-disc pl-4 text-xs">
39
+ {fileNames.map((name, index) => (
40
+ <li key={index}>{name}</li>
41
+ ))}
42
+ </ul>
43
+ ) : (
44
+ <span className="text-xs">{t('common.file_input.no_file')}</span>
45
+ )}
46
+ </div>
47
+ </div>
48
+ );
7
49
  }
8
50
  );
@@ -39,8 +39,8 @@ export const Input = forwardRef<
39
39
  const hasFloatingLabel = label && labelStyle === 'floating';
40
40
  const inputClass = twMerge(
41
41
  clsx(
42
- 'text-xs border px-2.5 h-10 placeholder:text-gray-600 peer',
43
- 'focus-visible:outline-none', // disable outline on focus
42
+ 'text-xs border px-2.5 h-10 placeholder:text-gray-600 peer disabled:bg-gray-50',
43
+ 'focus-visible:outline-hidden', // disable outline on focus
44
44
  error
45
45
  ? 'border-error focus:border-error'
46
46
  : 'border-gray-500 hover:border-black focus:border-black'
@@ -68,7 +68,7 @@ export const Input = forwardRef<
68
68
  className={twMerge(
69
69
  'text-xs text-gray-800 transition-all',
70
70
  clsx({
71
- 'absolute left-2.5 pointer-events-none transform flex items-center h-full !top-0 peer-placeholder-shown:-translate-y-2 peer-placeholder-shown:bg-white peer-placeholder-shown:inline-flex peer-placeholder-shown:h-auto':
71
+ 'absolute left-2.5 pointer-events-none transform flex items-center h-full top-0! peer-placeholder-shown:-translate-y-2 peer-placeholder-shown:bg-white peer-placeholder-shown:inline-flex peer-placeholder-shown:h-auto':
72
72
  hasFloatingLabel,
73
73
  'mb-2': !hasFloatingLabel,
74
74
  '-translate-y-2 bg-white inline-flex h-auto':
@@ -38,7 +38,7 @@ export const Modal = (props: ModalProps) => {
38
38
 
39
39
  return (
40
40
  <ReactPortal wrapperId={portalId}>
41
- <div className="fixed top-0 left-0 w-screen h-screen bg-primary bg-opacity-60 z-50" />
41
+ <div className="fixed top-0 left-0 w-screen h-screen bg-primary/60 z-50" />
42
42
  <section
43
43
  className={twMerge(
44
44
  'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-50 bg-white',
@@ -41,14 +41,14 @@ const Select = forwardRef<HTMLSelectElement, SelectProps>((props, ref) => {
41
41
  ref={ref}
42
42
  className={twMerge(
43
43
  clsx(
44
- 'cursor-pointer truncate h-10 w-40 px-2.5 shrink-0 outline-none',
44
+ 'cursor-pointer truncate h-10 w-40 px-2.5 shrink-0 outline-hidden',
45
45
  !borderless &&
46
46
  'border border-gray-200 transition-all duration-150 hover:border-primary'
47
47
  ),
48
48
  className
49
49
  )}
50
50
  >
51
- {options.map((option) => (
51
+ {options?.map((option) => (
52
52
  <option
53
53
  key={option.value}
54
54
  value={option.value}
@@ -12,7 +12,7 @@ export const Shimmer: React.FC<ShimmerProps> = ({ className }) => {
12
12
  >
13
13
  <div
14
14
  className={twMerge(
15
- 'w-full h-full bg-black bg-opacity-20 shadow-[0 0 30px 30px rgba(255,255,255,0.2)]',
15
+ 'w-full h-full bg-black/20 shadow-[0 0 30px 30px rgba(255,255,255,0.2)]',
16
16
  className
17
17
  )}
18
18
  ></div>
@@ -39,7 +39,7 @@ export const Tabs = (props: Props) => {
39
39
  tabBarPosition === 'left' || tabBarPosition === 'right'
40
40
  })}
41
41
  >
42
- {children.map((item, index) => (
42
+ {children?.map((item, index) => (
43
43
  <Tab
44
44
  key={item.props.title}
45
45
  title={item.props.title}
@@ -52,7 +52,7 @@ export const Tabs = (props: Props) => {
52
52
  ))}
53
53
  </ul>
54
54
 
55
- <div className="w-full">{children[selectedTabIndex]}</div>
55
+ <div className="w-full">{children?.[selectedTabIndex]}</div>
56
56
  </div>
57
57
  );
58
58
  };