@akinon/projectzero 2.0.0-beta.0 → 2.0.0-beta.10

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 (121) 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 +250 -0
  5. package/app-template/README.md +6 -0
  6. package/app-template/config/prebuild-tests.json +5 -0
  7. package/app-template/next-env.d.ts +1 -1
  8. package/app-template/{next.config.mjs → next.config.ts} +6 -3
  9. package/app-template/package.json +43 -40
  10. package/app-template/postcss.config.mjs +8 -0
  11. package/app-template/public/locales/en/account.json +4 -0
  12. package/app-template/public/locales/en/common.json +10 -0
  13. package/app-template/public/locales/tr/account.json +4 -0
  14. package/app-template/public/locales/tr/common.json +10 -0
  15. package/app-template/src/__tests__/middleware-matcher.test.ts +135 -0
  16. package/app-template/src/app/[commerce]/[locale]/[currency]/[...prettyurl]/page.tsx +6 -4
  17. package/app-template/src/app/[commerce]/[locale]/[currency]/account/orders/[id]/cancellation/page.tsx +98 -7
  18. package/app-template/src/app/[commerce]/[locale]/[currency]/account/orders/[id]/page.tsx +71 -7
  19. package/app-template/src/app/[commerce]/[locale]/[currency]/account/page.tsx +1 -1
  20. package/app-template/src/app/[commerce]/[locale]/[currency]/account/profile/page.tsx +1 -1
  21. package/app-template/src/app/[commerce]/[locale]/[currency]/address/stores/page.tsx +2 -2
  22. package/app-template/src/app/[commerce]/[locale]/[currency]/auth/page.tsx +1 -1
  23. package/app-template/src/app/[commerce]/[locale]/[currency]/basket/page.tsx +2 -2
  24. package/app-template/src/app/[commerce]/[locale]/[currency]/category/[pk]/page.tsx +4 -1
  25. package/app-template/src/app/[commerce]/[locale]/[currency]/error.tsx +12 -15
  26. package/app-template/src/app/[commerce]/[locale]/[currency]/flat-page/[pk]/page.tsx +3 -1
  27. package/app-template/src/app/[commerce]/[locale]/[currency]/forms/[pk]/generate/page.tsx +5 -3
  28. package/app-template/src/app/[commerce]/[locale]/[currency]/group-product/[pk]/page.tsx +7 -5
  29. package/app-template/src/app/[commerce]/[locale]/[currency]/landing-page/[pk]/page.tsx +3 -1
  30. package/app-template/src/app/[commerce]/[locale]/[currency]/layout.tsx +3 -1
  31. package/app-template/src/app/[commerce]/[locale]/[currency]/list/page.tsx +3 -1
  32. package/app-template/src/app/[commerce]/[locale]/[currency]/{pz-not-found/page.tsx → not-found.tsx} +2 -2
  33. package/app-template/src/app/[commerce]/[locale]/[currency]/orders/checkout/page.tsx +7 -4
  34. package/app-template/src/app/[commerce]/[locale]/[currency]/orders/completed/[token]/page.tsx +3 -7
  35. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/page.tsx +8 -5
  36. package/app-template/src/app/[commerce]/[locale]/[currency]/special-page/[pk]/page.tsx +3 -1
  37. package/app-template/src/app/[commerce]/[locale]/[currency]/users/email-set-primary/[[...id]]/page.tsx +4 -1
  38. package/app-template/src/app/[commerce]/[locale]/[currency]/users/registration/account-confirm-email/[[...id]]/page.tsx +3 -1
  39. package/app-template/src/app/[commerce]/[locale]/[currency]/users/reset/[[...id]]/page.tsx +11 -3
  40. package/app-template/src/app/[commerce]/[locale]/[currency]/xml-sitemap/[node]/route.ts +48 -2
  41. package/app-template/src/assets/globals.scss +162 -34
  42. package/app-template/src/components/__tests__/badge.test.tsx +2 -2
  43. package/app-template/src/components/accordion.tsx +1 -1
  44. package/app-template/src/components/button.tsx +50 -35
  45. package/app-template/src/components/file-input.tsx +44 -2
  46. package/app-template/src/components/input.tsx +3 -3
  47. package/app-template/src/components/modal.tsx +1 -1
  48. package/app-template/src/components/pwa-tags.tsx +0 -1
  49. package/app-template/src/components/select.tsx +2 -2
  50. package/app-template/src/components/shimmer.tsx +1 -1
  51. package/app-template/src/components/tabs.tsx +2 -2
  52. package/app-template/src/components/types/index.ts +4 -1
  53. package/app-template/src/middleware.ts +1 -0
  54. package/app-template/src/plugins.js +2 -1
  55. package/app-template/src/redux/middlewares/category.ts +1 -1
  56. package/app-template/src/redux/reducers/category.ts +1 -1
  57. package/app-template/src/redux/store.ts +4 -3
  58. package/app-template/src/settings.js +1 -2
  59. package/app-template/src/utils/convert-facet-search-params.ts +1 -1
  60. package/app-template/src/views/account/address-form.tsx +2 -2
  61. package/app-template/src/views/account/contact-form.tsx +4 -9
  62. package/app-template/src/views/account/content-header.tsx +2 -3
  63. package/app-template/src/views/account/order.tsx +1 -1
  64. package/app-template/src/views/account/orders/order-cancellation-item.tsx +5 -4
  65. package/app-template/src/views/anonymous-tracking/order-detail/index.tsx +1 -1
  66. package/app-template/src/views/basket/basket-item.tsx +1 -0
  67. package/app-template/src/views/category/category-active-filters.tsx +1 -1
  68. package/app-template/src/views/category/category-header.tsx +12 -6
  69. package/app-template/src/views/category/category-info.tsx +4 -4
  70. package/app-template/src/views/category/filters/index.tsx +2 -2
  71. package/app-template/src/views/checkout/auth.tsx +1 -1
  72. package/app-template/src/views/checkout/layout/header.tsx +1 -1
  73. package/app-template/src/views/checkout/steps/payment/index.tsx +1 -1
  74. package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +1 -1
  75. package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +4 -4
  76. package/app-template/src/views/checkout/steps/shipping/address-box.tsx +3 -3
  77. package/app-template/src/views/checkout/steps/shipping/addresses.tsx +1 -1
  78. package/app-template/src/views/checkout/summary.tsx +2 -2
  79. package/app-template/src/views/guest-login/index.tsx +1 -1
  80. package/app-template/src/views/header/action-menu.tsx +6 -3
  81. package/app-template/src/views/header/band.tsx +2 -2
  82. package/app-template/src/views/header/mini-basket.tsx +16 -5
  83. package/app-template/src/views/header/mobile-menu.tsx +6 -6
  84. package/app-template/src/views/header/navbar.tsx +1 -1
  85. package/app-template/src/views/header/pwa-back-button.tsx +1 -1
  86. package/app-template/src/views/header/search/index.tsx +16 -4
  87. package/app-template/src/views/header/search/results.tsx +1 -1
  88. package/app-template/src/views/header/user-menu.tsx +3 -1
  89. package/app-template/src/views/installment-options/index.tsx +1 -1
  90. package/app-template/src/views/login/index.tsx +30 -6
  91. package/app-template/src/views/otp-login/index.tsx +13 -15
  92. package/app-template/src/views/product/product-info.tsx +2 -2
  93. package/app-template/src/views/product/slider.tsx +1 -1
  94. package/app-template/src/views/product-pointer-banner-item.tsx +1 -1
  95. package/app-template/src/views/register/index.tsx +30 -5
  96. package/app-template/src/views/sales-contract-modal/index.tsx +17 -17
  97. package/app-template/src/widgets/footer-info.tsx +1 -1
  98. package/app-template/src/widgets/footer-menu.tsx +1 -1
  99. package/app-template/src/widgets/footer-subscription/index.tsx +1 -1
  100. package/app-template/src/widgets/home-stories-eng.tsx +1 -1
  101. package/app-template/tailwind.config.js +1 -137
  102. package/codemods/sentry-9/index.js +30 -0
  103. package/codemods/sentry-9/remove-sentry-configs.js +14 -0
  104. package/codemods/sentry-9/remove-sentry-dependency.js +25 -0
  105. package/codemods/sentry-9/replace-error-page.js +32 -0
  106. package/commands/codemod.ts +18 -0
  107. package/commands/index.ts +3 -1
  108. package/commands/plugins.ts +4 -0
  109. package/dist/codemods/sentry-9/templates/error.js +14 -0
  110. package/dist/commands/codemod.js +16 -0
  111. package/dist/commands/commerce-url.js +17 -7
  112. package/dist/commands/create.js +17 -7
  113. package/dist/commands/index.js +3 -1
  114. package/dist/commands/plugins.js +21 -7
  115. package/package.json +1 -1
  116. package/app-template/postcss.config.js +0 -6
  117. package/app-template/sentry.client.config.ts +0 -16
  118. package/app-template/sentry.edge.config.ts +0 -3
  119. package/app-template/sentry.properties +0 -4
  120. package/app-template/sentry.server.config.ts +0 -3
  121. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/loading.tsx +0 -67
@@ -48,11 +48,13 @@ async function RootLayout({
48
48
  translations,
49
49
  children
50
50
  }: RootLayoutProps) {
51
+ const layoutParams = await params;
52
+
51
53
  return (
52
54
  <html lang={locale.isoCode} {...(locale.rtl ? { dir: 'rtl' } : {})}>
53
55
  <head />
54
56
  <body className="overflow-x-hidden">
55
- <PzRoot translations={translations} {...params}>
57
+ <PzRoot translations={translations} {...layoutParams}>
56
58
  <ClientRoot>
57
59
  <div className="overflow-x-hidden">
58
60
  <MobileAppToggler>
@@ -3,7 +3,9 @@ import { withSegmentDefaults } from '@akinon/next/hocs/server';
3
3
  import { PageProps } from '@akinon/next/types';
4
4
  import CategoryLayout from '@theme/views/category/layout';
5
5
 
6
- async function Page({ searchParams }: PageProps) {
6
+ async function Page(props: PageProps) {
7
+ const searchParams = await props.searchParams;
8
+
7
9
  const data = await getListData({ searchParams });
8
10
 
9
11
  return (
@@ -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,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useRef } from 'react';
3
+ import { useEffect, useRef, use } from 'react';
4
4
  import { useAppDispatch } from '@akinon/next/redux/hooks';
5
5
  import { setCurrentStep } from '@akinon/next/redux/reducers/checkout';
6
6
  import { ROUTES } from '@theme/routes';
@@ -14,14 +14,10 @@ import { Image } from '@akinon/next/components/image';
14
14
  import { Trans } from '@akinon/next/components/trans';
15
15
  import { pushPurchaseEvent } from '@theme/utils/gtm';
16
16
 
17
- const CheckoutCompleted = ({
18
- params
19
- }: PageProps<{
20
- token: string;
21
- }>) => {
17
+ const CheckoutCompleted = (props) => {
22
18
  const { t } = useLocalization();
23
19
  const dispatch = useAppDispatch();
24
-
20
+ const params = use(props.params) as { token: string };
25
21
  const { data, isLoading } = useFetchCheckoutResultQuery(String(params.token));
26
22
 
27
23
  const carouselRef = useRef(null);
@@ -5,12 +5,12 @@ import { generateJsonLd } from '@theme/utils/generate-jsonld';
5
5
  import { AccordionWrapper, ProductInfo } from '@theme/views/product';
6
6
  import ProductLayout from '@theme/views/product/layout';
7
7
 
8
- export async function generateMetadata({
9
- params,
10
- searchParams
11
- }: PageProps<{ pk: number }>) {
8
+ export async function generateMetadata(props: PageProps): Promise<Metadata> {
12
9
  let result: Metadata = {};
13
10
 
11
+ const params = await props.params;
12
+ const searchParams = await props.searchParams;
13
+
14
14
  try {
15
15
  const {
16
16
  data: { product }
@@ -40,7 +40,10 @@ export async function generateMetadata({
40
40
  return result;
41
41
  }
42
42
 
43
- async function Page({ params, searchParams }: PageProps<{ pk: number }>) {
43
+ async function Page(props: PageProps<{ pk: number }>) {
44
+ const params = await props.params;
45
+ const searchParams = await props.searchParams;
46
+
44
47
  const [{ data, breadcrumbData }, deliveryReturn] = await Promise.all([
45
48
  getProductData({
46
49
  pk: params.pk,
@@ -5,7 +5,9 @@ import CategoryLayout from '@theme/views/category/layout';
5
5
  import SpecialPageBanner from '@theme/widgets/special-page-banner';
6
6
  import SpecialPageCarousel from '@theme/widgets/special-page-carousel';
7
7
 
8
- async function Page({ params, searchParams }: PageProps<{ pk: number }>) {
8
+ async function Page(props: PageProps<{ pk: number }>) {
9
+ const params = await props.params;
10
+ const searchParams = await props.searchParams;
9
11
  const data = await getSpecialPageData({ pk: params.pk, searchParams });
10
12
 
11
13
  return (
@@ -2,8 +2,11 @@
2
2
  import { Icon, Link, LoaderSpinner } from '@theme/components';
3
3
  import { useLocalization } from '@akinon/next/hooks';
4
4
  import { useChangeEmailVerificationQuery } from '@akinon/next/data/client/user';
5
+ import { use } from 'react';
6
+
7
+ export default function Page(props) {
8
+ const { id } = use(props.params) as { id: string[] };
5
9
 
6
- export default function Page({ params: { id } }) {
7
10
  const { t } = useLocalization();
8
11
  const { isSuccess, isLoading } = useChangeEmailVerificationQuery(
9
12
  id.join('/')
@@ -2,8 +2,10 @@
2
2
  import { Icon, Link, LoaderSpinner } from '@theme/components';
3
3
  import { useLocalization } from '@akinon/next/hooks';
4
4
  import { useConfirmEmailVerificationQuery } from '@akinon/next/data/client/user';
5
+ import { use } from 'react';
5
6
 
6
- export default function Page({ params: { id } }) {
7
+ export default function Page(props) {
8
+ const { id } = use(props.params) as { id: string[] };
7
9
  const { t } = useLocalization();
8
10
  const { isSuccess, isLoading } = useConfirmEmailVerificationQuery(
9
11
  id.join('/')
@@ -10,8 +10,10 @@ import {
10
10
  } from '@akinon/next/data/client/account';
11
11
  import { useLocalization } from '@akinon/next/hooks';
12
12
  import PasswordRulesFeedback from '@theme/components/password-rules-feedback';
13
+ import { use } from 'react';
13
14
 
14
- export default function NewPassword({ params: { id } }) {
15
+ export default function NewPassword(props) {
16
+ const { id } = use(props.params) as { id: string };
15
17
  const { t } = useLocalization();
16
18
  const [newPassword, { isSuccess: formSuccess }] = usePasswordResetMutation();
17
19
 
@@ -24,7 +26,10 @@ export default function NewPassword({ params: { id } }) {
24
26
  /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z\d]).{6,}$/,
25
27
  t('forgot_password.create_new_password.form.error.password_rule')
26
28
  )
27
- .max(50, t('forgot_password.create_new_password.form.error.password_max')),
29
+ .max(
30
+ 50,
31
+ t('forgot_password.create_new_password.form.error.password_max')
32
+ ),
28
33
  repeatPassword: yup
29
34
  .string()
30
35
  .required(t('forgot_password.create_new_password.form.error.required'))
@@ -79,7 +84,10 @@ export default function NewPassword({ params: { id } }) {
79
84
  required
80
85
  />
81
86
 
82
- <PasswordRulesFeedback password={passwordValue} isVisible={errors?.password ? true : false}/>
87
+ <PasswordRulesFeedback
88
+ password={passwordValue}
89
+ isVisible={errors?.password ? true : false}
90
+ />
83
91
  </div>
84
92
 
85
93
  <div className="mb-2 text-left">
@@ -1,15 +1,61 @@
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 }) {
6
- const node = context.params.node;
19
+ const node = (await context.params).node;
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',
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 {...rest} className={buttonClasses}>
58
+ {children}
44
59
  </button>
45
60
  );
46
61
  };
@@ -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
  );