create-warlock 5.11.0 → 5.12.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 +10 -0
- package/README.md +38 -5
- package/esm/commands/create-new-app/index.mjs +102 -9
- package/esm/commands/create-new-app/index.mjs.map +1 -1
- package/esm/commands/create-new-app/types.d.mts +8 -3
- package/esm/commands/create-warlock-app/index.mjs +12 -1
- package/esm/commands/create-warlock-app/index.mjs.map +1 -1
- package/esm/features/agent-kit-targets.mjs +134 -0
- package/esm/features/agent-kit-targets.mjs.map +1 -0
- package/esm/flags/definitions.mjs +89 -0
- package/esm/flags/definitions.mjs.map +1 -0
- package/esm/helpers/app.mjs +21 -0
- package/esm/helpers/app.mjs.map +1 -1
- package/esm/index.mjs +15 -23
- package/esm/index.mjs.map +1 -1
- package/esm/prompts/ask-stack.mjs +35 -0
- package/esm/prompts/ask-stack.mjs.map +1 -0
- package/esm/summary.mjs +25 -0
- package/esm/summary.mjs.map +1 -0
- package/llms-full.txt +65 -38
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/create-a-warlock-project/SKILL.md +65 -38
- package/templates/warlock/AGENTS.md +1 -1
- package/templates/warlock/skills/api-design/SKILL.md +2 -2
- package/templates/warlock/skills/security-baseline/SKILL.md +1 -1
- package/templates/warlock/src/app/auth/controllers/logout-all.controller.ts +1 -1
- package/templates/warlock/src/app/auth/controllers/logout.controller.ts +3 -3
- package/templates/warlock/src/app/auth/controllers/me.controller.ts +1 -1
- package/templates/warlock/src/app/auth/requests/guarded.request.ts +2 -2
- package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +1 -1
- package/templates/warlock/src/app/users/models/user/user.model.ts +8 -3
- package/templates/warlock/src/app/users/services/login-social.ts +4 -3
- package/templates/warlock/src/typings.d.ts +14 -8
- package/templates/warlock/src/web/home/components/contact-section.tsx +31 -14
- package/templates/warlock/src/web/home/hooks/use-contact-form.ts +18 -7
- package/templates/warlock/src/web/home/index.page.tsx +1 -1
- package/templates/warlock/src/web/root.tsx +5 -2
- package/templates/warlock/src/web/home/hooks/use-locale-switcher.ts +0 -35
|
@@ -1,51 +1,47 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: create-a-warlock-project
|
|
3
|
-
description: 'Scaffold a brand-new Warlock.js project with `create-warlock` —
|
|
3
|
+
description: 'Scaffold a brand-new Warlock.js project with `create-warlock` — fully headless by default (at most one prompt — API-only vs full-stack web) plus `--interactive`/`--customize` for the full long-form wizard. Every flag (`--name`, `--stack`, `--db`, `--pm`, `--features`, `--ai`, `--agents`, `--git/--no-git`, `--jwt/--no-jwt`, `--no-db`, `--yes`, `--interactive`), every valid database-driver / feature / AI-provider / agent-kit-target key, opting out of a database (`--db=none` / `--no-db`, which removes `src/config/database.ts`), the post-scaffold decisions summary, and the directory tree the template emits. Triggers: `create-warlock`, `npm create warlock`, `npx create-warlock`; "scaffold a warlock app", "start a new warlock project", "create-warlock flags", "non-interactive warlock scaffold", "scaffold without a database", "which features can I pass to create-warlock", "agent-kit targets". Skip: running an already-created project (`@warlock.js/core/run-app/SKILL.md`); adding a feature to an existing project (`warlock add`, `@warlock.js/core/add-connector/SKILL.md`); generating a module inside a project (`@warlock.js/core/create-module/SKILL.md`).'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# create-warlock — scaffold a new project
|
|
7
7
|
|
|
8
|
-
`create-warlock` builds a fresh Warlock.js project: it copies the `warlock` template, substitutes the project name, wires the chosen database driver into `.env` (or, when you opt out with `--db=none`, removes `src/config/database.ts`), then delegates every optional package install to the project's own `warlock add` so dependency versions are never duplicated by the scaffolder.
|
|
8
|
+
`create-warlock` builds a fresh Warlock.js project: it copies the `warlock` template, substitutes the project name, wires the chosen database driver into `.env` (or, when you opt out with `--db=none`, removes `src/config/database.ts`), derives `agent-kit` targets into the generated project, then delegates every optional package install to the project's own `warlock add` so dependency versions are never duplicated by the scaffolder.
|
|
9
9
|
|
|
10
10
|
## The shape
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
# or: npx create-warlock
|
|
13
|
+
# Default: at most ONE prompt (API-only vs full-stack web) — everything else is a flag/default
|
|
14
|
+
npx create-warlock my-app
|
|
16
15
|
|
|
17
|
-
#
|
|
16
|
+
# Fully headless — one command scaffolds the whole app, no prompts at all
|
|
18
17
|
npx create-warlock my-app --yes --db=postgres --features=test,redis --ai=ai-openai
|
|
19
18
|
|
|
19
|
+
# The one structural fork
|
|
20
|
+
npx create-warlock my-web-app --yes --stack=web
|
|
21
|
+
|
|
20
22
|
# No database at all
|
|
21
23
|
npx create-warlock my-api --yes --no-db --features=test
|
|
24
|
+
|
|
25
|
+
# The full long-form wizard, one prompt at a time
|
|
26
|
+
npx create-warlock --interactive
|
|
22
27
|
```
|
|
23
28
|
|
|
24
29
|
The first positional argument is the project name; it also becomes the directory created under the current working directory. If that directory already exists, the command aborts — it never overwrites.
|
|
25
30
|
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
The wizard asks, in order:
|
|
29
|
-
|
|
30
|
-
1. **Project name** — required.
|
|
31
|
-
2. **Package manager** — chosen from the managers detected on the system (npm is always offered; yarn / pnpm appear if installed).
|
|
32
|
-
3. **Database driver** — a single select: a driver, or **None** to scaffold without a database (see keys below).
|
|
33
|
-
4. **Features** — a multiselect of optional packages (`react` is pre-checked).
|
|
34
|
-
5. **AI providers** — a multiselect; picking any one pulls `@warlock.js/ai` automatically.
|
|
35
|
-
6. **Initialize Git?** — yes/no.
|
|
36
|
-
7. **Generate JWT secret keys?** — yes/no. If no, the cache is warmed instead.
|
|
31
|
+
## Three modes
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
1. **Default (a TTY, no `--yes`, no `--interactive`)** — asks the project name (if not already given) plus AT MOST ONE structural question: API-only or full-stack web (`--stack`). This is the only choice that changes the generated app's shape; every other choice below is reversible within minutes of scaffolding, so it is a flag with a sensible default, never a prompt.
|
|
34
|
+
2. **`--yes` (or no TTY at all — CI, a script, an agent harness)** — fully headless. Every answer comes from a flag or its default; nothing is ever prompted, and stdin is never read.
|
|
35
|
+
3. **`--interactive` (alias `--customize`)** — the full long-form wizard: project name, package manager, database driver, features, AI providers, git, JWT — one prompt after another, exactly like earlier `create-warlock` releases.
|
|
41
36
|
|
|
42
37
|
**Without a terminal, `--yes` is not required.** When stdin is not a TTY — CI, a
|
|
43
|
-
script, a container, an agent harness — the scaffolder does not try to prompt
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
script, a container, an agent harness — the scaffolder does not try to prompt,
|
|
39
|
+
regardless of which of the three modes was requested. If the flags already
|
|
40
|
+
answer everything it needs, it simply scaffolds. Prompting is the fallback,
|
|
41
|
+
not the precondition.
|
|
46
42
|
|
|
47
43
|
Only a question that genuinely cannot be answered stops the run, and in practice
|
|
48
|
-
that is just the project name, the one
|
|
44
|
+
that is just the project name, the one required answer with no default:
|
|
49
45
|
|
|
50
46
|
```
|
|
51
47
|
A project name is required and no terminal is available to ask for one. Pass it as
|
|
@@ -53,21 +49,51 @@ the first argument or --name=<name>. Non-interactive flags: --yes, --pm=<npm|yar
|
|
|
53
49
|
--db=<driver>|--no-db, --features=<list>, --ai=<list>, --git|--no-git, --jwt|--no-jwt.
|
|
54
50
|
```
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
| `--no-db` | no | — | Opt out of a database — shorthand for `--db=none`. |
|
|
61
|
-
| `--pm` | yes | auto-detected | Package manager — must be one of `npm` / `yarn` / `pnpm` / `bun`; anything else is rejected before scaffolding starts. |
|
|
62
|
-
| `--features` | yes (CSV) | none | Comma-separated optional feature keys. |
|
|
63
|
-
| `--ai` | yes (CSV) | none | Comma-separated AI provider keys (auto-pulls `@warlock.js/ai`). |
|
|
64
|
-
| `--git` / `--no-git` | no | off | Initialize a Git repo (`git init` + `main` branch + initial commit).|
|
|
65
|
-
| `--jwt` / `--no-jwt` | no | off | Generate JWT secrets via the project's `jwt` script. |
|
|
66
|
-
| `--yes` / `-y` | no | off | Non-interactive mode. |
|
|
52
|
+
## After scaffolding: the decisions summary
|
|
53
|
+
|
|
54
|
+
Because almost every choice defaults silently, `create-warlock` prints what it
|
|
55
|
+
decided right after scaffolding, e.g.:
|
|
67
56
|
|
|
68
|
-
|
|
57
|
+
```
|
|
58
|
+
Using npm · database: none · agent-kit targets: claude
|
|
59
|
+
change these — package manager: --pm, database: --db/--no-db, agents: --agents,
|
|
60
|
+
features: --features, ai: --ai, git: --git/--no-git, jwt: --jwt/--no-jwt — or
|
|
61
|
+
re-run with --interactive for the full wizard.
|
|
62
|
+
```
|
|
69
63
|
|
|
70
|
-
|
|
64
|
+
## Flags
|
|
65
|
+
|
|
66
|
+
| Flag | Takes value | Default | Purpose |
|
|
67
|
+
| --------------------------- | ----------- | -------------- | ------------------------------------------------------------------- |
|
|
68
|
+
| `<positional>` / `--name` | yes | — (required) | Project name + target directory. |
|
|
69
|
+
| `--stack` | yes | `api` | `api` or `web` — the one structural question; `web` seeds the `web` feature by default. |
|
|
70
|
+
| `--db` | yes | `mongodb` | Database driver key (`mongodb`, `postgres`, or `none`). |
|
|
71
|
+
| `--no-db` | no | — | Opt out of a database — shorthand for `--db=none`. |
|
|
72
|
+
| `--pm` | yes | inferred (`npm_config_user_agent` / system) | Package manager — must be one of `npm` / `yarn` / `pnpm` / `bun`; anything else is rejected before scaffolding starts. |
|
|
73
|
+
| `--features` | yes (CSV) | none, or `["web"]` when `--stack=web` and `--features` is not given | Comma-separated optional feature keys. An explicit `--features` always wins over the `--stack` default. |
|
|
74
|
+
| `--ai` | yes (CSV) | none | Comma-separated AI provider keys (auto-pulls `@warlock.js/ai`). |
|
|
75
|
+
| `--agents` | yes (CSV) | `claude` | Comma-separated `agent-kit` targets — see below. |
|
|
76
|
+
| `--git` / `--no-git` | no | off | Initialize a Git repo (`git init` + `main` branch + initial commit).|
|
|
77
|
+
| `--jwt` / `--no-jwt` | no | off | Generate JWT secrets via the project's `jwt` script. |
|
|
78
|
+
| `--yes` / `-y` | no | off | Fully headless mode. |
|
|
79
|
+
| `--interactive`, `--customize` | no | off | Restore the full long-form wizard. |
|
|
80
|
+
|
|
81
|
+
Value flags accept either `--db=postgres` or `--db postgres`. Unknown `--features` / `--ai` / `--agents` keys fail fast before any install.
|
|
82
|
+
|
|
83
|
+
`--help`/`-h` and `--version`/`-v` short-circuit before anything else — no prompt, filesystem write, or network call, and `--help` wins even over a positional project name. `--version` prints the installed `create-warlock` version. `--help` lists every flag above together with its default.
|
|
84
|
+
|
|
85
|
+
## `--agents` and agent-kit targets
|
|
86
|
+
|
|
87
|
+
`--agents` picks which `agent-kit` targets the generated project derives
|
|
88
|
+
docs/skills for (written into the scaffolded `package.json`'s `agentKit.targets`
|
|
89
|
+
when it differs from the default). The default, `claude`, is a tiny built-in
|
|
90
|
+
fallback that always works — even fully offline. Any other target
|
|
91
|
+
(`cursor`, `codex`, `kiro`, `copilot`, `antigravity`, `opencode`, `amp`,
|
|
92
|
+
`goose`, …) is validated against the list published at
|
|
93
|
+
https://mongez.js.org/agent-kit/llms.txt, fetched once and cached locally. If
|
|
94
|
+
that fetch fails for an explicitly requested non-default target, the run fails
|
|
95
|
+
visibly and names every target it *could* determine — it never silently
|
|
96
|
+
guesses.
|
|
71
97
|
|
|
72
98
|
## Valid keys
|
|
73
99
|
|
|
@@ -110,11 +136,12 @@ The template ships a working auth module (login / logout / logout-all / refresh
|
|
|
110
136
|
|
|
111
137
|
## Setup steps the scaffolder runs
|
|
112
138
|
|
|
113
|
-
1. Copy template → substitute name → wire `DB_DRIVER` / `DB_PORT` into `.env` (or, for `--db=none`, remove `src/config/database.ts`).
|
|
139
|
+
1. Copy template → substitute name → wire `DB_DRIVER` / `DB_PORT` into `.env` (or, for `--db=none`, remove `src/config/database.ts`) → record non-default `--agents` targets into `package.json`'s `agentKit.targets`.
|
|
114
140
|
2. Install base dependencies (so the `warlock` binary exists).
|
|
115
141
|
3. `warlock add <driver> <features...> <ai...> --no-install`, then one batched install. The `<driver>` is omitted for `--db=none`.
|
|
116
142
|
4. `git init` (if `--git`).
|
|
117
143
|
5. `jwt` script (if `--jwt`) — otherwise `warlock --warm-cache`.
|
|
144
|
+
6. Print the decisions summary.
|
|
118
145
|
|
|
119
146
|
Failures surface the error and halt; there is no rollback. After it finishes, `cd my-app` and run the dev server.
|
|
120
147
|
|
|
@@ -110,7 +110,7 @@ Three things that trip everyone up on a fresh app:
|
|
|
110
110
|
field is required" message), but it is redundant on a plain field, so the
|
|
111
111
|
generated code and these docs don't use it.
|
|
112
112
|
- **Declare server-set foreign keys on the MODEL schema.** A controller that
|
|
113
|
-
sets a column the client doesn't send (e.g. `authorId: request.user.id`) must
|
|
113
|
+
sets a column the client doesn't send (e.g. `authorId: request.locals.user.id`) must
|
|
114
114
|
still have that column declared on the model's seal schema
|
|
115
115
|
(`authorId: v.number()`), or `Model.create()` rejects it as an unknown key.
|
|
116
116
|
The client-facing request schema and the model schema are separate, so this
|
|
@@ -354,11 +354,11 @@ createLeadController.validation = {
|
|
|
354
354
|
|
|
355
355
|
| Route kind | Controller type | What it adds |
|
|
356
356
|
| -------------------------------- | ------------------------------------- | ------------------------------------- |
|
|
357
|
-
| Guarded (auth required) + body | `GuardedRequestHandler<Schema>` | typed `request.validated()` + `request.user` |
|
|
357
|
+
| Guarded (auth required) + body | `GuardedRequestHandler<Schema>` | typed `request.validated()` + `request.locals.user` |
|
|
358
358
|
| Public + body | `RequestHandler<Request<Schema>>` | typed `request.validated()` |
|
|
359
359
|
| Read / list (no body) | `RequestHandler` | nothing extra |
|
|
360
360
|
|
|
361
|
-
`GuardedRequestHandler<Schema>` (defined in `app/auth/requests/guarded.request.ts`) is `RequestHandler<GuardedRequest<Schema>>` — it both types the validated payload and guarantees `request.user` is present.
|
|
361
|
+
`GuardedRequestHandler<Schema>` (defined in `app/auth/requests/guarded.request.ts`) is `RequestHandler<GuardedRequest<Schema>>` — it both types the validated payload and guarantees `request.locals.user` is present.
|
|
362
362
|
|
|
363
363
|
### 6.3 Accessing inputs
|
|
364
364
|
|
|
@@ -95,7 +95,7 @@ AuthN proves who you are. AuthZ decides what you can do. Conflating them is how
|
|
|
95
95
|
const order = await ordersRepository.find({ id, userId: request.input("userId") });
|
|
96
96
|
|
|
97
97
|
// ✅ identity from the authenticated session
|
|
98
|
-
const user = request.user;
|
|
98
|
+
const user = request.locals.user;
|
|
99
99
|
const order = await ordersRepository.find({ id, userId: user.id });
|
|
100
100
|
```
|
|
101
101
|
|
|
@@ -9,7 +9,7 @@ import { logoutAllService } from "../services/auth.service";
|
|
|
9
9
|
* Guarded route — see the note on `logoutController`.
|
|
10
10
|
*/
|
|
11
11
|
export const logoutAllController: GuardedRequestHandler = async ({ request, response }) => {
|
|
12
|
-
await logoutAllService(request.user);
|
|
12
|
+
await logoutAllService(request.locals.user);
|
|
13
13
|
|
|
14
14
|
return response.success({
|
|
15
15
|
message: t("auth.loggedOutAll"),
|
|
@@ -7,11 +7,11 @@ import { logoutService } from "../services/auth.service";
|
|
|
7
7
|
* POST /auth/logout
|
|
8
8
|
*
|
|
9
9
|
* Typed as `GuardedRequestHandler` because the route sits inside `guarded()`
|
|
10
|
-
* (see `../routes.ts`). A plain `RequestHandler` types `request.user`
|
|
11
|
-
* `RequestUser | undefined`, which is not assignable to `logoutService`.
|
|
10
|
+
* (see `../routes.ts`). A plain `RequestHandler` types `request.locals.user`
|
|
11
|
+
* as `RequestUser | undefined`, which is not assignable to `logoutService`.
|
|
12
12
|
*/
|
|
13
13
|
export const logoutController: GuardedRequestHandler = async ({ request, response }) => {
|
|
14
|
-
await logoutService(request.user);
|
|
14
|
+
await logoutService(request.locals.user);
|
|
15
15
|
|
|
16
16
|
return response.success({
|
|
17
17
|
message: t("auth.loggedOut"),
|
|
@@ -11,7 +11,7 @@ import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
|
11
11
|
*/
|
|
12
12
|
export const meController: GuardedRequestHandler = async ({ request, response }) => {
|
|
13
13
|
return response.success({
|
|
14
|
-
user: request.user,
|
|
14
|
+
user: request.locals.user,
|
|
15
15
|
});
|
|
16
16
|
};
|
|
17
17
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Request, RequestHandler } from "@warlock.js/core";
|
|
1
|
+
import type { Request, RequestHandler, RequestLocals } from "@warlock.js/core";
|
|
2
2
|
import type { User } from "app/users/models/user";
|
|
3
3
|
|
|
4
4
|
export type GuardedRequest<RequestPayload = unknown> = Request<RequestPayload> & {
|
|
5
|
-
user: User;
|
|
5
|
+
locals: RequestLocals & { user: User };
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
export type GuardedRequestHandler<RequestPayload = unknown> = RequestHandler<
|
|
@@ -7,13 +7,18 @@ import { UserResource } from "app/users/resources/user.resource";
|
|
|
7
7
|
export const userSchema = v.object({
|
|
8
8
|
name: v.string().required(),
|
|
9
9
|
email: v.email().requiredIfEmpty("id"),
|
|
10
|
-
|
|
10
|
+
// Required at the controller layer (`create-user.schema.ts`) on
|
|
11
|
+
// registration, but not here: a freshly-registered user's record is
|
|
12
|
+
// written before that upload completes, and a seeded/social-login user may
|
|
13
|
+
// never get an avatar at all.
|
|
14
|
+
image: v.string().optional(),
|
|
11
15
|
password: v.string().min(6).requiredIfEmpty("id").addTransformer(useHashedPassword()),
|
|
12
16
|
// Written by the social-login handler (`app/users/services/login-social.ts`).
|
|
13
17
|
// A field only reachable through `save({ merge })` still has to be declared
|
|
14
18
|
// here — `merge` is typed against this schema, so an undeclared key is a
|
|
15
|
-
// compile error, not a silent write.
|
|
16
|
-
lastLogin
|
|
19
|
+
// compile error, not a silent write. Optional: a user who has never logged
|
|
20
|
+
// in (freshly registered or freshly seeded) has no lastLogin yet.
|
|
21
|
+
lastLogin: v.date().optional(),
|
|
17
22
|
});
|
|
18
23
|
|
|
19
24
|
export type UserSchema = Infer<typeof userSchema>;
|
|
@@ -5,11 +5,12 @@ import type { GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
|
5
5
|
*
|
|
6
6
|
* Despite living under `services/`, this is a route handler: it consumes the
|
|
7
7
|
* request and returns a response. It is typed as a `GuardedRequestHandler` so
|
|
8
|
-
* it can be wired straight into a route, and so `request.user`
|
|
9
|
-
* app's `User` model rather than core's optional
|
|
8
|
+
* it can be wired straight into a route, and so `request.locals.user`
|
|
9
|
+
* resolves to the app's `User` model rather than core's optional
|
|
10
|
+
* `RequestUser`.
|
|
10
11
|
*/
|
|
11
12
|
const loginSocial: GuardedRequestHandler = async ({ request, response }) => {
|
|
12
|
-
const user = request.user;
|
|
13
|
+
const user = request.locals.user;
|
|
13
14
|
|
|
14
15
|
const auth = await user.generateAccessToken();
|
|
15
16
|
|
|
@@ -53,20 +53,26 @@ declare module "@warlock.js/core" {
|
|
|
53
53
|
* ```
|
|
54
54
|
*/
|
|
55
55
|
interface RequestLocals {}
|
|
56
|
+
}
|
|
56
57
|
|
|
58
|
+
declare module "@warlock.js/auth" {
|
|
57
59
|
/**
|
|
58
|
-
* The authenticated user — `request.user`.
|
|
60
|
+
* The authenticated user — `request.locals.user`.
|
|
59
61
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
62
|
+
* `@warlock.js/auth` already augments core's `RequestLocals` with an
|
|
63
|
+
* optional `user?: RequestUser` key; this augments `RequestUser` itself
|
|
64
|
+
* (owned by `@warlock.js/auth`, not core — core removed its own `request.user`
|
|
65
|
+
* and the `RequestUser` it used to declare). The auth middleware assigns the
|
|
66
|
+
* resolved user model here. Declaring its shape gives unguarded handlers
|
|
67
|
+
* (ones that may or may not have a user) a real type instead of `{}`.
|
|
63
68
|
*
|
|
64
|
-
* This does NOT make `request.user` non-optional:
|
|
65
|
-
*
|
|
69
|
+
* This does NOT make `request.locals.user` non-optional: it stays
|
|
70
|
+
* `user?: RequestUser`, and augmentation can add members but cannot remove
|
|
66
71
|
* the `?`. For routes that are actually behind the auth guard, type the
|
|
67
72
|
* handler as `GuardedRequestHandler` (see
|
|
68
|
-
* `src/app/auth/requests/guarded.request.ts`) — that narrows
|
|
69
|
-
* to the app's `User` model AND drops the
|
|
73
|
+
* `src/app/auth/requests/guarded.request.ts`) — that narrows
|
|
74
|
+
* `request.locals.user` to the app's `User` model AND drops the
|
|
75
|
+
* `| undefined`.
|
|
70
76
|
*
|
|
71
77
|
* @example
|
|
72
78
|
* ```ts
|
|
@@ -1,23 +1,40 @@
|
|
|
1
1
|
import { Form } from "@mongez/react-form";
|
|
2
|
-
import { useTrans } from "@warlock.js/web";
|
|
2
|
+
import { changeLocaleCode, useLocale, useTextDirection, useTrans } from "@warlock.js/web";
|
|
3
|
+
import { useState } from "react";
|
|
3
4
|
import { contactSchema } from "../../../shared/contact.schema";
|
|
4
|
-
import type { LocaleCode } from "../../../shared/locales";
|
|
5
5
|
import { useContactForm } from "../hooks/use-contact-form";
|
|
6
|
-
import { useLocaleSwitcher } from "../hooks/use-locale-switcher";
|
|
7
6
|
import { ContactField, ContactSubmitButton } from "./contact-form-controls";
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export function ContactSection({ locale }: ContactSectionProps) {
|
|
8
|
+
export function ContactSection() {
|
|
12
9
|
const { status: contactStatus, submitContact } = useContactForm();
|
|
13
|
-
const
|
|
10
|
+
const locale = useLocale();
|
|
11
|
+
const direction = useTextDirection();
|
|
14
12
|
const translate = useTrans();
|
|
13
|
+
const [isSwitchingLocale, setIsSwitchingLocale] = useState(false);
|
|
14
|
+
const [localeError, setLocaleError] = useState("");
|
|
15
|
+
|
|
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
|
+
}
|
|
15
36
|
return (
|
|
16
|
-
<section
|
|
17
|
-
className="warlock-section warlock-contact"
|
|
18
|
-
id="contact"
|
|
19
|
-
dir={locale === "ar" ? "rtl" : "ltr"}
|
|
20
|
-
>
|
|
37
|
+
<section className="warlock-section warlock-contact" id="contact" dir={direction}>
|
|
21
38
|
<div className="warlock-contact-copy">
|
|
22
39
|
<p className="warlock-overline">{translate("contact.overline")}</p>
|
|
23
40
|
<h2>{translate("contact.title")}</h2>
|
|
@@ -62,8 +79,8 @@ export function ContactSection({ locale }: ContactSectionProps) {
|
|
|
62
79
|
<button
|
|
63
80
|
className="warlock-locale-toggle"
|
|
64
81
|
type="button"
|
|
65
|
-
disabled={
|
|
66
|
-
onClick={
|
|
82
|
+
disabled={isSwitchingLocale}
|
|
83
|
+
onClick={toggleLocale}
|
|
67
84
|
>
|
|
68
85
|
{translate("contact.toggle")}
|
|
69
86
|
</button>
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { http } from "@mongez/http";
|
|
2
1
|
import { type FormSubmitOptions, type InferFormValues } from "@mongez/react-form";
|
|
3
2
|
import { useState } from "react";
|
|
4
3
|
import type { contactSchema } from "../../../shared/contact.schema";
|
|
@@ -14,6 +13,11 @@ type ContactResponse = {
|
|
|
14
13
|
};
|
|
15
14
|
};
|
|
16
15
|
|
|
16
|
+
type ContactErrorBody = {
|
|
17
|
+
message?: string;
|
|
18
|
+
errors?: unknown;
|
|
19
|
+
};
|
|
20
|
+
|
|
17
21
|
type ContactStatus = {
|
|
18
22
|
state: "idle" | "submitting" | "success" | "error";
|
|
19
23
|
message: string;
|
|
@@ -29,21 +33,28 @@ export function useContactForm() {
|
|
|
29
33
|
setStatus({ state: "submitting", message: "Sending to the backend…" });
|
|
30
34
|
|
|
31
35
|
try {
|
|
32
|
-
const
|
|
36
|
+
const response = await fetch("/api/contact", {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
body: JSON.stringify(values as ContactValues),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const body = (await response.json().catch(() => ({}))) as ContactErrorBody &
|
|
43
|
+
Partial<ContactResponse>;
|
|
33
44
|
|
|
34
|
-
if (
|
|
35
|
-
if (
|
|
36
|
-
setFormErrors(
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
if (response.status === 422) {
|
|
47
|
+
setFormErrors({ body }, form);
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
setStatus({
|
|
40
51
|
state: "error",
|
|
41
|
-
message:
|
|
52
|
+
message: body.message || "The request was not accepted.",
|
|
42
53
|
});
|
|
43
54
|
return;
|
|
44
55
|
}
|
|
45
56
|
|
|
46
|
-
setStatus({ state: "success", message:
|
|
57
|
+
setStatus({ state: "success", message: body.message ?? "" });
|
|
47
58
|
form.reset();
|
|
48
59
|
} catch (error) {
|
|
49
60
|
setStatus({
|
|
@@ -37,7 +37,7 @@ export default function HomePage({ data }: HomePageProps) {
|
|
|
37
37
|
<HomeHeader />
|
|
38
38
|
<HeroSection statusMessage={data.statusMessage} />
|
|
39
39
|
<ContentSections capabilities={data.capabilities} packages={data.packages} />
|
|
40
|
-
<ContactSection
|
|
40
|
+
<ContactSection />
|
|
41
41
|
<HomeFooter />
|
|
42
42
|
</main>
|
|
43
43
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AppProps } from "@warlock.js/web";
|
|
2
|
-
import { Head, Scripts } from "@warlock.js/web";
|
|
2
|
+
import { Head, Scripts, useLocale, useTextDirection } from "@warlock.js/web";
|
|
3
3
|
import "./app.css";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -9,8 +9,11 @@ import "./app.css";
|
|
|
9
9
|
* again in the browser during hydration, where neither exists.
|
|
10
10
|
*/
|
|
11
11
|
export default function App({ children }: AppProps) {
|
|
12
|
+
const locale = useLocale();
|
|
13
|
+
const direction = useTextDirection();
|
|
14
|
+
|
|
12
15
|
return (
|
|
13
|
-
<html lang=
|
|
16
|
+
<html lang={locale} dir={direction}>
|
|
14
17
|
<head>
|
|
15
18
|
{/*
|
|
16
19
|
Placement only. The framework injects the page's `metadata`, the
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { http } from "@mongez/http";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import { type LocaleCode } from "../../../shared/locales";
|
|
4
|
-
|
|
5
|
-
type LocalePreferenceResponse = {
|
|
6
|
-
locale: LocaleCode;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export function useLocaleSwitcher() {
|
|
10
|
-
const [isSwitching, setIsSwitching] = useState(false);
|
|
11
|
-
const [error, setError] = useState("");
|
|
12
|
-
|
|
13
|
-
async function switchLocale(locale: LocaleCode): Promise<void> {
|
|
14
|
-
setIsSwitching(true);
|
|
15
|
-
setError("");
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const result = await http.post<LocalePreferenceResponse>("/api/locale", { locale });
|
|
19
|
-
|
|
20
|
-
if (result.error) {
|
|
21
|
-
setError(result.error.message || "Could not update the locale.");
|
|
22
|
-
setIsSwitching(false);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
window.location.hash = "contact";
|
|
27
|
-
window.location.reload();
|
|
28
|
-
} catch (caughtError) {
|
|
29
|
-
setError(caughtError instanceof Error ? caughtError.message : "Could not update the locale.");
|
|
30
|
-
setIsSwitching(false);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return { error, isSwitching, switchLocale };
|
|
35
|
-
}
|