@jarwizz/create-jarshop 0.1.1 → 0.1.3

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/README.md +10 -2
  2. package/dist/generator.js +10 -4
  3. package/dist/generator.js.map +1 -1
  4. package/dist/template/AGENTS.md +3 -3
  5. package/dist/template/CONTEXT.md +14 -0
  6. package/dist/template/README.md +25 -3
  7. package/dist/template/docker-compose.yml +5 -0
  8. package/dist/template/docs/smtp-smoke.md +37 -0
  9. package/dist/template/env.example.template +18 -0
  10. package/dist/template/gitignore.template +2 -0
  11. package/dist/template/package.json +19 -6
  12. package/dist/template/pnpm-lock.yaml +2389 -98
  13. package/dist/template/pnpm-workspace.yaml +12 -1
  14. package/dist/template/src/dev-storefront.ts +58 -0
  15. package/dist/template/src/dev.ts +67 -4
  16. package/dist/template/src/email-smoke.ts +27 -0
  17. package/dist/template/src/environment.d.ts +6 -0
  18. package/dist/template/src/migrate.ts +16 -0
  19. package/dist/template/src/migrations/1786024108434-jarshop-checkout-attempt.ts +28 -0
  20. package/dist/template/src/migrations/1786024108435-jarshop-legal-and-confirmation.ts +60 -0
  21. package/dist/template/src/migrations/1786024108436-jarshop-order-email-outbox.ts +37 -0
  22. package/dist/template/src/migrations/1786030000000-jarshop-order-email-delivery-lease.ts +27 -0
  23. package/dist/template/src/plugins/jarshop-checkout/checkout-attempt.entity.ts +42 -0
  24. package/dist/template/src/plugins/jarshop-checkout/cod-payment.test.ts +44 -0
  25. package/dist/template/src/plugins/jarshop-checkout/cod-payment.ts +28 -0
  26. package/dist/template/src/plugins/jarshop-checkout/confirmation-token.entity.ts +31 -0
  27. package/dist/template/src/plugins/jarshop-checkout/confirmation.ts +59 -0
  28. package/dist/template/src/plugins/jarshop-checkout/dashboard/index.tsx +107 -0
  29. package/dist/template/src/plugins/jarshop-checkout/dashboard/order-email-delivery-status.ts +43 -0
  30. package/dist/template/src/plugins/jarshop-checkout/email-smoke-core.ts +56 -0
  31. package/dist/template/src/plugins/jarshop-checkout/index.ts +51 -0
  32. package/dist/template/src/plugins/jarshop-checkout/jarshop-checkout.plugin.ts +870 -0
  33. package/dist/template/src/plugins/jarshop-checkout/legal.entity.ts +39 -0
  34. package/dist/template/src/plugins/jarshop-checkout/order-consent.entity.ts +32 -0
  35. package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-core.ts +337 -0
  36. package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-status.ts +39 -0
  37. package/dist/template/src/plugins/jarshop-checkout/order-email-delivery.entity.ts +66 -0
  38. package/dist/template/src/plugins/jarshop-checkout/order-email-outbox.ts +108 -0
  39. package/dist/template/src/plugins/jarshop-checkout/order-email-transport.ts +35 -0
  40. package/dist/template/src/plugins/jarshop-checkout/order-email-worker.ts +281 -0
  41. package/dist/template/src/plugins/jarshop-checkout/shipping.test.ts +41 -0
  42. package/dist/template/src/plugins/jarshop-checkout/shipping.ts +162 -0
  43. package/dist/template/src/seed.ts +171 -0
  44. package/dist/template/src/vendure-config.ts +72 -7
  45. package/dist/template/static/email/templates/email-verification/body.hbs +4 -4
  46. package/dist/template/storefront/app/[locale]/[slug]/add-to-cart-form.tsx +44 -0
  47. package/dist/template/storefront/app/[locale]/[slug]/page.tsx +35 -26
  48. package/dist/template/storefront/app/[locale]/account/account-mutation-form.tsx +44 -0
  49. package/dist/template/storefront/app/[locale]/account/change-password-form.tsx +65 -0
  50. package/dist/template/storefront/app/[locale]/account/login/login-form.tsx +60 -0
  51. package/dist/template/storefront/app/[locale]/account/login/page.tsx +23 -0
  52. package/dist/template/storefront/app/[locale]/account/page.tsx +215 -0
  53. package/dist/template/storefront/app/[locale]/account/password-reset/callback/route.ts +33 -0
  54. package/dist/template/storefront/app/[locale]/account/password-reset/complete/page.tsx +27 -0
  55. package/dist/template/storefront/app/[locale]/account/password-reset/page.tsx +37 -0
  56. package/dist/template/storefront/app/[locale]/account/password-reset/password-reset-forms.tsx +99 -0
  57. package/dist/template/storefront/app/[locale]/account/register/page.tsx +23 -0
  58. package/dist/template/storefront/app/[locale]/account/register/register-form.tsx +67 -0
  59. package/dist/template/storefront/app/[locale]/account/verify/complete/route.ts +33 -0
  60. package/dist/template/storefront/app/[locale]/account/verify/page.tsx +41 -0
  61. package/dist/template/storefront/app/[locale]/account/verify/refresh-form.tsx +35 -0
  62. package/dist/template/storefront/app/[locale]/cart/page.tsx +78 -0
  63. package/dist/template/storefront/app/[locale]/checkout/checkout-form.tsx +112 -0
  64. package/dist/template/storefront/app/[locale]/checkout/confirmation/route.ts +21 -0
  65. package/dist/template/storefront/app/[locale]/checkout/page.tsx +44 -0
  66. package/dist/template/storefront/app/[locale]/checkout/success/page.tsx +28 -0
  67. package/dist/template/storefront/app/[locale]/layout.tsx +48 -0
  68. package/dist/template/storefront/app/[locale]/page.tsx +12 -13
  69. package/dist/template/storefront/app/globals.css +372 -4
  70. package/dist/template/storefront/app/layout.tsx +13 -0
  71. package/dist/template/storefront/eslint.config.mjs +8 -1
  72. package/dist/template/storefront/gitignore.template +1 -0
  73. package/dist/template/storefront/lib/account-security.test.ts +74 -0
  74. package/dist/template/storefront/lib/account-security.ts +81 -0
  75. package/dist/template/storefront/lib/account.ts +366 -0
  76. package/dist/template/storefront/lib/cart-contract.ts +52 -0
  77. package/dist/template/storefront/lib/cart.test.ts +30 -0
  78. package/dist/template/storefront/lib/cart.ts +130 -0
  79. package/dist/template/storefront/lib/checkout-contract.test.ts +20 -0
  80. package/dist/template/storefront/lib/checkout-contract.ts +16 -0
  81. package/dist/template/storefront/lib/checkout.ts +149 -0
  82. package/dist/template/storefront/lib/commerce-session.test.ts +77 -0
  83. package/dist/template/storefront/lib/commerce-session.ts +85 -0
  84. package/dist/template/storefront/lib/commerce.ts +129 -4
  85. package/dist/template/storefront/lib/confirmation-cookie.ts +9 -0
  86. package/dist/template/storefront/lib/confirmation.ts +95 -0
  87. package/dist/template/storefront/lib/form-values.test.ts +14 -0
  88. package/dist/template/storefront/lib/form-values.ts +7 -0
  89. package/dist/template/storefront/lib/i18n.test.ts +50 -0
  90. package/dist/template/storefront/lib/i18n.ts +44 -0
  91. package/dist/template/storefront/lib/password-reset-token.test.ts +31 -0
  92. package/dist/template/storefront/lib/password-reset-token.ts +17 -0
  93. package/dist/template/storefront/lib/valkey-rate-limit.ts +72 -0
  94. package/dist/template/storefront/messages/en.json +89 -0
  95. package/dist/template/storefront/messages/sk.json +89 -0
  96. package/dist/template/storefront/next.config.ts +3 -1
  97. package/dist/template/storefront/package.json +11 -4
  98. package/dist/template/storefront/vitest.config.ts +8 -0
  99. package/dist/template/tsconfig.json +2 -1
  100. package/dist/template/turbo.json +8 -0
  101. package/dist/template/vite.config.mts +4 -1
  102. package/dist/template-manifest.json +1 -1
  103. package/dist/template.js +3 -0
  104. package/dist/template.js.map +1 -1
  105. package/package.json +1 -1
  106. package/dist/template/storefront/pnpm-lock.yaml +0 -3562
  107. package/dist/template/storefront/pnpm-workspace.yaml +0 -26
@@ -3,7 +3,6 @@ import {
3
3
  DefaultSchedulerPlugin,
4
4
  DefaultSearchPlugin,
5
5
  VendureConfig,
6
- dummyPaymentHandler,
7
6
  } from "@vendure/core";
8
7
  import { AssetServerPlugin } from "@vendure/asset-server-plugin";
9
8
  import { DashboardPlugin } from "@vendure/dashboard/plugin";
@@ -13,10 +12,31 @@ import {
13
12
  defaultEmailHandlers,
14
13
  } from "@vendure/email-plugin";
15
14
  import { GraphiqlPlugin } from "@vendure/graphiql-plugin";
15
+ import {
16
+ JarShopCheckoutPlugin,
17
+ jarShopCodPaymentHandler,
18
+ } from "./plugins/jarshop-checkout/index.js";
16
19
  import "dotenv/config";
17
20
  import path from "node:path";
18
21
 
19
22
  const isDev = process.env.APP_ENV === "dev";
23
+ const storefrontUrl = new URL(
24
+ process.env.JARSHOP_STOREFRONT_URL ?? "http://localhost:3001",
25
+ );
26
+
27
+ function requiredEnvironment(name: string): string {
28
+ const value = process.env[name]?.trim();
29
+ if (!value) throw new Error(`${name} is required outside development`);
30
+ return value;
31
+ }
32
+
33
+ function storefrontAccountUrl(locale: string, path: string): string {
34
+ const supportedLocale = locale === "en" ? "en" : "sk";
35
+ return new URL(
36
+ `/${supportedLocale}/account/${path}`,
37
+ storefrontUrl,
38
+ ).toString();
39
+ }
20
40
 
21
41
  export const config: VendureConfig = {
22
42
  apiOptions: {
@@ -28,11 +48,23 @@ export const config: VendureConfig = {
28
48
  },
29
49
  authOptions: {
30
50
  tokenMethod: ["bearer", "cookie"],
51
+ requireVerification: true,
52
+ sessionDuration: "30d",
31
53
  superadminCredentials: {
32
54
  identifier: process.env.SUPERADMIN_USERNAME ?? "",
33
55
  password: process.env.SUPERADMIN_PASSWORD ?? "",
34
56
  },
35
- cookieOptions: { secret: process.env.COOKIE_SECRET ?? "" },
57
+ cookieOptions: {
58
+ name: {
59
+ admin: "jarshop-admin-session",
60
+ shop: "jarshop-shop-session",
61
+ },
62
+ secret: process.env.COOKIE_SECRET ?? "",
63
+ httpOnly: true,
64
+ path: "/",
65
+ sameSite: "lax",
66
+ secure: !isDev,
67
+ },
36
68
  },
37
69
  dbConnectionOptions: {
38
70
  type: "postgres",
@@ -46,8 +78,9 @@ export const config: VendureConfig = {
46
78
  username: process.env.DB_USERNAME,
47
79
  password: process.env.DB_PASSWORD,
48
80
  },
49
- paymentOptions: { paymentMethodHandlers: [dummyPaymentHandler] },
81
+ paymentOptions: { paymentMethodHandlers: [jarShopCodPaymentHandler] },
50
82
  plugins: [
83
+ JarShopCheckoutPlugin,
51
84
  GraphiqlPlugin.init(),
52
85
  AssetServerPlugin.init({
53
86
  route: "assets",
@@ -57,13 +90,45 @@ export const config: VendureConfig = {
57
90
  DefaultJobQueuePlugin.init({ useDatabaseForBuffer: true }),
58
91
  DefaultSearchPlugin.init({ bufferUpdates: false, indexStockStatus: true }),
59
92
  EmailPlugin.init({
60
- devMode: true,
61
- outputPath: path.join(__dirname, "../static/email/test-emails"),
62
- route: "mailbox",
63
- handlers: defaultEmailHandlers,
93
+ globalTemplateVars: (ctx) => ({
94
+ fromAddress:
95
+ process.env.JARSHOP_EMAIL_SMTP_FROM ?? "JarShop <noreply@localhost>",
96
+ verifyEmailAddressUrl: storefrontAccountUrl(
97
+ ctx.languageCode,
98
+ "verify/complete",
99
+ ),
100
+ passwordResetUrl: storefrontAccountUrl(
101
+ ctx.languageCode,
102
+ "password-reset/callback",
103
+ ),
104
+ }),
105
+ // JarShop owns confirmation delivery through its durable outbox. Retain
106
+ // Vendure's account and password handlers but prevent a second receipt.
107
+ handlers: defaultEmailHandlers.filter(
108
+ (handler) => handler.type !== "order-confirmation",
109
+ ),
64
110
  templateLoader: new FileBasedTemplateLoader(
65
111
  path.join(__dirname, "../static/email/templates"),
66
112
  ),
113
+ ...(isDev
114
+ ? {
115
+ devMode: true as const,
116
+ outputPath: path.join(__dirname, "../static/email/test-emails"),
117
+ route: "mailbox",
118
+ }
119
+ : {
120
+ transport: () => ({
121
+ type: "smtp" as const,
122
+ host: requiredEnvironment("JARSHOP_EMAIL_SMTP_HOST"),
123
+ port: Number(requiredEnvironment("JARSHOP_EMAIL_SMTP_PORT")),
124
+ secure:
125
+ requiredEnvironment("JARSHOP_EMAIL_SMTP_SECURE") === "true",
126
+ auth: {
127
+ user: requiredEnvironment("JARSHOP_EMAIL_SMTP_USERNAME"),
128
+ pass: requiredEnvironment("JARSHOP_EMAIL_SMTP_PASSWORD"),
129
+ },
130
+ }),
131
+ }),
67
132
  }),
68
133
  DashboardPlugin.init({
69
134
  route: "dashboard",
@@ -1,17 +1,17 @@
1
- {{> header title="Verify Your Email Address" }}
1
+ {{> header title="Complete Your JarShop Account" }}
2
2
 
3
3
  <mj-section background-color="#fafafa">
4
4
  <mj-column>
5
5
  <mj-text color="#525252">
6
- Thank you for creating an account. Click the button below to verify this email address and
7
- complete the registration process:
6
+ Click the button below to complete your account. If you have previously shopped with this
7
+ email address, this activates your sign-in; otherwise it verifies your new account:
8
8
  </mj-text>
9
9
 
10
10
  <mj-button font-family="Helvetica"
11
11
  background-color="#f45e43"
12
12
  color="white"
13
13
  href="{{ verifyEmailAddressUrl }}?token={{ verificationToken }}">
14
- Verify Me!
14
+ Complete account
15
15
  </mj-button>
16
16
  </mj-column>
17
17
  </mj-section>
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useActionState } from "react";
5
+
6
+ import type { CommerceLocale } from "../../../lib/commerce";
7
+ import type { AddToCartState } from "../../../lib/cart";
8
+ import { createTranslator } from "../../../lib/i18n";
9
+
10
+ interface AddToCartFormProps {
11
+ readonly locale: CommerceLocale;
12
+ readonly action: (
13
+ previousState: AddToCartState,
14
+ formData: FormData,
15
+ ) => Promise<AddToCartState>;
16
+ }
17
+
18
+ const initialState: AddToCartState = { status: "idle" };
19
+
20
+ export function AddToCartForm({ locale, action }: AddToCartFormProps) {
21
+ const [state, formAction, pending] = useActionState(action, initialState);
22
+ const t = createTranslator(locale);
23
+
24
+ return (
25
+ <div className="add-to-cart">
26
+ <form action={formAction}>
27
+ <button type="submit" disabled={pending}>
28
+ {pending ? "…" : t("product.addToCart")}
29
+ </button>
30
+ </form>
31
+ {state.status === "added" ? (
32
+ <div className="cart-toast" role="status">
33
+ <span>{t("product.addedToCart")}</span>
34
+ <Link href={`/${locale}/cart`}>{t("product.viewCart")}</Link>
35
+ </div>
36
+ ) : null}
37
+ {state.status === "error" ? (
38
+ <p className="form-error" role="alert">
39
+ {state.message}
40
+ </p>
41
+ ) : null}
42
+ </div>
43
+ );
44
+ }
@@ -1,49 +1,58 @@
1
1
  import { getProduct } from "../../../lib/commerce";
2
+ import { addToCart } from "../../../lib/cart";
3
+ import { createTranslator, parseLocale } from "../../../lib/i18n";
4
+ import { AddToCartForm } from "./add-to-cart-form";
2
5
 
3
6
  interface ProductPageProps {
4
7
  params: Promise<{ locale: string; slug: string }>;
5
8
  }
6
9
 
7
- const labels = {
8
- sk: {
9
- back: "Späť",
10
- notFound: "Produkt sa nenašiel.",
11
- inStock: "Skladom",
12
- outOfStock: "Vypredané",
13
- },
14
- en: {
15
- back: "Back",
16
- notFound: "Product not found.",
17
- inStock: "In stock",
18
- outOfStock: "Out of stock",
19
- },
20
- } as const;
21
-
22
10
  export default async function ProductPage({ params }: ProductPageProps) {
23
11
  const { locale, slug } = await params;
24
- const language = locale === "en" ? "en" : "sk";
12
+ const language = parseLocale(locale);
25
13
  const result = await getProduct(slug, language);
26
- const copy = labels[language];
14
+ const t = createTranslator(language);
27
15
 
28
16
  if (!result.ok || !result.data) {
29
17
  return (
30
18
  <main lang={language}>
31
- <p>{copy.notFound}</p>
19
+ <p>{t("product.notFound")}</p>
32
20
  </main>
33
21
  );
34
22
  }
35
23
 
36
24
  return (
37
- <main lang={language}>
38
- <a href={`/${language}`}>← {copy.back}</a>
39
- <h1>{result.data.name}</h1>
40
- <p>{result.data.description}</p>
41
- <ul>
25
+ <main className="page product-page" lang={language}>
26
+ <a className="back-link" href={`/${language}`}>
27
+ {t("product.back")}
28
+ </a>
29
+ <div className="product-page__intro">
30
+ <h1>{result.data.name}</h1>
31
+ <p>{result.data.description}</p>
32
+ </div>
33
+ <ul className="variant-list">
42
34
  {result.data.variants.map((variant) => (
43
35
  <li key={variant.id}>
44
- {variant.name}: {(variant.price.amount / 100).toFixed(2)}{" "}
45
- {variant.price.currencyCode} —{" "}
46
- {variant.inStock ? copy.inStock : copy.outOfStock}
36
+ <div>
37
+ <strong>{variant.name}</strong>
38
+ <span className="price">
39
+ {(variant.price.amount / 100).toFixed(2)}{" "}
40
+ {variant.price.currencyCode}
41
+ </span>
42
+ </div>
43
+ <span
44
+ className={
45
+ variant.inStock ? "stock stock--in" : "stock stock--out"
46
+ }
47
+ >
48
+ {variant.inStock ? t("product.inStock") : t("product.outOfStock")}
49
+ </span>
50
+ {variant.inStock ? (
51
+ <AddToCartForm
52
+ action={addToCart.bind(null, language, variant.id)}
53
+ locale={language}
54
+ />
55
+ ) : null}
47
56
  </li>
48
57
  ))}
49
58
  </ul>
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { useActionState } from "react";
5
+
6
+ import type { AccountActionState } from "../../../lib/account";
7
+
8
+ interface AccountMutationFormProps {
9
+ action: (
10
+ previousState: AccountActionState,
11
+ formData: FormData,
12
+ ) => Promise<AccountActionState>;
13
+ children?: ReactNode;
14
+ className?: string;
15
+ errorMessage: string;
16
+ submitLabel: string;
17
+ successMessage: string;
18
+ }
19
+
20
+ export function AccountMutationForm({
21
+ action,
22
+ children,
23
+ className,
24
+ errorMessage,
25
+ submitLabel,
26
+ successMessage,
27
+ }: AccountMutationFormProps) {
28
+ const [state, formAction, pending] = useActionState(action, {
29
+ status: "idle",
30
+ });
31
+
32
+ return (
33
+ <form action={formAction} className={className}>
34
+ {children}
35
+ {state.status === "success" ? (
36
+ <p role="status">{successMessage}</p>
37
+ ) : null}
38
+ {state.status === "error" ? <p role="alert">{errorMessage}</p> : null}
39
+ <button disabled={pending} type="submit">
40
+ {submitLabel}
41
+ </button>
42
+ </form>
43
+ );
44
+ }
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import { useActionState } from "react";
4
+
5
+ import type { AccountActionState } from "../../../lib/account";
6
+ import type { CommerceLocale } from "../../../lib/commerce";
7
+ import { createTranslator } from "../../../lib/i18n";
8
+
9
+ interface ChangePasswordFormProps {
10
+ locale: CommerceLocale;
11
+ action: (
12
+ previousState: AccountActionState,
13
+ formData: FormData,
14
+ ) => Promise<AccountActionState>;
15
+ }
16
+
17
+ export function ChangePasswordForm({
18
+ locale,
19
+ action,
20
+ }: ChangePasswordFormProps) {
21
+ const [state, formAction, pending] = useActionState(action, {
22
+ status: "idle",
23
+ });
24
+ const t = createTranslator(locale);
25
+
26
+ return (
27
+ <form action={formAction} className="checkout-form">
28
+ <label>
29
+ {t("account.currentPassword")}
30
+ <input
31
+ autoComplete="current-password"
32
+ name="currentPassword"
33
+ required
34
+ type="password"
35
+ />
36
+ </label>
37
+ <label>
38
+ {t("account.newPassword")}
39
+ <input
40
+ autoComplete="new-password"
41
+ name="newPassword"
42
+ required
43
+ type="password"
44
+ />
45
+ </label>
46
+ {state.status === "success" ? (
47
+ <p role="status">{t("account.passwordChanged")}</p>
48
+ ) : null}
49
+ {state.status === "error" ? (
50
+ <p role="alert">
51
+ {t(
52
+ state.reason === "passwordInvalid"
53
+ ? "account.passwordInvalid"
54
+ : state.reason === "invalidCredentials"
55
+ ? "account.invalidCredentials"
56
+ : "account.signInFailed",
57
+ )}
58
+ </p>
59
+ ) : null}
60
+ <button disabled={pending} type="submit">
61
+ {t("account.saveProfile")}
62
+ </button>
63
+ </form>
64
+ );
65
+ }
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ import { useActionState } from "react";
4
+
5
+ import type { LoginActionState } from "../../../../lib/account";
6
+ import type { CommerceLocale } from "../../../../lib/commerce";
7
+ import { createTranslator } from "../../../../lib/i18n";
8
+
9
+ interface LoginFormProps {
10
+ locale: CommerceLocale;
11
+ action: (
12
+ previousState: LoginActionState,
13
+ formData: FormData,
14
+ ) => Promise<LoginActionState>;
15
+ }
16
+
17
+ export function LoginForm({ locale, action }: LoginFormProps) {
18
+ const [state, formAction, pending] = useActionState(action, {
19
+ status: "idle",
20
+ });
21
+ const t = createTranslator(locale);
22
+
23
+ return (
24
+ <form action={formAction} className="checkout-form">
25
+ <label>
26
+ {t("account.email")}
27
+ <input autoComplete="email" name="emailAddress" required type="email" />
28
+ </label>
29
+ <label>
30
+ {t("account.password")}
31
+ <input
32
+ autoComplete="current-password"
33
+ name="password"
34
+ required
35
+ type="password"
36
+ />
37
+ </label>
38
+ {state.status === "error" ? (
39
+ <p role="alert">
40
+ {t(
41
+ state.reason === "invalidCredentials"
42
+ ? "account.invalidCredentials"
43
+ : "account.signInFailed",
44
+ )}
45
+ </p>
46
+ ) : null}
47
+ <button disabled={pending} type="submit">
48
+ {t("account.signIn")}
49
+ </button>
50
+ <p>
51
+ <a href={`/${locale}/account/register`}>{t("account.register")}</a>
52
+ </p>
53
+ <p>
54
+ <a href={`/${locale}/account/password-reset`}>
55
+ {t("account.requestPasswordReset")}
56
+ </a>
57
+ </p>
58
+ </form>
59
+ );
60
+ }
@@ -0,0 +1,23 @@
1
+ import { login } from "../../../../lib/account";
2
+ import { createTranslator, parseLocale } from "../../../../lib/i18n";
3
+ import { LoginForm } from "./login-form";
4
+
5
+ interface LoginPageProps {
6
+ params: Promise<{ locale: string }>;
7
+ }
8
+
9
+ export default async function LoginPage({ params }: LoginPageProps) {
10
+ const { locale } = await params;
11
+ const language = parseLocale(locale);
12
+ const t = createTranslator(language);
13
+
14
+ return (
15
+ <main className="page" lang={language}>
16
+ <div className="page-heading">
17
+ <p className="eyebrow">JarShop starter</p>
18
+ <h1>{t("account.signIn")}</h1>
19
+ </div>
20
+ <LoginForm locale={language} action={login.bind(null, language)} />
21
+ </main>
22
+ );
23
+ }
@@ -0,0 +1,215 @@
1
+ import { redirect } from "next/navigation";
2
+ import Link from "next/link";
3
+
4
+ import {
5
+ addAddress,
6
+ changePassword,
7
+ getSignedInAccountProfile,
8
+ logout,
9
+ removeAddress,
10
+ updateAddress,
11
+ updateProfile,
12
+ } from "../../../lib/account";
13
+ import { createTranslator, parseLocale } from "../../../lib/i18n";
14
+ import { ChangePasswordForm } from "./change-password-form";
15
+ import { AccountMutationForm } from "./account-mutation-form";
16
+
17
+ interface AccountPageProps {
18
+ params: Promise<{ locale: string }>;
19
+ searchParams: Promise<{ orders?: string | string[] }>;
20
+ }
21
+
22
+ export default async function AccountPage({
23
+ params,
24
+ searchParams,
25
+ }: AccountPageProps) {
26
+ const { locale } = await params;
27
+ const query = await searchParams;
28
+ const language = parseLocale(locale);
29
+ const requestedOrders =
30
+ typeof query.orders === "string" ? Number(query.orders) : 10;
31
+ const orderTake =
32
+ Number.isInteger(requestedOrders) && requestedOrders > 0
33
+ ? Math.min(requestedOrders, 1000)
34
+ : 10;
35
+ const account = await getSignedInAccountProfile(language, orderTake);
36
+ if (!account) redirect(`/${language}/account/login`);
37
+ const t = createTranslator(language);
38
+
39
+ return (
40
+ <main className="page" lang={language}>
41
+ <div className="page-heading">
42
+ <p className="eyebrow">JarShop starter</p>
43
+ <h1>{t("account.title")}</h1>
44
+ </div>
45
+ <section className="cart-summary">
46
+ <h2>{t("account.profile")}</h2>
47
+ <AccountMutationForm
48
+ action={updateProfile.bind(null, language)}
49
+ className="checkout-form"
50
+ errorMessage={t("account.saveFailed")}
51
+ submitLabel={t("account.saveProfile")}
52
+ successMessage={t("account.saved")}
53
+ >
54
+ <label>
55
+ {t("checkout.firstName")}
56
+ <input defaultValue={account.firstName} name="firstName" required />
57
+ </label>
58
+ <label>
59
+ {t("checkout.lastName")}
60
+ <input defaultValue={account.lastName} name="lastName" required />
61
+ </label>
62
+ <label>
63
+ {t("account.phone")}
64
+ <input
65
+ defaultValue={account.phoneNumber}
66
+ name="phoneNumber"
67
+ type="tel"
68
+ />
69
+ </label>
70
+ <p>
71
+ {t("account.email")}: {account.emailAddress}
72
+ </p>
73
+ </AccountMutationForm>
74
+ <form action={logout.bind(null, language)}>
75
+ <button type="submit">{t("account.signOut")}</button>
76
+ </form>
77
+ </section>
78
+ <section className="cart-summary">
79
+ <h2>{t("account.password")}</h2>
80
+ <ChangePasswordForm
81
+ locale={language}
82
+ action={changePassword.bind(null, language)}
83
+ />
84
+ </section>
85
+ <section className="cart-summary">
86
+ <h2>{t("account.addresses")}</h2>
87
+ {account.addresses.map((address) => (
88
+ <article key={address.id}>
89
+ <AccountMutationForm
90
+ action={updateAddress.bind(null, language, address.id)}
91
+ className="checkout-form"
92
+ errorMessage={t("account.saveFailed")}
93
+ submitLabel={t("account.saveProfile")}
94
+ successMessage={t("account.saved")}
95
+ >
96
+ <label>
97
+ {t("account.fullName")}
98
+ <input defaultValue={address.fullName} name="fullName" />
99
+ </label>
100
+ <label>
101
+ {t("account.street")}
102
+ <input defaultValue={address.streetLine1} name="streetLine1" />
103
+ </label>
104
+ <label>
105
+ {t("account.city")}
106
+ <input defaultValue={address.city} name="city" />
107
+ </label>
108
+ <label>
109
+ {t("account.postalCode")}
110
+ <input defaultValue={address.postalCode} name="postalCode" />
111
+ </label>
112
+ <label>
113
+ {t("account.country")}
114
+ <input defaultValue={address.countryCode} name="countryCode" />
115
+ </label>
116
+ <label>
117
+ {t("account.phone")}
118
+ <input
119
+ defaultValue={address.phoneNumber}
120
+ name="phoneNumber"
121
+ type="tel"
122
+ />
123
+ </label>
124
+ <label>
125
+ <input
126
+ defaultChecked={address.defaultShippingAddress}
127
+ name="defaultShippingAddress"
128
+ type="checkbox"
129
+ />{" "}
130
+ {t("account.defaultShippingAddress")}
131
+ </label>
132
+ <label>
133
+ <input
134
+ defaultChecked={address.defaultBillingAddress}
135
+ name="defaultBillingAddress"
136
+ type="checkbox"
137
+ />{" "}
138
+ {t("account.defaultBillingAddress")}
139
+ </label>
140
+ </AccountMutationForm>
141
+ <AccountMutationForm
142
+ action={removeAddress.bind(null, language, address.id)}
143
+ errorMessage={t("account.saveFailed")}
144
+ submitLabel={t("account.removeAddress")}
145
+ successMessage={t("account.addressRemoved")}
146
+ />
147
+ </article>
148
+ ))}
149
+ <AccountMutationForm
150
+ action={addAddress.bind(null, language)}
151
+ className="checkout-form"
152
+ errorMessage={t("account.saveFailed")}
153
+ submitLabel={t("account.addAddress")}
154
+ successMessage={t("account.addressAdded")}
155
+ >
156
+ <label>
157
+ {t("account.fullName")}
158
+ <input name="fullName" />
159
+ </label>
160
+ <label>
161
+ {t("account.street")}
162
+ <input name="streetLine1" required />
163
+ </label>
164
+ <label>
165
+ {t("account.city")}
166
+ <input name="city" />
167
+ </label>
168
+ <label>
169
+ {t("account.postalCode")}
170
+ <input name="postalCode" />
171
+ </label>
172
+ <label>
173
+ {t("account.country")}
174
+ <input defaultValue="SK" name="countryCode" required />
175
+ </label>
176
+ <label>
177
+ {t("account.phone")}
178
+ <input name="phoneNumber" type="tel" />
179
+ </label>
180
+ <label>
181
+ <input name="defaultShippingAddress" type="checkbox" />{" "}
182
+ {t("account.defaultShippingAddress")}
183
+ </label>
184
+ <label>
185
+ <input name="defaultBillingAddress" type="checkbox" />{" "}
186
+ {t("account.defaultBillingAddress")}
187
+ </label>
188
+ </AccountMutationForm>
189
+ </section>
190
+ <section className="cart-summary">
191
+ <h2>{t("account.orders")}</h2>
192
+ {account.orders.length === 0 ? (
193
+ <p>{t("account.noOrders")}</p>
194
+ ) : (
195
+ <ul>
196
+ {account.orders.map((order) => (
197
+ <li key={order.id}>
198
+ {order.code} — {order.state} —{" "}
199
+ {new Intl.NumberFormat(language === "sk" ? "sk-SK" : "en-GB", {
200
+ style: "currency",
201
+ currency: order.total.currencyCode,
202
+ }).format(order.total.amount / 100)}
203
+ </li>
204
+ ))}
205
+ </ul>
206
+ )}
207
+ {account.orders.length < account.orderTotalItems ? (
208
+ <Link href={`/${language}/account?orders=${orderTake + 10}`}>
209
+ {t("account.loadMoreOrders")}
210
+ </Link>
211
+ ) : null}
212
+ </section>
213
+ </main>
214
+ );
215
+ }