create-warlock 5.34.2 → 5.35.0
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/CHANGELOG.md +38 -0
- package/README.md +8 -0
- package/esm/commands/create-new-app/index.mjs +4 -3
- package/esm/commands/create-new-app/index.mjs.map +1 -1
- package/esm/commands/create-warlock-app/index.mjs +7 -1
- package/esm/commands/create-warlock-app/index.mjs.map +1 -1
- package/esm/features/features-map.mjs +8 -2
- package/esm/features/features-map.mjs.map +1 -1
- package/esm/helpers/app.mjs +1 -0
- package/esm/helpers/app.mjs.map +1 -1
- package/esm/prompts/ask-stack.mjs +1 -1
- package/esm/prompts/ask-stack.mjs.map +1 -1
- package/llms-full.txt +64 -28
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/{create-a-warlock-project/SKILL.md → SKILL.md} +63 -27
- package/templates/design-system/SOURCES.md +23 -0
- package/templates/design-system/package.json +14 -0
- package/templates/design-system/src/web/components/ui/LICENSE.md +21 -0
- package/templates/design-system/src/web/components/ui/button.tsx +61 -0
- package/templates/design-system/src/web/components/ui/calendar.tsx +178 -0
- package/templates/design-system/src/web/components/ui/checkbox.tsx +28 -0
- package/templates/design-system/src/web/components/ui/command.tsx +161 -0
- package/templates/design-system/src/web/components/ui/dialog.tsx +144 -0
- package/templates/design-system/src/web/components/ui/input.tsx +20 -0
- package/templates/design-system/src/web/components/ui/label.tsx +20 -0
- package/templates/design-system/src/web/components/ui/popover.tsx +73 -0
- package/templates/design-system/src/web/components/ui/radio-group.tsx +44 -0
- package/templates/design-system/src/web/components/ui/switch.tsx +34 -0
- package/templates/design-system/src/web/components/ui/textarea.tsx +17 -0
- package/templates/design-system/src/web/design-system/components/checkbox-input.tsx +19 -0
- package/templates/design-system/src/web/design-system/components/date-picker-input.tsx +59 -0
- package/templates/design-system/src/web/design-system/components/date-time-picker-input.tsx +67 -0
- package/templates/design-system/src/web/design-system/components/email-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/field-frame.tsx +36 -0
- package/templates/design-system/src/web/design-system/components/file-input.tsx +24 -0
- package/templates/design-system/src/web/design-system/components/multi-select-input.tsx +94 -0
- package/templates/design-system/src/web/design-system/components/password-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/radio-input.tsx +41 -0
- package/templates/design-system/src/web/design-system/components/select-input.tsx +94 -0
- package/templates/design-system/src/web/design-system/components/submit-button.tsx +20 -0
- package/templates/design-system/src/web/design-system/components/switch-input.tsx +19 -0
- package/templates/design-system/src/web/design-system/components/text-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/textarea-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/components/time-picker-input.tsx +22 -0
- package/templates/design-system/src/web/design-system/hooks/use-calendar-day-button.ts +9 -0
- package/templates/design-system/src/web/design-system/hooks/use-control.ts +29 -0
- package/templates/design-system/src/web/design-system/hooks/use-date-control.ts +42 -0
- package/templates/design-system/src/web/design-system/hooks/use-file-control.ts +24 -0
- package/templates/design-system/src/web/design-system/hooks/use-select-control.ts +78 -0
- package/templates/design-system/src/web/design-system/hooks/use-submit-control.ts +13 -0
- package/templates/design-system/src/web/design-system/types/control-props.ts +46 -0
- package/templates/toolchains/oxc/.oxlintrc.json +3 -1
- package/templates/warlock/.oxlintrc.json +3 -1
- package/templates/warlock/AGENTS.md +10 -10
- package/templates/warlock/e2e/home.e2e.ts +19 -0
- package/templates/warlock/package.json +1 -1
- package/templates/warlock/skills/api-design/SKILL.md +180 -57
- package/templates/warlock/skills/code-standards/SKILL.md +48 -39
- package/templates/warlock/skills/data-and-persistence/SKILL.md +72 -57
- package/templates/warlock/skills/git-workflow/SKILL.md +9 -9
- package/templates/warlock/skills/module-boundaries/SKILL.md +2 -2
- package/templates/warlock/skills/observability-and-resilience/SKILL.md +8 -6
- package/templates/warlock/skills/security-baseline/SKILL.md +23 -22
- package/templates/warlock/skills/testing-strategy/SKILL.md +53 -12
- package/templates/warlock/skills/web-code-standards/SKILL.md +195 -60
- package/templates/warlock/src/app/auth/controllers/forgot-password.controller.ts +6 -0
- package/templates/warlock/src/app/auth/controllers/login.controller.ts +17 -0
- package/templates/warlock/src/app/auth/controllers/logout-all.controller.ts +6 -0
- package/templates/warlock/src/app/auth/controllers/logout.controller.ts +6 -0
- package/templates/warlock/src/app/auth/controllers/me.controller.ts +7 -0
- package/templates/warlock/src/app/auth/controllers/refresh-token.controller.ts +12 -0
- package/templates/warlock/src/app/auth/controllers/reset-password.controller.ts +6 -0
- package/templates/warlock/src/app/auth/models/otp/otp.model.ts +9 -3
- package/templates/warlock/src/app/auth/services/otp.service.ts +18 -6
- package/templates/warlock/src/app/contact/controllers/contact.controller.ts +6 -0
- package/templates/warlock/src/app/home/controllers/home-page.controller.ts +4 -0
- package/templates/warlock/src/app/locale/controllers/locale.controller.ts +6 -0
- package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +7 -0
- package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +15 -9
- package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts +1 -0
- package/templates/warlock/src/app/posts/models/post/post.model.ts +6 -0
- package/templates/warlock/src/app/posts/resources/post.resource.ts +1 -1
- package/templates/warlock/src/app/posts/schema/update-post.schema.ts +1 -0
- package/templates/warlock/src/app/shared/utils/format-http-error-response.ts +53 -0
- package/templates/warlock/src/app/users/commands/hello-world.command.ts +2 -1
- package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +13 -1
- package/templates/warlock/src/app/users/controllers/list-users.controller.ts +24 -1
- package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +1 -0
- package/templates/warlock/src/app/users/models/user/user.model.ts +5 -0
- package/templates/warlock/src/app/users/repositories/users.repository.ts +2 -2
- package/templates/warlock/src/app/users/resources/user.resource.ts +1 -1
- package/templates/warlock/src/app/users/schema/list-users.schema.ts +1 -1
- package/templates/warlock/src/config/http.ts +6 -0
- package/templates/warlock/src/web/home/components/contact-field.tsx +18 -0
- package/templates/warlock/src/web/home/components/contact-section.tsx +5 -29
- package/templates/warlock/src/web/home/components/contact-submit-button.tsx +16 -0
- package/templates/warlock/src/web/home/hooks/use-contact-field.ts +9 -0
- package/templates/warlock/src/web/home/hooks/use-contact-form.ts +5 -8
- package/templates/warlock/src/web/home/hooks/use-contact-locale.ts +34 -0
- package/templates/warlock/src/web/home/hooks/use-contact-submit-button.ts +6 -0
- package/templates/warlock/src/web/home/index.page.tsx +1 -1
- package/templates/warlock/src/web/home/types/contact-controls.type.ts +13 -0
- package/templates/warlock/src/web/shared/utils/set-form-errors.ts +3 -1
- package/templates/warlock/warlock.config.ts +1 -0
- package/templates/warlock/src/web/home/components/contact-form-controls.tsx +0 -42
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { HttpErrorResponseContext } from "@warlock.js/core";
|
|
2
|
+
|
|
3
|
+
const statusCodes: Record<number, string> = {
|
|
4
|
+
400: "badRequest",
|
|
5
|
+
401: "unauthorized",
|
|
6
|
+
403: "forbidden",
|
|
7
|
+
404: "notFound",
|
|
8
|
+
409: "conflict",
|
|
9
|
+
422: "validationFailed",
|
|
10
|
+
429: "tooManyRequests",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Convert framework JSON errors to the project's public error contract. */
|
|
14
|
+
export function formatHttpErrorResponse({
|
|
15
|
+
status,
|
|
16
|
+
body,
|
|
17
|
+
requestId,
|
|
18
|
+
appError,
|
|
19
|
+
}: HttpErrorResponseContext) {
|
|
20
|
+
if (appError) {
|
|
21
|
+
return {
|
|
22
|
+
error: { code: appError.code, message: appError.publicMessage },
|
|
23
|
+
...(appError.details === undefined ? {} : { details: appError.details }),
|
|
24
|
+
requestId,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const source = isRecord(body) ? body : {};
|
|
28
|
+
const error = isRecord(source.error) ? source.error : {};
|
|
29
|
+
const serverFault = status >= 500;
|
|
30
|
+
const code = serverFault
|
|
31
|
+
? "serverError"
|
|
32
|
+
: (text(error.code) ?? text(source.errorCode) ?? statusCodes[status] ?? "requestFailed");
|
|
33
|
+
const message = serverFault
|
|
34
|
+
? "Internal server error."
|
|
35
|
+
: (text(error.message) ?? text(source.message) ?? text(source.error) ?? "Request failed.");
|
|
36
|
+
// Only deliberately public details belong here. Never include a 5xx payload
|
|
37
|
+
// or the development-only stack attached by the framework error funnel.
|
|
38
|
+
const details = serverFault ? undefined : (source.details ?? source.errors ?? source.payload);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
error: { code, message },
|
|
42
|
+
...(details === undefined ? {} : { details }),
|
|
43
|
+
requestId,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
48
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function text(value: unknown): string | undefined {
|
|
52
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
53
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
2
|
+
import { UserResource } from "app/users/resources/user.resource";
|
|
2
3
|
import { User } from "../models/user";
|
|
3
4
|
import { createUserSchema, type CreateUserSchema } from "../schema/create-user.schema";
|
|
4
5
|
|
|
@@ -6,7 +7,11 @@ export const createNewUserController: GuardedRequestHandler<CreateUserSchema> =
|
|
|
6
7
|
request,
|
|
7
8
|
response,
|
|
8
9
|
}) => {
|
|
9
|
-
const file = request.file("image")
|
|
10
|
+
const file = request.file("image");
|
|
11
|
+
|
|
12
|
+
if (!file) {
|
|
13
|
+
return response.badRequest({ error: { code: "imageRequired", message: "Image is required." } });
|
|
14
|
+
}
|
|
10
15
|
|
|
11
16
|
const output = await file.save("images");
|
|
12
17
|
|
|
@@ -25,3 +30,10 @@ export const createNewUserController: GuardedRequestHandler<CreateUserSchema> =
|
|
|
25
30
|
createNewUserController.validation = {
|
|
26
31
|
schema: createUserSchema,
|
|
27
32
|
};
|
|
33
|
+
|
|
34
|
+
createNewUserController.responseSchema = {
|
|
35
|
+
201: { body: { message: "string", user: UserResource } },
|
|
36
|
+
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
37
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
38
|
+
400: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
39
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
2
|
+
import { UserResource } from "app/users/resources/user.resource";
|
|
2
3
|
import { usersRepository } from "../repositories/users.repository";
|
|
3
4
|
import { listUsersSchema, type ListUsersSchema } from "../schema/list-users.schema";
|
|
4
5
|
|
|
@@ -6,10 +7,15 @@ export const listUsersController: GuardedRequestHandler<ListUsersSchema> = async
|
|
|
6
7
|
request,
|
|
7
8
|
response,
|
|
8
9
|
}) => {
|
|
9
|
-
const
|
|
10
|
+
const { id, ...filters } = request.validated();
|
|
11
|
+
const { data: users, pagination } = await usersRepository.listCached({
|
|
12
|
+
...filters,
|
|
13
|
+
publicId: id,
|
|
14
|
+
});
|
|
10
15
|
|
|
11
16
|
return response.success({
|
|
12
17
|
users,
|
|
18
|
+
pagination,
|
|
13
19
|
});
|
|
14
20
|
};
|
|
15
21
|
|
|
@@ -18,3 +24,20 @@ listUsersController.description = "List Users Controller";
|
|
|
18
24
|
listUsersController.validation = {
|
|
19
25
|
schema: listUsersSchema,
|
|
20
26
|
};
|
|
27
|
+
|
|
28
|
+
listUsersController.responseSchema = {
|
|
29
|
+
200: {
|
|
30
|
+
body: {
|
|
31
|
+
users: [UserResource],
|
|
32
|
+
pagination: {
|
|
33
|
+
limit: "number",
|
|
34
|
+
result: "number",
|
|
35
|
+
page: "number",
|
|
36
|
+
total: "number",
|
|
37
|
+
pages: "number",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
42
|
+
500: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
|
|
43
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { Auth } from "@warlock.js/auth";
|
|
2
3
|
import { RegisterModel } from "@warlock.js/cascade";
|
|
3
4
|
import { useHashedPassword } from "@warlock.js/core";
|
|
@@ -5,6 +6,10 @@ import { v, type Infer } from "@warlock.js/seal";
|
|
|
5
6
|
import { UserResource } from "app/users/resources/user.resource";
|
|
6
7
|
|
|
7
8
|
export const userSchema = v.object({
|
|
9
|
+
publicId: v
|
|
10
|
+
.string()
|
|
11
|
+
.uuid()
|
|
12
|
+
.default(() => randomUUID()),
|
|
8
13
|
name: v.string(),
|
|
9
14
|
email: v.email().requiredIfEmpty("id"),
|
|
10
15
|
// Required at the controller layer (`create-user.schema.ts`) on
|
|
@@ -5,7 +5,7 @@ import { User } from "../models/user";
|
|
|
5
5
|
export type UserFilter = {
|
|
6
6
|
email?: string;
|
|
7
7
|
name?: string;
|
|
8
|
-
|
|
8
|
+
publicId?: string;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export type UsersListOptions = RepositoryOptions & UserFilter;
|
|
@@ -14,7 +14,7 @@ class UsersRepository extends RepositoryManager<User, UserFilter> {
|
|
|
14
14
|
public source = User;
|
|
15
15
|
|
|
16
16
|
public filterBy: FilterRules = {
|
|
17
|
-
|
|
17
|
+
publicId: "=",
|
|
18
18
|
name: "like",
|
|
19
19
|
email: "=",
|
|
20
20
|
};
|
|
@@ -5,7 +5,7 @@ import { v, type Infer } from "@warlock.js/seal";
|
|
|
5
5
|
* smuggled operator (`?email[$ne]=`) never reaches the repository filters.
|
|
6
6
|
*/
|
|
7
7
|
export const listUsersSchema = v.object({
|
|
8
|
-
id: v.string().optional(),
|
|
8
|
+
id: v.string().uuid().optional(),
|
|
9
9
|
name: v.string().max(100).optional(),
|
|
10
10
|
email: v.string().max(255).optional(),
|
|
11
11
|
page: v.numeric().min(1).optional(),
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { HttpConfigurations } from "@warlock.js/core";
|
|
2
2
|
import { env } from "@warlock.js/core";
|
|
3
|
+
import { formatHttpErrorResponse } from "../app/shared/utils/format-http-error-response";
|
|
3
4
|
|
|
4
5
|
const httpConfigurations: HttpConfigurations = {
|
|
5
6
|
port: env("HTTP_PORT", 3000),
|
|
6
7
|
host: env("HTTP_HOST", "localhost"),
|
|
7
8
|
log: true,
|
|
9
|
+
// errorMapper classifies thrown non-HttpErrors; it cannot shape returned
|
|
10
|
+
// validation, not-found, or rate-limit responses. Format all JSON errors at
|
|
11
|
+
// the transport boundary, including Fastify's early rejections.
|
|
12
|
+
errorMapper: () => ({ status: 500 }),
|
|
13
|
+
errorResponse: formatHttpErrorResponse,
|
|
8
14
|
fileUploadLimit: 12 * 1024 * 1024 * 1024,
|
|
9
15
|
rateLimit: {
|
|
10
16
|
max: 260,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useContactField } from "../hooks/use-contact-field";
|
|
2
|
+
import type { ContactFieldProps } from "../types/contact-controls.type";
|
|
3
|
+
|
|
4
|
+
export default function ContactField({ label, multiline, rows, ...props }: ContactFieldProps) {
|
|
5
|
+
const { error, getErrorProps, inputProps, id } = useContactField(props, rows);
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<label htmlFor={id}>
|
|
9
|
+
<span>{label}</span>
|
|
10
|
+
{multiline ? <textarea {...inputProps} /> : <input {...inputProps} />}
|
|
11
|
+
{error ? (
|
|
12
|
+
<small className="warlock-field-error" {...getErrorProps()}>
|
|
13
|
+
{error}
|
|
14
|
+
</small>
|
|
15
|
+
) : null}
|
|
16
|
+
</label>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -1,38 +1,14 @@
|
|
|
1
1
|
import { Form } from "@mongez/react-form";
|
|
2
|
-
import { changeLocaleCode, useLocale, useTextDirection, useTrans } from "@warlock.js/web";
|
|
3
|
-
import { useState } from "react";
|
|
4
2
|
import { contactSchema } from "../../../shared/contact.schema";
|
|
5
3
|
import { useContactForm } from "../hooks/use-contact-form";
|
|
6
|
-
import {
|
|
4
|
+
import { useContactLocale } from "../hooks/use-contact-locale";
|
|
5
|
+
import ContactField from "./contact-field";
|
|
6
|
+
import ContactSubmitButton from "./contact-submit-button";
|
|
7
7
|
|
|
8
|
-
export function ContactSection() {
|
|
8
|
+
export default function ContactSection() {
|
|
9
9
|
const { status: contactStatus, submitContact } = useContactForm();
|
|
10
|
-
const
|
|
11
|
-
const direction = useTextDirection();
|
|
12
|
-
const translate = useTrans();
|
|
13
|
-
const [isSwitchingLocale, setIsSwitchingLocale] = useState(false);
|
|
14
|
-
const [localeError, setLocaleError] = useState("");
|
|
10
|
+
const { direction, translate, isSwitchingLocale, localeError, toggleLocale } = useContactLocale();
|
|
15
11
|
|
|
16
|
-
/**
|
|
17
|
-
* Switch locale via the framework's `changeLocaleCode` — no reload, no
|
|
18
|
-
* manual cookie write. The control is disabled while the request is in
|
|
19
|
-
* flight, and a rejection leaves the current locale in place while
|
|
20
|
-
* surfacing an error instead of throwing.
|
|
21
|
-
*/
|
|
22
|
-
async function toggleLocale() {
|
|
23
|
-
const nextLocale = locale === "en" ? "ar" : "en";
|
|
24
|
-
|
|
25
|
-
setIsSwitchingLocale(true);
|
|
26
|
-
setLocaleError("");
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
await changeLocaleCode(nextLocale);
|
|
30
|
-
} catch (error) {
|
|
31
|
-
setLocaleError(error instanceof Error ? error.message : "Could not update the locale.");
|
|
32
|
-
} finally {
|
|
33
|
-
setIsSwitchingLocale(false);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
12
|
return (
|
|
37
13
|
<section className="warlock-section warlock-contact" id="contact" dir={direction}>
|
|
38
14
|
<div className="warlock-contact-copy">
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useContactSubmitButton } from "../hooks/use-contact-submit-button";
|
|
2
|
+
import type { ContactSubmitButtonProps } from "../types/contact-controls.type";
|
|
3
|
+
|
|
4
|
+
export default function ContactSubmitButton({
|
|
5
|
+
idleLabel,
|
|
6
|
+
submittingLabel,
|
|
7
|
+
}: ContactSubmitButtonProps) {
|
|
8
|
+
const { disabled, isSubmitting } = useContactSubmitButton();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<button className="warlock-contact-submit" type="submit" disabled={disabled}>
|
|
12
|
+
{isSubmitting ? submittingLabel : idleLabel}
|
|
13
|
+
<span aria-hidden="true">→</span>
|
|
14
|
+
</button>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useFormControl, type FormControlProps } from "@mongez/react-form";
|
|
2
|
+
|
|
3
|
+
/** Keep control identity, validation and DOM wiring in one hook. */
|
|
4
|
+
export function useContactField(props: FormControlProps, rows?: number) {
|
|
5
|
+
const { error, getErrorProps, getInputProps, id } = useFormControl(props);
|
|
6
|
+
const inputProps = getInputProps(rows ? { rows } : undefined);
|
|
7
|
+
|
|
8
|
+
return { error, getErrorProps, id, inputProps };
|
|
9
|
+
}
|
|
@@ -7,15 +7,12 @@ type ContactValues = InferFormValues<typeof contactSchema>;
|
|
|
7
7
|
|
|
8
8
|
type ContactResponse = {
|
|
9
9
|
message: string;
|
|
10
|
-
received: {
|
|
11
|
-
email: string;
|
|
12
|
-
characters: number;
|
|
13
|
-
};
|
|
14
10
|
};
|
|
15
11
|
|
|
16
12
|
type ContactErrorBody = {
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
error?: { code: string; message: string };
|
|
14
|
+
details?: unknown;
|
|
15
|
+
requestId?: string;
|
|
19
16
|
};
|
|
20
17
|
|
|
21
18
|
type ContactStatus = {
|
|
@@ -44,12 +41,12 @@ export function useContactForm() {
|
|
|
44
41
|
|
|
45
42
|
if (!response.ok) {
|
|
46
43
|
if (response.status === 422) {
|
|
47
|
-
setFormErrors({ body }, form);
|
|
44
|
+
setFormErrors({ body: { errors: body.details } }, form);
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
setStatus({
|
|
51
48
|
state: "error",
|
|
52
|
-
message: body.message || "The request was not accepted.",
|
|
49
|
+
message: body.error?.message || "The request was not accepted.",
|
|
53
50
|
});
|
|
54
51
|
return;
|
|
55
52
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { changeLocaleCode, useLocale, useTextDirection, useTrans } from "@warlock.js/web";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
/** Own the contact section's locale switch and recoverable error state. */
|
|
5
|
+
export function useContactLocale() {
|
|
6
|
+
const locale = useLocale();
|
|
7
|
+
const direction = useTextDirection();
|
|
8
|
+
const translate = useTrans();
|
|
9
|
+
const [isSwitchingLocale, setIsSwitchingLocale] = useState(false);
|
|
10
|
+
const [localeError, setLocaleError] = useState("");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Switch locale via the framework's `changeLocaleCode` — no reload, no
|
|
14
|
+
* manual cookie write. The control is disabled while the request is in
|
|
15
|
+
* flight, and a rejection leaves the current locale in place while
|
|
16
|
+
* surfacing an error instead of throwing.
|
|
17
|
+
*/
|
|
18
|
+
async function toggleLocale() {
|
|
19
|
+
const nextLocale = locale === "en" ? "ar" : "en";
|
|
20
|
+
|
|
21
|
+
setIsSwitchingLocale(true);
|
|
22
|
+
setLocaleError("");
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await changeLocaleCode(nextLocale);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
setLocaleError(error instanceof Error ? error.message : "Could not update the locale.");
|
|
28
|
+
} finally {
|
|
29
|
+
setIsSwitchingLocale(false);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { direction, translate, isSwitchingLocale, localeError, toggleLocale };
|
|
34
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PageProps } from "@warlock.js/web";
|
|
2
|
-
import
|
|
2
|
+
import ContactSection from "./components/contact-section";
|
|
3
3
|
import { ContentSections } from "./components/content-sections";
|
|
4
4
|
import { HeroSection } from "./components/hero-section";
|
|
5
5
|
import { HomeFooter } from "./components/home-footer";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FormControlProps } from "@mongez/react-form";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export type ContactFieldProps = FormControlProps & {
|
|
5
|
+
label: ReactNode;
|
|
6
|
+
multiline?: boolean;
|
|
7
|
+
rows?: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ContactSubmitButtonProps = {
|
|
11
|
+
idleLabel: ReactNode;
|
|
12
|
+
submittingLabel: ReactNode;
|
|
13
|
+
};
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { defineConfig } from "@warlock.js/core";
|
|
7
7
|
|
|
8
8
|
export default defineConfig({
|
|
9
|
+
build: { budgets: { page: { js: "150kb", css: "50kb" }, mode: "error" } },
|
|
9
10
|
cli: {
|
|
10
11
|
commands: [registerJWTSecretGeneratorCommand(), registerAuthCleanupCommand()],
|
|
11
12
|
},
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { useFormControl, useSubmitButton, type FormControlProps } from "@mongez/react-form";
|
|
2
|
-
import { type ReactNode } from "react";
|
|
3
|
-
|
|
4
|
-
type ContactFieldProps = FormControlProps & {
|
|
5
|
-
label: ReactNode;
|
|
6
|
-
multiline?: boolean;
|
|
7
|
-
rows?: number;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export function ContactField({ label, multiline, rows, ...props }: ContactFieldProps) {
|
|
11
|
-
const { error, getErrorProps, getInputProps, id } = useFormControl(props);
|
|
12
|
-
const inputProps = getInputProps(rows ? { rows } : undefined);
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<label htmlFor={id}>
|
|
16
|
-
<span>{label}</span>
|
|
17
|
-
{multiline ? <textarea {...inputProps} /> : <input {...inputProps} />}
|
|
18
|
-
{error ? (
|
|
19
|
-
<small className="warlock-field-error" {...getErrorProps()}>
|
|
20
|
-
{error}
|
|
21
|
-
</small>
|
|
22
|
-
) : null}
|
|
23
|
-
</label>
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function ContactSubmitButton({
|
|
28
|
-
idleLabel,
|
|
29
|
-
submittingLabel,
|
|
30
|
-
}: {
|
|
31
|
-
idleLabel: ReactNode;
|
|
32
|
-
submittingLabel: ReactNode;
|
|
33
|
-
}) {
|
|
34
|
-
const { disabled, isSubmitting } = useSubmitButton();
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<button className="warlock-contact-submit" type="submit" disabled={disabled}>
|
|
38
|
-
{isSubmitting ? submittingLabel : idleLabel}
|
|
39
|
-
<span aria-hidden="true">→</span>
|
|
40
|
-
</button>
|
|
41
|
-
);
|
|
42
|
-
}
|