@jarwizz/create-jarshop 0.1.1 → 0.1.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.
- package/README.md +8 -0
- package/dist/generator.js +6 -3
- package/dist/generator.js.map +1 -1
- package/dist/template/CONTEXT.md +10 -0
- package/dist/template/README.md +20 -3
- package/dist/template/docker-compose.yml +5 -0
- package/dist/template/docs/smtp-smoke.md +37 -0
- package/dist/template/env.example.template +18 -0
- package/dist/template/gitignore.template +2 -0
- package/dist/template/package.json +13 -5
- package/dist/template/pnpm-lock.yaml +38 -7
- package/dist/template/src/dev-storefront.ts +58 -0
- package/dist/template/src/dev.ts +67 -4
- package/dist/template/src/email-smoke.ts +27 -0
- package/dist/template/src/environment.d.ts +6 -0
- package/dist/template/src/migrate.ts +16 -0
- package/dist/template/src/migrations/1786024108434-jarshop-checkout-attempt.ts +28 -0
- package/dist/template/src/migrations/1786024108435-jarshop-legal-and-confirmation.ts +60 -0
- package/dist/template/src/migrations/1786024108436-jarshop-order-email-outbox.ts +37 -0
- package/dist/template/src/migrations/1786030000000-jarshop-order-email-delivery-lease.ts +27 -0
- package/dist/template/src/plugins/jarshop-checkout/checkout-attempt.entity.ts +42 -0
- package/dist/template/src/plugins/jarshop-checkout/confirmation-token.entity.ts +31 -0
- package/dist/template/src/plugins/jarshop-checkout/confirmation.ts +26 -0
- package/dist/template/src/plugins/jarshop-checkout/dashboard/index.tsx +107 -0
- package/dist/template/src/plugins/jarshop-checkout/dashboard/order-email-delivery-status.ts +43 -0
- package/dist/template/src/plugins/jarshop-checkout/email-smoke-core.ts +56 -0
- package/dist/template/src/plugins/jarshop-checkout/index.ts +47 -0
- package/dist/template/src/plugins/jarshop-checkout/jarshop-checkout.plugin.ts +813 -0
- package/dist/template/src/plugins/jarshop-checkout/legal.entity.ts +39 -0
- package/dist/template/src/plugins/jarshop-checkout/order-consent.entity.ts +32 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-core.ts +337 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-status.ts +39 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-delivery.entity.ts +66 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-outbox.ts +108 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-transport.ts +35 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-worker.ts +281 -0
- package/dist/template/src/plugins/jarshop-checkout/shipping.test.ts +41 -0
- package/dist/template/src/plugins/jarshop-checkout/shipping.ts +162 -0
- package/dist/template/src/seed.ts +149 -0
- package/dist/template/src/vendure-config.ts +68 -5
- package/dist/template/static/email/templates/email-verification/body.hbs +4 -4
- package/dist/template/storefront/app/[locale]/[slug]/add-to-cart-form.tsx +44 -0
- package/dist/template/storefront/app/[locale]/[slug]/page.tsx +35 -26
- package/dist/template/storefront/app/[locale]/account/account-mutation-form.tsx +44 -0
- package/dist/template/storefront/app/[locale]/account/change-password-form.tsx +65 -0
- package/dist/template/storefront/app/[locale]/account/login/login-form.tsx +60 -0
- package/dist/template/storefront/app/[locale]/account/login/page.tsx +23 -0
- package/dist/template/storefront/app/[locale]/account/page.tsx +215 -0
- package/dist/template/storefront/app/[locale]/account/password-reset/callback/route.ts +33 -0
- package/dist/template/storefront/app/[locale]/account/password-reset/complete/page.tsx +27 -0
- package/dist/template/storefront/app/[locale]/account/password-reset/page.tsx +37 -0
- package/dist/template/storefront/app/[locale]/account/password-reset/password-reset-forms.tsx +99 -0
- package/dist/template/storefront/app/[locale]/account/register/page.tsx +23 -0
- package/dist/template/storefront/app/[locale]/account/register/register-form.tsx +67 -0
- package/dist/template/storefront/app/[locale]/account/verify/complete/route.ts +33 -0
- package/dist/template/storefront/app/[locale]/account/verify/page.tsx +41 -0
- package/dist/template/storefront/app/[locale]/account/verify/refresh-form.tsx +35 -0
- package/dist/template/storefront/app/[locale]/cart/page.tsx +78 -0
- package/dist/template/storefront/app/[locale]/checkout/checkout-form.tsx +112 -0
- package/dist/template/storefront/app/[locale]/checkout/page.tsx +44 -0
- package/dist/template/storefront/app/[locale]/checkout/success/page.tsx +23 -0
- package/dist/template/storefront/app/[locale]/layout.tsx +48 -0
- package/dist/template/storefront/app/[locale]/page.tsx +10 -11
- package/dist/template/storefront/app/globals.css +372 -4
- package/dist/template/storefront/app/layout.tsx +13 -0
- package/dist/template/storefront/eslint.config.mjs +1 -1
- package/dist/template/storefront/gitignore.template +1 -0
- package/dist/template/storefront/lib/account-security.test.ts +74 -0
- package/dist/template/storefront/lib/account-security.ts +81 -0
- package/dist/template/storefront/lib/account.ts +366 -0
- package/dist/template/storefront/lib/cart-contract.ts +52 -0
- package/dist/template/storefront/lib/cart.test.ts +30 -0
- package/dist/template/storefront/lib/cart.ts +130 -0
- package/dist/template/storefront/lib/checkout-contract.test.ts +20 -0
- package/dist/template/storefront/lib/checkout-contract.ts +16 -0
- package/dist/template/storefront/lib/checkout.ts +143 -0
- package/dist/template/storefront/lib/commerce-session.test.ts +77 -0
- package/dist/template/storefront/lib/commerce-session.ts +85 -0
- package/dist/template/storefront/lib/commerce.ts +122 -1
- package/dist/template/storefront/lib/confirmation-cookie.ts +9 -0
- package/dist/template/storefront/lib/confirmation.ts +47 -0
- package/dist/template/storefront/lib/form-values.test.ts +14 -0
- package/dist/template/storefront/lib/form-values.ts +7 -0
- package/dist/template/storefront/lib/i18n.test.ts +50 -0
- package/dist/template/storefront/lib/i18n.ts +44 -0
- package/dist/template/storefront/lib/password-reset-token.test.ts +31 -0
- package/dist/template/storefront/lib/password-reset-token.ts +17 -0
- package/dist/template/storefront/lib/valkey-rate-limit.ts +72 -0
- package/dist/template/storefront/messages/en.json +89 -0
- package/dist/template/storefront/messages/sk.json +89 -0
- package/dist/template/storefront/next.config.ts +3 -1
- package/dist/template/storefront/package.json +4 -1
- package/dist/template/storefront/pnpm-lock.yaml +756 -2
- package/dist/template/tsconfig.json +2 -1
- package/dist/template/turbo.json +1 -0
- package/dist/template/vite.config.mts +4 -1
- package/dist/template-manifest.json +1 -1
- package/dist/template.js +3 -0
- package/dist/template.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
PASSWORD_RESET_TOKEN_COOKIE,
|
|
5
|
+
passwordResetTokenCookieOptions,
|
|
6
|
+
} from "../../../../../lib/password-reset-token";
|
|
7
|
+
import { parseLocale } from "../../../../../lib/i18n";
|
|
8
|
+
|
|
9
|
+
interface PasswordResetCallbackContext {
|
|
10
|
+
params: Promise<{ locale: string }>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function GET(
|
|
14
|
+
request: NextRequest,
|
|
15
|
+
context: PasswordResetCallbackContext,
|
|
16
|
+
): Promise<NextResponse> {
|
|
17
|
+
const locale = parseLocale((await context.params).locale);
|
|
18
|
+
const resetUrl = new URL(
|
|
19
|
+
`/${locale}/account/password-reset/complete`,
|
|
20
|
+
request.url,
|
|
21
|
+
);
|
|
22
|
+
const requestUrl = new URL(`/${locale}/account/password-reset`, request.url);
|
|
23
|
+
const token = request.nextUrl.searchParams.get("token")?.trim();
|
|
24
|
+
if (!token) return NextResponse.redirect(requestUrl);
|
|
25
|
+
|
|
26
|
+
const response = NextResponse.redirect(resetUrl);
|
|
27
|
+
response.cookies.set(
|
|
28
|
+
PASSWORD_RESET_TOKEN_COOKIE,
|
|
29
|
+
token,
|
|
30
|
+
passwordResetTokenCookieOptions,
|
|
31
|
+
);
|
|
32
|
+
return response;
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { resetPassword } from "../../../../../lib/account";
|
|
2
|
+
import { createTranslator, parseLocale } from "../../../../../lib/i18n";
|
|
3
|
+
import { ResetPasswordForm } from "../password-reset-forms";
|
|
4
|
+
|
|
5
|
+
interface CompletePasswordResetPageProps {
|
|
6
|
+
params: Promise<{ locale: string }>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default async function CompletePasswordResetPage({
|
|
10
|
+
params,
|
|
11
|
+
}: CompletePasswordResetPageProps) {
|
|
12
|
+
const language = parseLocale((await params).locale);
|
|
13
|
+
const t = createTranslator(language);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<main className="page" lang={language}>
|
|
17
|
+
<div className="page-heading">
|
|
18
|
+
<p className="eyebrow">JarShop starter</p>
|
|
19
|
+
<h1>{t("account.resetPassword")}</h1>
|
|
20
|
+
</div>
|
|
21
|
+
<ResetPasswordForm
|
|
22
|
+
locale={language}
|
|
23
|
+
resetAction={resetPassword.bind(null, language)}
|
|
24
|
+
/>
|
|
25
|
+
</main>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { requestPasswordReset } from "../../../../lib/account";
|
|
2
|
+
import { createTranslator, parseLocale } from "../../../../lib/i18n";
|
|
3
|
+
import { PasswordResetRequestForm } from "./password-reset-forms";
|
|
4
|
+
import { redirect } from "next/navigation";
|
|
5
|
+
|
|
6
|
+
interface PasswordResetPageProps {
|
|
7
|
+
params: Promise<{ locale: string }>;
|
|
8
|
+
searchParams: Promise<{ token?: string | string[] }>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default async function PasswordResetPage({
|
|
12
|
+
params,
|
|
13
|
+
searchParams,
|
|
14
|
+
}: PasswordResetPageProps) {
|
|
15
|
+
const { locale } = await params;
|
|
16
|
+
const { token } = await searchParams;
|
|
17
|
+
const language = parseLocale(locale);
|
|
18
|
+
if (typeof token === "string" && token.trim()) {
|
|
19
|
+
redirect(
|
|
20
|
+
`/${language}/account/password-reset/callback?token=${encodeURIComponent(token)}`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const t = createTranslator(language);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<main className="page" lang={language}>
|
|
27
|
+
<div className="page-heading">
|
|
28
|
+
<p className="eyebrow">JarShop starter</p>
|
|
29
|
+
<h1>{t("account.resetPassword")}</h1>
|
|
30
|
+
</div>
|
|
31
|
+
<PasswordResetRequestForm
|
|
32
|
+
locale={language}
|
|
33
|
+
requestAction={requestPasswordReset.bind(null, language)}
|
|
34
|
+
/>
|
|
35
|
+
</main>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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 PasswordResetRequestFormProps {
|
|
10
|
+
locale: CommerceLocale;
|
|
11
|
+
requestAction: (
|
|
12
|
+
previousState: AccountActionState,
|
|
13
|
+
formData: FormData,
|
|
14
|
+
) => Promise<AccountActionState>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ResetPasswordFormProps {
|
|
18
|
+
locale: CommerceLocale;
|
|
19
|
+
resetAction: (
|
|
20
|
+
previousState: AccountActionState,
|
|
21
|
+
formData: FormData,
|
|
22
|
+
) => Promise<AccountActionState>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function PasswordResetRequestForm({
|
|
26
|
+
locale,
|
|
27
|
+
requestAction,
|
|
28
|
+
}: PasswordResetRequestFormProps) {
|
|
29
|
+
const [requestState, requestFormAction, requestPending] = useActionState(
|
|
30
|
+
requestAction,
|
|
31
|
+
{ status: "idle" },
|
|
32
|
+
);
|
|
33
|
+
const t = createTranslator(locale);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<form action={requestFormAction} className="checkout-form">
|
|
37
|
+
<label>
|
|
38
|
+
{t("account.email")}
|
|
39
|
+
<input autoComplete="email" name="emailAddress" required type="email" />
|
|
40
|
+
</label>
|
|
41
|
+
{requestState.status === "success" ? (
|
|
42
|
+
<p role="status">{t("account.resetSent")}</p>
|
|
43
|
+
) : null}
|
|
44
|
+
{requestState.status === "error" ? (
|
|
45
|
+
<p role="alert">{t("account.signInFailed")}</p>
|
|
46
|
+
) : null}
|
|
47
|
+
<button disabled={requestPending} type="submit">
|
|
48
|
+
{t("account.requestPasswordReset")}
|
|
49
|
+
</button>
|
|
50
|
+
</form>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function ResetPasswordForm({
|
|
55
|
+
locale,
|
|
56
|
+
resetAction,
|
|
57
|
+
}: ResetPasswordFormProps) {
|
|
58
|
+
const [state, formAction, pending] = useActionState(resetAction, {
|
|
59
|
+
status: "idle",
|
|
60
|
+
});
|
|
61
|
+
const t = createTranslator(locale);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<form action={formAction} className="checkout-form">
|
|
65
|
+
<label>
|
|
66
|
+
{t("account.newPassword")}
|
|
67
|
+
<input
|
|
68
|
+
autoComplete="new-password"
|
|
69
|
+
name="password"
|
|
70
|
+
required
|
|
71
|
+
type="password"
|
|
72
|
+
/>
|
|
73
|
+
</label>
|
|
74
|
+
<label>
|
|
75
|
+
{t("account.confirmNewPassword")}
|
|
76
|
+
<input
|
|
77
|
+
autoComplete="new-password"
|
|
78
|
+
name="confirmPassword"
|
|
79
|
+
required
|
|
80
|
+
type="password"
|
|
81
|
+
/>
|
|
82
|
+
</label>
|
|
83
|
+
{state.status === "error" ? (
|
|
84
|
+
<p role="alert">
|
|
85
|
+
{t(
|
|
86
|
+
state.reason === "passwordMismatch"
|
|
87
|
+
? "account.passwordMismatch"
|
|
88
|
+
: state.reason === "passwordInvalid"
|
|
89
|
+
? "account.passwordInvalid"
|
|
90
|
+
: "account.invalidToken",
|
|
91
|
+
)}
|
|
92
|
+
</p>
|
|
93
|
+
) : null}
|
|
94
|
+
<button disabled={pending} type="submit">
|
|
95
|
+
{t("account.resetPassword")}
|
|
96
|
+
</button>
|
|
97
|
+
</form>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { register } from "../../../../lib/account";
|
|
2
|
+
import { createTranslator, parseLocale } from "../../../../lib/i18n";
|
|
3
|
+
import { RegisterForm } from "./register-form";
|
|
4
|
+
|
|
5
|
+
interface RegisterPageProps {
|
|
6
|
+
params: Promise<{ locale: string }>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default async function RegisterPage({ params }: RegisterPageProps) {
|
|
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.register")}</h1>
|
|
19
|
+
</div>
|
|
20
|
+
<RegisterForm locale={language} action={register.bind(null, language)} />
|
|
21
|
+
</main>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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 RegisterFormProps {
|
|
10
|
+
locale: CommerceLocale;
|
|
11
|
+
action: (
|
|
12
|
+
previousState: AccountActionState,
|
|
13
|
+
formData: FormData,
|
|
14
|
+
) => Promise<AccountActionState>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function RegisterForm({ locale, action }: RegisterFormProps) {
|
|
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("checkout.firstName")}
|
|
31
|
+
<input autoComplete="given-name" name="firstName" required />
|
|
32
|
+
</label>
|
|
33
|
+
<label>
|
|
34
|
+
{t("checkout.lastName")}
|
|
35
|
+
<input autoComplete="family-name" name="lastName" required />
|
|
36
|
+
</label>
|
|
37
|
+
<label>
|
|
38
|
+
{t("account.phone")}
|
|
39
|
+
<input autoComplete="tel" name="phoneNumber" type="tel" />
|
|
40
|
+
</label>
|
|
41
|
+
<label>
|
|
42
|
+
{t("account.password")}
|
|
43
|
+
<input
|
|
44
|
+
autoComplete="new-password"
|
|
45
|
+
name="password"
|
|
46
|
+
required
|
|
47
|
+
type="password"
|
|
48
|
+
/>
|
|
49
|
+
</label>
|
|
50
|
+
{state.status === "success" ? (
|
|
51
|
+
<p role="status">{t("account.registrationSent")}</p>
|
|
52
|
+
) : null}
|
|
53
|
+
{state.status === "error" ? (
|
|
54
|
+
<p role="alert">
|
|
55
|
+
{t(
|
|
56
|
+
state.reason === "passwordInvalid"
|
|
57
|
+
? "account.passwordInvalid"
|
|
58
|
+
: "account.signInFailed",
|
|
59
|
+
)}
|
|
60
|
+
</p>
|
|
61
|
+
) : null}
|
|
62
|
+
<button disabled={pending} type="submit">
|
|
63
|
+
{t("account.register")}
|
|
64
|
+
</button>
|
|
65
|
+
</form>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
import { completeAccountVerification } from "../../../../../lib/account";
|
|
4
|
+
import { createCommerceSessionStore } from "../../../../../lib/commerce-session";
|
|
5
|
+
import { parseLocale } from "../../../../../lib/i18n";
|
|
6
|
+
|
|
7
|
+
interface VerifyAccountCallbackContext {
|
|
8
|
+
params: Promise<{ locale: string }>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function GET(
|
|
12
|
+
request: NextRequest,
|
|
13
|
+
context: VerifyAccountCallbackContext,
|
|
14
|
+
): Promise<NextResponse> {
|
|
15
|
+
const locale = parseLocale((await context.params).locale);
|
|
16
|
+
const accountUrl = new URL(`/${locale}/account`, request.url);
|
|
17
|
+
const invalidUrl = new URL(`/${locale}/account/verify`, request.url);
|
|
18
|
+
const response = NextResponse.redirect(accountUrl);
|
|
19
|
+
const session = createCommerceSessionStore({
|
|
20
|
+
get: (name) => request.cookies.get(name),
|
|
21
|
+
set: (name, value, options) => response.cookies.set(name, value, options),
|
|
22
|
+
delete: (name) => response.cookies.delete(name),
|
|
23
|
+
});
|
|
24
|
+
const token = request.nextUrl.searchParams.get("token")?.trim() ?? "";
|
|
25
|
+
const verified = await completeAccountVerification(
|
|
26
|
+
locale,
|
|
27
|
+
session,
|
|
28
|
+
token,
|
|
29
|
+
request.headers,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return verified ? response : NextResponse.redirect(invalidUrl);
|
|
33
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createTranslator, parseLocale } from "../../../../lib/i18n";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { refreshVerification } from "../../../../lib/account";
|
|
4
|
+
import { RefreshVerificationForm } from "./refresh-form";
|
|
5
|
+
|
|
6
|
+
interface VerifyPageProps {
|
|
7
|
+
params: Promise<{ locale: string }>;
|
|
8
|
+
searchParams: Promise<{ token?: string | string[] }>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default async function VerifyPage({
|
|
12
|
+
params,
|
|
13
|
+
searchParams,
|
|
14
|
+
}: VerifyPageProps) {
|
|
15
|
+
const { locale } = await params;
|
|
16
|
+
const { token } = await searchParams;
|
|
17
|
+
const language = parseLocale(locale);
|
|
18
|
+
const t = createTranslator(language);
|
|
19
|
+
|
|
20
|
+
if (typeof token === "string" && token) {
|
|
21
|
+
redirect(
|
|
22
|
+
`/${language}/account/verify/complete?token=${encodeURIComponent(token)}`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<main className="page" lang={language}>
|
|
28
|
+
<div className="page-heading">
|
|
29
|
+
<p className="eyebrow">JarShop starter</p>
|
|
30
|
+
<h1>{t("account.verify")}</h1>
|
|
31
|
+
</div>
|
|
32
|
+
<p role="alert">{t("account.invalidToken")}</p>
|
|
33
|
+
<RefreshVerificationForm
|
|
34
|
+
action={refreshVerification.bind(null, language)}
|
|
35
|
+
emailLabel={t("account.email")}
|
|
36
|
+
sentMessage={t("account.verificationSent")}
|
|
37
|
+
submitLabel={t("account.resendVerification")}
|
|
38
|
+
/>
|
|
39
|
+
</main>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useActionState } from "react";
|
|
4
|
+
|
|
5
|
+
import type { AccountActionState } from "../../../../lib/account";
|
|
6
|
+
|
|
7
|
+
interface RefreshVerificationFormProps {
|
|
8
|
+
action: (
|
|
9
|
+
previousState: AccountActionState,
|
|
10
|
+
formData: FormData,
|
|
11
|
+
) => Promise<AccountActionState>;
|
|
12
|
+
emailLabel: string;
|
|
13
|
+
sentMessage: string;
|
|
14
|
+
submitLabel: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function RefreshVerificationForm(props: RefreshVerificationFormProps) {
|
|
18
|
+
const [state, formAction, pending] = useActionState(props.action, {
|
|
19
|
+
status: "idle",
|
|
20
|
+
});
|
|
21
|
+
return (
|
|
22
|
+
<form action={formAction} className="checkout-form">
|
|
23
|
+
<label>
|
|
24
|
+
{props.emailLabel}
|
|
25
|
+
<input name="emailAddress" required type="email" />
|
|
26
|
+
</label>
|
|
27
|
+
{state.status === "success" ? (
|
|
28
|
+
<p role="status">{props.sentMessage}</p>
|
|
29
|
+
) : null}
|
|
30
|
+
<button disabled={pending} type="submit">
|
|
31
|
+
{props.submitLabel}
|
|
32
|
+
</button>
|
|
33
|
+
</form>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { getCart, removeCartLine, updateCartLine } from "../../../lib/cart";
|
|
2
|
+
import { createTranslator, parseLocale } from "../../../lib/i18n";
|
|
3
|
+
|
|
4
|
+
interface CartPageProps {
|
|
5
|
+
params: Promise<{ locale: string }>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function formatMoney(amount: number, currencyCode: string): string {
|
|
9
|
+
return `${(amount / 100).toFixed(2)} ${currencyCode}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default async function CartPage({ params }: CartPageProps) {
|
|
13
|
+
const { locale } = await params;
|
|
14
|
+
const language = parseLocale(locale);
|
|
15
|
+
const t = createTranslator(language);
|
|
16
|
+
const cart = await getCart(language);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<main className="page" lang={language}>
|
|
20
|
+
<div className="page-heading">
|
|
21
|
+
<p className="eyebrow">JarShop starter</p>
|
|
22
|
+
<h1>{t("cart.title")}</h1>
|
|
23
|
+
</div>
|
|
24
|
+
{!cart || cart.lines.length === 0 ? (
|
|
25
|
+
<p>{t("cart.empty")}</p>
|
|
26
|
+
) : (
|
|
27
|
+
<>
|
|
28
|
+
<div className="cart-layout">
|
|
29
|
+
<ul className="cart-lines">
|
|
30
|
+
{cart.lines.map((line) => (
|
|
31
|
+
<li key={line.id}>
|
|
32
|
+
<a href={`/${language}/${line.productSlug}`}>
|
|
33
|
+
{line.productName} — {line.variantName}
|
|
34
|
+
</a>
|
|
35
|
+
<p>{formatMoney(line.linePrice, line.currencyCode)}</p>
|
|
36
|
+
<form action={updateCartLine.bind(null, language, line.id)}>
|
|
37
|
+
<label>
|
|
38
|
+
{t("cart.quantity")}
|
|
39
|
+
<input
|
|
40
|
+
name="quantity"
|
|
41
|
+
type="number"
|
|
42
|
+
min="0"
|
|
43
|
+
defaultValue={line.quantity}
|
|
44
|
+
/>
|
|
45
|
+
</label>
|
|
46
|
+
<button type="submit">{t("cart.update")}</button>
|
|
47
|
+
</form>
|
|
48
|
+
<form action={removeCartLine.bind(null, language, line.id)}>
|
|
49
|
+
<button type="submit">{t("cart.remove")}</button>
|
|
50
|
+
</form>
|
|
51
|
+
</li>
|
|
52
|
+
))}
|
|
53
|
+
</ul>
|
|
54
|
+
<aside className="cart-summary">
|
|
55
|
+
<dl>
|
|
56
|
+
<div>
|
|
57
|
+
<dt>{t("cart.subtotal")}</dt>
|
|
58
|
+
<dd>{formatMoney(cart.subtotal, cart.currencyCode)}</dd>
|
|
59
|
+
</div>
|
|
60
|
+
<div>
|
|
61
|
+
<dt>{t("cart.shipping")}</dt>
|
|
62
|
+
<dd>{formatMoney(cart.shipping, cart.currencyCode)}</dd>
|
|
63
|
+
</div>
|
|
64
|
+
<div>
|
|
65
|
+
<dt>{t("cart.total")}</dt>
|
|
66
|
+
<dd>{formatMoney(cart.total, cart.currencyCode)}</dd>
|
|
67
|
+
</div>
|
|
68
|
+
</dl>
|
|
69
|
+
<a className="button-link" href={`/${language}/checkout`}>
|
|
70
|
+
{t("cart.checkout")}
|
|
71
|
+
</a>
|
|
72
|
+
</aside>
|
|
73
|
+
</div>
|
|
74
|
+
</>
|
|
75
|
+
)}
|
|
76
|
+
</main>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useActionState, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import type { CommerceLocale } from "../../../lib/commerce";
|
|
6
|
+
import type {
|
|
7
|
+
CheckoutActionState,
|
|
8
|
+
CheckoutShippingMethod,
|
|
9
|
+
} from "../../../lib/checkout";
|
|
10
|
+
import { createTranslator } from "../../../lib/i18n";
|
|
11
|
+
|
|
12
|
+
interface CheckoutFormProps {
|
|
13
|
+
locale: CommerceLocale;
|
|
14
|
+
action: (
|
|
15
|
+
previousState: CheckoutActionState,
|
|
16
|
+
formData: FormData,
|
|
17
|
+
) => Promise<CheckoutActionState>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function CheckoutForm({ locale, action }: CheckoutFormProps) {
|
|
21
|
+
const [state, formAction, pending] = useActionState(action, { ok: true });
|
|
22
|
+
const [shippingMethod, setShippingMethod] =
|
|
23
|
+
useState<CheckoutShippingMethod>("pickup");
|
|
24
|
+
const t = createTranslator(locale);
|
|
25
|
+
const courier = shippingMethod === "courier";
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<form action={formAction} className="checkout-form">
|
|
29
|
+
<label>
|
|
30
|
+
{t("checkout.email")}
|
|
31
|
+
<input name="emailAddress" type="email" required />
|
|
32
|
+
</label>
|
|
33
|
+
<label>
|
|
34
|
+
{t("checkout.firstName")}
|
|
35
|
+
<input name="firstName" required />
|
|
36
|
+
</label>
|
|
37
|
+
<label>
|
|
38
|
+
{t("checkout.lastName")}
|
|
39
|
+
<input name="lastName" required />
|
|
40
|
+
</label>
|
|
41
|
+
<label>
|
|
42
|
+
{t("checkout.phone")}
|
|
43
|
+
<input name="phoneNumber" type="tel" required />
|
|
44
|
+
</label>
|
|
45
|
+
<fieldset>
|
|
46
|
+
<legend>{t("checkout.shippingMethod")}</legend>
|
|
47
|
+
<label>
|
|
48
|
+
<input
|
|
49
|
+
name="shippingMethodCode"
|
|
50
|
+
type="radio"
|
|
51
|
+
value="pickup"
|
|
52
|
+
checked={shippingMethod === "pickup"}
|
|
53
|
+
onChange={() => setShippingMethod("pickup")}
|
|
54
|
+
/>
|
|
55
|
+
{t("checkout.pickup")}
|
|
56
|
+
</label>
|
|
57
|
+
<label>
|
|
58
|
+
<input
|
|
59
|
+
name="shippingMethodCode"
|
|
60
|
+
type="radio"
|
|
61
|
+
value="courier"
|
|
62
|
+
checked={shippingMethod === "courier"}
|
|
63
|
+
onChange={() => setShippingMethod("courier")}
|
|
64
|
+
/>
|
|
65
|
+
{t("checkout.courier")}
|
|
66
|
+
</label>
|
|
67
|
+
</fieldset>
|
|
68
|
+
{courier ? (
|
|
69
|
+
<fieldset>
|
|
70
|
+
<legend>{t("checkout.courier")}</legend>
|
|
71
|
+
<label>
|
|
72
|
+
{t("checkout.street")}
|
|
73
|
+
<input name="streetLine1" required />
|
|
74
|
+
</label>
|
|
75
|
+
<label>
|
|
76
|
+
{t("checkout.city")}
|
|
77
|
+
<input name="city" required />
|
|
78
|
+
</label>
|
|
79
|
+
<label>
|
|
80
|
+
{t("checkout.postalCode")}
|
|
81
|
+
<input name="postalCode" required />
|
|
82
|
+
</label>
|
|
83
|
+
<label>
|
|
84
|
+
{t("checkout.country")}
|
|
85
|
+
<select name="countryCode" defaultValue="SK" required>
|
|
86
|
+
<option value="SK">{t("checkout.slovakia")}</option>
|
|
87
|
+
</select>
|
|
88
|
+
</label>
|
|
89
|
+
</fieldset>
|
|
90
|
+
) : null}
|
|
91
|
+
<p>
|
|
92
|
+
{courier ? t("checkout.courierSummary") : t("checkout.pickupSummary")}
|
|
93
|
+
</p>
|
|
94
|
+
{!state.ok ? (
|
|
95
|
+
<p role="alert">
|
|
96
|
+
{state.errorCode === "PRICE_CHANGED"
|
|
97
|
+
? t("checkout.priceChanged")
|
|
98
|
+
: state.errorCode === "INSUFFICIENT_STOCK"
|
|
99
|
+
? t("checkout.insufficientStock")
|
|
100
|
+
: state.message}
|
|
101
|
+
</p>
|
|
102
|
+
) : null}
|
|
103
|
+
<label>
|
|
104
|
+
<input name="termsAccepted" type="checkbox" required />{" "}
|
|
105
|
+
{t("checkout.terms")}
|
|
106
|
+
</label>
|
|
107
|
+
<button type="submit" disabled={pending}>
|
|
108
|
+
{courier ? t("checkout.submitCourier") : t("checkout.submitPickup")}
|
|
109
|
+
</button>
|
|
110
|
+
</form>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { placeOrder } from "../../../lib/checkout";
|
|
2
|
+
import { getCart } from "../../../lib/cart";
|
|
3
|
+
import { createTranslator, parseLocale } from "../../../lib/i18n";
|
|
4
|
+
import { CheckoutForm } from "./checkout-form";
|
|
5
|
+
|
|
6
|
+
interface CheckoutPageProps {
|
|
7
|
+
params: Promise<{ locale: string }>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default async function CheckoutPage({ params }: CheckoutPageProps) {
|
|
11
|
+
const { locale } = await params;
|
|
12
|
+
const language = parseLocale(locale);
|
|
13
|
+
const attemptId = crypto.randomUUID();
|
|
14
|
+
const cart = await getCart(language);
|
|
15
|
+
const t = createTranslator(language);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<main className="page checkout-page" lang={language}>
|
|
19
|
+
<div className="page-heading">
|
|
20
|
+
<p className="eyebrow">JarShop starter</p>
|
|
21
|
+
<h1>{t("checkout.title")}</h1>
|
|
22
|
+
</div>
|
|
23
|
+
<CheckoutForm
|
|
24
|
+
locale={language}
|
|
25
|
+
action={placeOrder.bind(
|
|
26
|
+
null,
|
|
27
|
+
language,
|
|
28
|
+
attemptId,
|
|
29
|
+
cart
|
|
30
|
+
? {
|
|
31
|
+
currencyCode: cart.currencyCode,
|
|
32
|
+
subtotalWithTax: cart.subtotal,
|
|
33
|
+
lines: cart.lines.map((line) => ({
|
|
34
|
+
lineId: line.id,
|
|
35
|
+
quantity: line.quantity,
|
|
36
|
+
linePriceWithTax: line.linePrice,
|
|
37
|
+
})),
|
|
38
|
+
}
|
|
39
|
+
: null,
|
|
40
|
+
)}
|
|
41
|
+
/>
|
|
42
|
+
</main>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createTranslator, parseLocale } from "../../../../lib/i18n";
|
|
2
|
+
import { getOrderConfirmation } from "../../../../lib/confirmation";
|
|
3
|
+
|
|
4
|
+
interface CheckoutSuccessProps {
|
|
5
|
+
params: Promise<{ locale: string }>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default async function CheckoutSuccess({
|
|
9
|
+
params,
|
|
10
|
+
}: CheckoutSuccessProps) {
|
|
11
|
+
const { locale } = await params;
|
|
12
|
+
const language = parseLocale(locale);
|
|
13
|
+
const t = createTranslator(language);
|
|
14
|
+
const confirmation = await getOrderConfirmation(language);
|
|
15
|
+
return (
|
|
16
|
+
<main lang={language}>
|
|
17
|
+
<h1>{t("success.title")}</h1>
|
|
18
|
+
<p>
|
|
19
|
+
{t("success.orderCode")}: {confirmation?.orderCode ?? "—"}
|
|
20
|
+
</p>
|
|
21
|
+
</main>
|
|
22
|
+
);
|
|
23
|
+
}
|