feat: add initial account creation form
This commit is contained in:
@@ -10,16 +10,20 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@spec/relay": "workspace:*",
|
||||||
|
"@spec/schemas": "workspace:*",
|
||||||
"@tanstack/react-query": "5",
|
"@tanstack/react-query": "5",
|
||||||
"@tanstack/react-router": "1",
|
"@tanstack/react-router": "1",
|
||||||
"@valkyr/db": "1",
|
"@valkyr/db": "1",
|
||||||
"@valkyr/event-emitter": "npm:@jsr/valkyr__event-emitter@1",
|
"@valkyr/event-emitter": "npm:@jsr/valkyr__event-emitter@1",
|
||||||
"fast-equals": "5",
|
"fast-equals": "5",
|
||||||
"react": "19",
|
"react": "19",
|
||||||
"react-dom": "19"
|
"react-dom": "19",
|
||||||
|
"zod": "4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9",
|
"@eslint/js": "9",
|
||||||
|
"@tanstack/react-router-devtools": "1",
|
||||||
"@types/react": "19",
|
"@types/react": "19",
|
||||||
"@types/react-dom": "19",
|
"@types/react-dom": "19",
|
||||||
"@vitejs/plugin-react": "4",
|
"@vitejs/plugin-react": "4",
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
#root {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
.logo.react:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
a:nth-of-type(2) .logo {
|
|
||||||
animation: logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import "./App.css";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import viteLogo from "/vite.svg";
|
|
||||||
|
|
||||||
import reactLogo from "./assets/react.svg";
|
|
||||||
import { Session } from "./components/Session.tsx";
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [count, setCount] = useState(0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<a href="https://vite.dev" target="_blank">
|
|
||||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
|
||||||
</a>
|
|
||||||
<a href="https://react.dev" target="_blank">
|
|
||||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<h1>Vite + React</h1>
|
|
||||||
<div className="card">
|
|
||||||
<button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
|
|
||||||
<p>
|
|
||||||
Edit <code>src/App.tsx</code> and save to test HMR
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p className="read-the-docs">Click on the Vite and React logos to learn more</p>
|
|
||||||
<Session />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
:root {
|
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
269
apps/react/src/libraries/form.ts
Normal file
269
apps/react/src/libraries/form.ts
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
import z, { type ZodObject, type ZodRawShape } from "zod";
|
||||||
|
|
||||||
|
export class Form<TSchema extends ZodRawShape, TInputs = z.infer<ZodObject<TSchema>>> {
|
||||||
|
readonly schema: ZodObject<TSchema>;
|
||||||
|
|
||||||
|
readonly inputs: Partial<TInputs> = {};
|
||||||
|
|
||||||
|
#debounce: FormDebounce<TInputs> = {
|
||||||
|
validate: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
#defaults: Partial<TInputs>;
|
||||||
|
#errors: FormErrors<TInputs> = {};
|
||||||
|
#elements: Record<string, HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement> = {};
|
||||||
|
|
||||||
|
#onChange?: OnChangeCallback<TInputs>;
|
||||||
|
#onProcessing?: OnProcessingCallback;
|
||||||
|
#onError?: OnErrorCallback<TInputs>;
|
||||||
|
#onSubmit?: OnSubmitCallback<TInputs>;
|
||||||
|
#onResponse?: OnResponseCallback<any, any>;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
| Constructor
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
constructor(schema: TSchema, defaults: Partial<TInputs> = {}) {
|
||||||
|
this.schema = z.object(schema);
|
||||||
|
this.#defaults = defaults;
|
||||||
|
this.#bindMethods();
|
||||||
|
this.#setDefaults();
|
||||||
|
this.#setSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
#bindMethods() {
|
||||||
|
this.register = this.register.bind(this);
|
||||||
|
this.set = this.set.bind(this);
|
||||||
|
this.get = this.get.bind(this);
|
||||||
|
this.validate = this.validate.bind(this);
|
||||||
|
this.submit = this.submit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
#setDefaults() {
|
||||||
|
for (const key in this.#defaults) {
|
||||||
|
this.inputs[key] = this.#defaults[key] ?? ("" as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#setSubmit() {
|
||||||
|
if ((this.constructor as any).submit !== undefined) {
|
||||||
|
this.onSubmit((this.constructor as any).submit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
| Accessors
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
get isValid(): boolean {
|
||||||
|
return Object.keys(this.#getFormErrors()).length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasError() {
|
||||||
|
return Object.keys(this.errors).length !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get errors(): FormErrors<TInputs> {
|
||||||
|
return this.#errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
set errors(value: FormErrors<TInputs>) {
|
||||||
|
this.#errors = value;
|
||||||
|
this.#onError?.(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a input element with the form. This registers form related methods and a
|
||||||
|
* reference to the element itself that can be utilized by the form.
|
||||||
|
*
|
||||||
|
* @param name - Name of the input field.
|
||||||
|
*/
|
||||||
|
register<TKey extends keyof TInputs>(name: TKey) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
ref: (element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null) => {
|
||||||
|
if (element !== null) {
|
||||||
|
this.#elements[name as string] = element;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultValue: this.get(name),
|
||||||
|
onChange: ({ target: { value } }: any) => {
|
||||||
|
this.set(name, value);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
| Registrars
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
onChange(callback: OnChangeCallback<TInputs>): this {
|
||||||
|
this.#onChange = callback;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
onProcessing(callback: OnProcessingCallback): this {
|
||||||
|
this.#onProcessing = callback;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
onError(callback: OnErrorCallback<TInputs>): this {
|
||||||
|
this.#onError = callback;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(callback: OnSubmitCallback<TInputs>): this {
|
||||||
|
this.#onSubmit = callback;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
onResponse<E, R>(callback: OnResponseCallback<E, R>): this {
|
||||||
|
this.#onResponse = callback;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
| Data
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value of an input field.
|
||||||
|
*
|
||||||
|
* @param name - Name of the input field.
|
||||||
|
* @param value - Value to set.
|
||||||
|
*/
|
||||||
|
set<TKey extends keyof TInputs>(name: TKey, value: TInputs[TKey]): void {
|
||||||
|
this.inputs[name] = value;
|
||||||
|
this.#onChange?.(name, value);
|
||||||
|
clearTimeout(this.#debounce.validate[name]);
|
||||||
|
this.#debounce.validate[name] = setTimeout(() => {
|
||||||
|
this.validate(name);
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current input values or a specific input value.
|
||||||
|
*
|
||||||
|
* @param name - Name of the input field. _(Optional)_
|
||||||
|
*/
|
||||||
|
get(): Partial<TInputs>;
|
||||||
|
get<TKey extends keyof TInputs>(name: TKey): TInputs[TKey] | undefined;
|
||||||
|
get<TKey extends keyof TInputs>(name?: TKey): Partial<TInputs> | TInputs[TKey] | undefined {
|
||||||
|
if (name === undefined) {
|
||||||
|
return { ...this.inputs };
|
||||||
|
}
|
||||||
|
return this.inputs[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset form back to its default values.
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
for (const key in this.inputs) {
|
||||||
|
const value = this.#defaults[key] ?? "";
|
||||||
|
(this.inputs as any)[key] = value;
|
||||||
|
if (this.#elements[key] !== undefined) {
|
||||||
|
(this.#elements as any)[key].value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
| Submission
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
async submit(event: any) {
|
||||||
|
event.preventDefault?.();
|
||||||
|
this.#onProcessing?.(true);
|
||||||
|
this.validate();
|
||||||
|
if (this.hasError === false) {
|
||||||
|
try {
|
||||||
|
const response = await this.#onSubmit?.(this.schema.parse(this.inputs) as TInputs);
|
||||||
|
this.#onResponse?.(undefined, response);
|
||||||
|
} catch (error) {
|
||||||
|
this.#onResponse?.(error, undefined as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#onProcessing?.(false);
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(name?: keyof TInputs) {
|
||||||
|
if (name !== undefined) {
|
||||||
|
this.#validateInput(name);
|
||||||
|
} else {
|
||||||
|
this.#validateForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#validateForm(): void {
|
||||||
|
this.errors = this.#getFormErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
#validateInput(name: keyof TInputs): void {
|
||||||
|
const errors = this.#getFormErrors();
|
||||||
|
let hasChanges = false;
|
||||||
|
if (errors[name] === undefined && this.errors[name] !== undefined) {
|
||||||
|
delete this.errors[name];
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
if (errors[name] !== undefined && this.errors[name] !== errors[name]) {
|
||||||
|
this.errors[name] = errors[name];
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
if (hasChanges === true) {
|
||||||
|
this.#onError?.({ ...this.errors });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#getFormErrors(): FormErrors<TInputs> {
|
||||||
|
const result = this.schema.safeParse(this.inputs);
|
||||||
|
if (result.success === false) {
|
||||||
|
throw result.error.flatten;
|
||||||
|
// return result.error.details.reduce<Partial<TInputs>>(
|
||||||
|
// (error, next) => ({
|
||||||
|
// ...error,
|
||||||
|
// [next.path[0]]: next.message,
|
||||||
|
// }),
|
||||||
|
// {},
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
| Types
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
type OnChangeCallback<TInputs, TKey extends keyof TInputs = keyof TInputs> = (name: TKey, value: TInputs[TKey]) => void;
|
||||||
|
|
||||||
|
type OnProcessingCallback = (value: boolean) => void;
|
||||||
|
|
||||||
|
type OnErrorCallback<TInputs> = (errors: FormErrors<TInputs>) => void;
|
||||||
|
|
||||||
|
type OnSubmitCallback<TInputs> = (inputs: TInputs) => Promise<any>;
|
||||||
|
|
||||||
|
type OnResponseCallback<Error, Response> = (err: Error, res: Response) => void;
|
||||||
|
|
||||||
|
type FormDebounce<TInputs> = {
|
||||||
|
validate: {
|
||||||
|
[TKey in keyof TInputs]?: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormErrors<TInputs> = {
|
||||||
|
[TKey in keyof TInputs]?: string;
|
||||||
|
};
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
import "./index.css";
|
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
||||||
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
import { routeTree } from "./routes.tsx";
|
import { routeTree } from "./routes.tsx";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
const router = createRouter({ routeTree });
|
const router = createRouter({ routeTree });
|
||||||
|
|
||||||
declare module "@tanstack/react-router" {
|
declare module "@tanstack/react-router" {
|
||||||
@@ -19,8 +14,6 @@ declare module "@tanstack/react-router" {
|
|||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<RouterProvider router={router} />
|
||||||
<RouterProvider router={router} />
|
|
||||||
</QueryClientProvider>
|
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,29 @@
|
|||||||
import { createRootRoute } from "@tanstack/react-router";
|
import { createRootRoute, createRoute, Outlet } from "@tanstack/react-router";
|
||||||
|
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||||
|
|
||||||
import App from "./App.tsx";
|
import { CreateAccountView } from "./views/account/create.view.tsx";
|
||||||
|
|
||||||
const rootRoute = createRootRoute({
|
const rootRoute = createRootRoute({
|
||||||
component: App,
|
component: () => (
|
||||||
|
<>
|
||||||
|
<Outlet />
|
||||||
|
<TanStackRouterDevtools />
|
||||||
|
</>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const routeTree = rootRoute.addChildren([]);
|
const homeRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: "/",
|
||||||
|
component: function Index() {
|
||||||
|
return <h3>Welcome Home!</h3>;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createAccountRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: "/accounts",
|
||||||
|
component: CreateAccountView,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const routeTree = rootRoute.addChildren([homeRoute, createAccountRoute]);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const api = makeClient(
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
account: (await import("@spec/modules/account/mod.ts")).routes,
|
account: (await import("@spec/schemas/account/routes.ts")).routes,
|
||||||
auth: (await import("@spec/modules/auth/mod.ts")).routes,
|
auth: (await import("@spec/schemas/auth/routes.ts")).routes,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
36
apps/react/src/views/account/create.controller.ts
Normal file
36
apps/react/src/views/account/create.controller.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { Controller } from "../../libraries/controller.ts";
|
||||||
|
import { Form } from "../../libraries/form.ts";
|
||||||
|
import { api } from "../../services/api.ts";
|
||||||
|
|
||||||
|
const inputs = {
|
||||||
|
givenName: z.string(),
|
||||||
|
familyName: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export class CreateController extends Controller<{
|
||||||
|
form: Form<typeof inputs>;
|
||||||
|
}> {
|
||||||
|
async onInit() {
|
||||||
|
return {
|
||||||
|
form: new Form(inputs).onSubmit(async ({ givenName, familyName, email }) => {
|
||||||
|
const response = await api.account.create({
|
||||||
|
body: {
|
||||||
|
name: {
|
||||||
|
given: givenName,
|
||||||
|
family: familyName,
|
||||||
|
},
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if ("error" in response) {
|
||||||
|
console.log(response.error);
|
||||||
|
} else {
|
||||||
|
console.log(response.data);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
13
apps/react/src/views/account/create.view.tsx
Normal file
13
apps/react/src/views/account/create.view.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { makeControllerView } from "../../libraries/view.ts";
|
||||||
|
import { CreateController } from "./create.controller.ts";
|
||||||
|
|
||||||
|
export const CreateAccountView = makeControllerView(CreateController, ({ state: { form } }) => {
|
||||||
|
return (
|
||||||
|
<form onSubmit={form.submit}>
|
||||||
|
<input placeholder="Given Name" {...form.register("givenName")} />
|
||||||
|
<input placeholder="Family Name" {...form.register("familyName")} />
|
||||||
|
<input placeholder="Email" {...form.register("email")} />
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
});
|
||||||
103
deno.lock
generated
103
deno.lock
generated
@@ -14,11 +14,13 @@
|
|||||||
"npm:@jsr/valkyr__event-store@2.0.0-beta.6": "2.0.0-beta.6",
|
"npm:@jsr/valkyr__event-store@2.0.0-beta.6": "2.0.0-beta.6",
|
||||||
"npm:@jsr/valkyr__inverse@1": "1.0.1",
|
"npm:@jsr/valkyr__inverse@1": "1.0.1",
|
||||||
"npm:@tanstack/react-query@5": "5.84.2_react@19.1.1",
|
"npm:@tanstack/react-query@5": "5.84.2_react@19.1.1",
|
||||||
|
"npm:@tanstack/react-router-devtools@1": "1.131.7_@tanstack+react-router@1.131.5__react@19.1.1__react-dom@19.1.1___react@19.1.1_react@19.1.1_react-dom@19.1.1__react@19.1.1",
|
||||||
"npm:@tanstack/react-router@1": "1.131.5_react@19.1.1_react-dom@19.1.1__react@19.1.1",
|
"npm:@tanstack/react-router@1": "1.131.5_react@19.1.1_react-dom@19.1.1__react@19.1.1",
|
||||||
|
"npm:@types/node@*": "22.15.15",
|
||||||
"npm:@types/react-dom@19": "19.1.7_@types+react@19.1.9",
|
"npm:@types/react-dom@19": "19.1.7_@types+react@19.1.9",
|
||||||
"npm:@types/react@19": "19.1.9",
|
"npm:@types/react@19": "19.1.9",
|
||||||
"npm:@valkyr/db@1": "1.0.1",
|
"npm:@valkyr/db@1": "1.0.1",
|
||||||
"npm:@vitejs/plugin-react@4": "4.7.0_vite@7.1.2__picomatch@4.0.3_@babel+core@7.28.0",
|
"npm:@vitejs/plugin-react@4": "4.7.0_vite@7.1.2__picomatch@4.0.3_@babel+core@7.28.0_@types+node@22.15.15",
|
||||||
"npm:cookie@1": "1.0.2",
|
"npm:cookie@1": "1.0.2",
|
||||||
"npm:eslint-plugin-react-hooks@5": "5.2.0_eslint@9.33.0",
|
"npm:eslint-plugin-react-hooks@5": "5.2.0_eslint@9.33.0",
|
||||||
"npm:eslint-plugin-react-refresh@0.4": "0.4.20_eslint@9.33.0",
|
"npm:eslint-plugin-react-refresh@0.4": "0.4.20_eslint@9.33.0",
|
||||||
@@ -33,7 +35,8 @@
|
|||||||
"npm:react@19": "19.1.1",
|
"npm:react@19": "19.1.1",
|
||||||
"npm:typescript-eslint@8": "8.39.1_eslint@9.33.0_typescript@5.9.2_@typescript-eslint+parser@8.39.1__eslint@9.33.0__typescript@5.9.2",
|
"npm:typescript-eslint@8": "8.39.1_eslint@9.33.0_typescript@5.9.2_@typescript-eslint+parser@8.39.1__eslint@9.33.0__typescript@5.9.2",
|
||||||
"npm:typescript@5": "5.9.2",
|
"npm:typescript@5": "5.9.2",
|
||||||
"npm:vite@7": "7.1.2_picomatch@4.0.3",
|
"npm:vite@7": "7.1.2_picomatch@4.0.3_@types+node@22.15.15",
|
||||||
|
"npm:vite@7.1.2": "7.1.2_picomatch@4.0.3_@types+node@22.15.15",
|
||||||
"npm:zod@4": "4.0.17"
|
"npm:zod@4": "4.0.17"
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
@@ -670,12 +673,21 @@
|
|||||||
"react"
|
"react"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@tanstack/react-router-devtools@1.131.7_@tanstack+react-router@1.131.5__react@19.1.1__react-dom@19.1.1___react@19.1.1_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
|
||||||
|
"integrity": "sha512-RLxjwsD8A9iavGtMA1RhQ+j/gfAdQcEf9pygGk9RZuWV7XJ4RXZeeKQHDKyJ/Rry5NkYbO+eJzeToq/szuQbuw==",
|
||||||
|
"dependencies": [
|
||||||
|
"@tanstack/react-router",
|
||||||
|
"@tanstack/router-devtools-core",
|
||||||
|
"react",
|
||||||
|
"react-dom"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@tanstack/react-router@1.131.5_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
|
"@tanstack/react-router@1.131.5_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
|
||||||
"integrity": "sha512-71suJGuCmrHN9PLLRUDB3CGnW5RNcEEfgfX616TOpKamHs977H8P4/75BgWPRWcLHCga/1kkA6c7bddCwZ35Fw==",
|
"integrity": "sha512-71suJGuCmrHN9PLLRUDB3CGnW5RNcEEfgfX616TOpKamHs977H8P4/75BgWPRWcLHCga/1kkA6c7bddCwZ35Fw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@tanstack/history",
|
"@tanstack/history",
|
||||||
"@tanstack/react-store",
|
"@tanstack/react-store",
|
||||||
"@tanstack/router-core",
|
"@tanstack/router-core@1.131.5_seroval@1.3.2",
|
||||||
"isbot",
|
"isbot",
|
||||||
"react",
|
"react",
|
||||||
"react-dom",
|
"react-dom",
|
||||||
@@ -704,6 +716,28 @@
|
|||||||
"tiny-warning"
|
"tiny-warning"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@tanstack/router-core@1.131.7_seroval@1.3.2": {
|
||||||
|
"integrity": "sha512-NpFfAG1muv4abrCij6sEtRrVzlU+xYpY30NAgquHNhMMMNIiN7djzsaGV+vCJdR4u5mi13+f0c3f+f9MdekY5A==",
|
||||||
|
"dependencies": [
|
||||||
|
"@tanstack/history",
|
||||||
|
"@tanstack/store",
|
||||||
|
"cookie-es",
|
||||||
|
"seroval",
|
||||||
|
"seroval-plugins",
|
||||||
|
"tiny-invariant",
|
||||||
|
"tiny-warning"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@tanstack/router-devtools-core@1.131.7_@tanstack+router-core@1.131.7__seroval@1.3.2_solid-js@1.9.9__seroval@1.3.2_tiny-invariant@1.3.3": {
|
||||||
|
"integrity": "sha512-1GHWILJr69Ej/c8UUMhT7Srx392FbsDqRrPhCWWtrjmYOv6Fdx3HdKDJt/YdJGBc8z6x+V7EE41j+LZggD+70Q==",
|
||||||
|
"dependencies": [
|
||||||
|
"@tanstack/router-core@1.131.7_seroval@1.3.2",
|
||||||
|
"clsx",
|
||||||
|
"goober",
|
||||||
|
"solid-js",
|
||||||
|
"tiny-invariant"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@tanstack/store@0.7.2": {
|
"@tanstack/store@0.7.2": {
|
||||||
"integrity": "sha512-RP80Z30BYiPX2Pyo0Nyw4s1SJFH2jyM6f9i3HfX4pA+gm5jsnYryscdq2aIQLnL4TaGuQMO+zXmN9nh1Qck+Pg=="
|
"integrity": "sha512-RP80Z30BYiPX2Pyo0Nyw4s1SJFH2jyM6f9i3HfX4pA+gm5jsnYryscdq2aIQLnL4TaGuQMO+zXmN9nh1Qck+Pg=="
|
||||||
},
|
},
|
||||||
@@ -742,6 +776,12 @@
|
|||||||
"@types/json-schema@7.0.15": {
|
"@types/json-schema@7.0.15": {
|
||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
|
||||||
},
|
},
|
||||||
|
"@types/node@22.15.15": {
|
||||||
|
"integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==",
|
||||||
|
"dependencies": [
|
||||||
|
"undici-types"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@types/react-dom@19.1.7_@types+react@19.1.9": {
|
"@types/react-dom@19.1.7_@types+react@19.1.9": {
|
||||||
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
|
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -884,7 +924,19 @@
|
|||||||
"@rolldown/pluginutils",
|
"@rolldown/pluginutils",
|
||||||
"@types/babel__core",
|
"@types/babel__core",
|
||||||
"react-refresh",
|
"react-refresh",
|
||||||
"vite"
|
"vite@7.1.2_picomatch@4.0.3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@vitejs/plugin-react@4.7.0_vite@7.1.2__picomatch@4.0.3_@babel+core@7.28.0_@types+node@22.15.15": {
|
||||||
|
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
|
||||||
|
"dependencies": [
|
||||||
|
"@babel/core",
|
||||||
|
"@babel/plugin-transform-react-jsx-self",
|
||||||
|
"@babel/plugin-transform-react-jsx-source",
|
||||||
|
"@rolldown/pluginutils",
|
||||||
|
"@types/babel__core",
|
||||||
|
"react-refresh",
|
||||||
|
"vite@7.1.2_picomatch@4.0.3_@types+node@22.15.15"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"acorn-jsx@5.3.2_acorn@8.15.0": {
|
"acorn-jsx@5.3.2_acorn@8.15.0": {
|
||||||
@@ -963,6 +1015,9 @@
|
|||||||
"supports-color"
|
"supports-color"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"clsx@2.1.1": {
|
||||||
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="
|
||||||
|
},
|
||||||
"color-convert@2.0.1": {
|
"color-convert@2.0.1": {
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -1248,6 +1303,12 @@
|
|||||||
"globals@16.3.0": {
|
"globals@16.3.0": {
|
||||||
"integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="
|
"integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="
|
||||||
},
|
},
|
||||||
|
"goober@2.1.16_csstype@3.1.3": {
|
||||||
|
"integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
|
||||||
|
"dependencies": [
|
||||||
|
"csstype"
|
||||||
|
]
|
||||||
|
},
|
||||||
"graphemer@1.4.0": {
|
"graphemer@1.4.0": {
|
||||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
|
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
|
||||||
},
|
},
|
||||||
@@ -1573,6 +1634,14 @@
|
|||||||
"shebang-regex@3.0.0": {
|
"shebang-regex@3.0.0": {
|
||||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
|
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
|
||||||
},
|
},
|
||||||
|
"solid-js@1.9.9_seroval@1.3.2": {
|
||||||
|
"integrity": "sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA==",
|
||||||
|
"dependencies": [
|
||||||
|
"csstype",
|
||||||
|
"seroval",
|
||||||
|
"seroval-plugins"
|
||||||
|
]
|
||||||
|
},
|
||||||
"source-map-js@1.2.1": {
|
"source-map-js@1.2.1": {
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
|
||||||
},
|
},
|
||||||
@@ -1649,6 +1718,9 @@
|
|||||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||||
"bin": true
|
"bin": true
|
||||||
},
|
},
|
||||||
|
"undici-types@6.21.0": {
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
|
||||||
|
},
|
||||||
"update-browserslist-db@1.1.3_browserslist@4.25.2": {
|
"update-browserslist-db@1.1.3_browserslist@4.25.2": {
|
||||||
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -1685,6 +1757,25 @@
|
|||||||
],
|
],
|
||||||
"bin": true
|
"bin": true
|
||||||
},
|
},
|
||||||
|
"vite@7.1.2_picomatch@4.0.3_@types+node@22.15.15": {
|
||||||
|
"integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"@types/node",
|
||||||
|
"esbuild",
|
||||||
|
"fdir",
|
||||||
|
"picomatch@4.0.3",
|
||||||
|
"postcss",
|
||||||
|
"rollup",
|
||||||
|
"tinyglobby"
|
||||||
|
],
|
||||||
|
"optionalDependencies": [
|
||||||
|
"fsevents"
|
||||||
|
],
|
||||||
|
"optionalPeers": [
|
||||||
|
"@types/node"
|
||||||
|
],
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
"webidl-conversions@7.0.0": {
|
"webidl-conversions@7.0.0": {
|
||||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
|
||||||
},
|
},
|
||||||
@@ -1753,6 +1844,7 @@
|
|||||||
"npm:@eslint/js@9",
|
"npm:@eslint/js@9",
|
||||||
"npm:@jsr/valkyr__event-emitter@1",
|
"npm:@jsr/valkyr__event-emitter@1",
|
||||||
"npm:@tanstack/react-query@5",
|
"npm:@tanstack/react-query@5",
|
||||||
|
"npm:@tanstack/react-router-devtools@1",
|
||||||
"npm:@tanstack/react-router@1",
|
"npm:@tanstack/react-router@1",
|
||||||
"npm:@types/react-dom@19",
|
"npm:@types/react-dom@19",
|
||||||
"npm:@types/react@19",
|
"npm:@types/react@19",
|
||||||
@@ -1767,7 +1859,8 @@
|
|||||||
"npm:react@19",
|
"npm:react@19",
|
||||||
"npm:typescript-eslint@8",
|
"npm:typescript-eslint@8",
|
||||||
"npm:typescript@5",
|
"npm:typescript@5",
|
||||||
"npm:vite@7"
|
"npm:vite@7",
|
||||||
|
"npm:zod@4"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ConflictError } from "@spec/relay/mod.ts";
|
import { ConflictError } from "@spec/relay";
|
||||||
|
|
||||||
export class AccountEmailClaimedError extends ConflictError {
|
export class AccountEmailClaimedError extends ConflictError {
|
||||||
constructor(email: string) {
|
constructor(email: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user