@haruhimemoe/next-kit 0.7.0 → 0.8.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 +9 -1
- package/README.md +2 -2
- package/dist/auth-react/AccountMenu.d.ts +1 -1
- package/dist/auth-react/AccountMenu.js +7 -3
- package/dist/auth-react/DeleteAccountForm.d.ts +9 -8
- package/dist/auth-react/DeleteAccountForm.js +29 -29
- package/dist/auth-react/RestoreSignedIn.d.ts +1 -1
- package/dist/auth-react/RestoreSignedIn.js +3 -2
- package/dist/auth-react/SignInWithOsu.d.ts +1 -1
- package/dist/auth-react/SignInWithOsu.js +3 -3
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.8.0] - 2026-10-05
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **Breaking.** The `@haruhimemoe/ui` peer range is now `^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0` (0.15 to 0.17 not yet published): `DeleteAccountForm` needs ui's `ConfirmDialog`.
|
|
13
|
+
- `DeleteAccountForm` is a "Delete my account" button that opens a dialog: what goes is its description, the username is typed there, and a refusal or no answer is said in the dialog, which stays open. Once the account is deleted, focus moves to the "Your account is deleted." line.
|
|
14
|
+
- The account components use ui's text tones (`Text`, `textClasses`) and Button's own width. No change in how they look.
|
|
15
|
+
|
|
9
16
|
## [0.7.0] - 2026-10-05
|
|
10
17
|
|
|
11
18
|
### Added
|
|
@@ -89,7 +96,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
89
96
|
- `@haruhimemoe/next-kit/auth-react`: `createSignedInMarker`, `createAccountStore`, `useAccount`, `createAccount`, `RestoreSignedIn` and `osuSignIn`.
|
|
90
97
|
- `@haruhimemoe/next-kit/testing`: `startMemoryMongo`, `setupTestDb`, `setupMsw` and the fake osu! app env.
|
|
91
98
|
|
|
92
|
-
[unreleased]: https://github.com/haruhimemoe/next-kit/compare/v0.
|
|
99
|
+
[unreleased]: https://github.com/haruhimemoe/next-kit/compare/v0.8.0...HEAD
|
|
100
|
+
[0.8.0]: https://github.com/haruhimemoe/next-kit/compare/v0.7.0...v0.8.0
|
|
93
101
|
[0.6.2]: https://github.com/haruhimemoe/next-kit/compare/v0.6.1...v0.6.2
|
|
94
102
|
[0.6.1]: https://github.com/haruhimemoe/next-kit/compare/v0.6.0...v0.6.1
|
|
95
103
|
[0.6.0]: https://github.com/haruhimemoe/next-kit/compare/v0.5.0...v0.6.0
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ bun add @haruhimemoe/next-kit zod
|
|
|
32
32
|
| `env` | nothing else |
|
|
33
33
|
| `mongo` | `mongodb` ^7.6.0, `mongoose` ^9.10.2 |
|
|
34
34
|
| `auth` | `better-auth` ^1.7.5, `mongodb`, `@haruhimemoe/osu` 0.2 or 0.3 |
|
|
35
|
-
| `auth-react` | `react` ^19.3.0, `next` ^16.3.6, `@haruhimemoe/ui` ^0.
|
|
35
|
+
| `auth-react` | `react` ^19.3.0, `next` ^16.3.6, `@haruhimemoe/ui` ^0.14.0 \|\| ^0.15.0 \|\| ^0.16.0 \|\| ^0.17.0 (with its theme set up) |
|
|
36
36
|
| `seo` | `next` ^16.3.6 types only (nothing loads at runtime) |
|
|
37
37
|
| `testing` | `vitest` ^5.0.1, `msw` ^2.15.0, `mongodb-memory-server` ^11.3.0 |
|
|
38
38
|
| `api-keys` | `mongodb` ^7.6.0 |
|
|
@@ -304,7 +304,7 @@ The command prints one `pass` or `FAIL` line per standard, names each missing fi
|
|
|
304
304
|
| `SignInWithOsu({ next, signIn, signInPath?, label?, pendingLabel?, failedMessage? })` | Since 0.2.0. The large "Sign in with osu!" button. Calls `signIn(osuSignIn(next))`; an error (better-auth's `{ message }` or `{ error: { message } }`, or a throw) shows in a `role="alert"` line and the button comes back. `signInErrorMessage(error, fallback?)` reads it. |
|
|
305
305
|
| `SignOutButton({ signOut, onSignedOut, redirectTo?, variant?, className?, label?, pendingLabel? })` | Since 0.2.0. Signs out, runs `onSignedOut` only once the session is gone, then `router.replace(redirectTo ?? "/")` and `router.refresh()`. |
|
|
306
306
|
| `AccountMenu({ account, items, signOut, onSignedOut, avatarSrc?, signInPath?, signInLabel?, signOutLabel?, signOutRedirect? })` | Since 0.2.0. A sized blank while loading, a "Sign in" link back to this page when signed out, else ui's `HeaderMenu` with the avatar and name, `items` and Sign out. The avatar is a plain `<img>` (your CSP's img-src must allow a.ppy.sh and osu.ppy.sh); `avatarSrc` defaults to `osuAvatarSrc`. |
|
|
307
|
-
| `DeleteAccountForm({ username, appName, deletes, onDeleted, endpoint?, homeHref?, homeLabel?, fetcher? })` | Since 0.2.0. ui's `
|
|
307
|
+
| `DeleteAccountForm({ username, appName, deletes, onDeleted, endpoint?, homeHref?, homeLabel?, fetcher? })` | Since 0.2.0. A "Delete my account" button (0.8.0; an in-page `TypeToConfirm` before) that opens ui's `ConfirmDialog`: `deletes` is its description, the username must be typed, then "Delete for good" sends `DELETE endpoint` (default `/api/account`) with `{ username }`. 204: `onDeleted`, "Your account is deleted." (focused) and home. Another 2xx: `onDeleted` and the answer's `notice` after that line, staying. A refusal shows `error.message` (else "Deleting failed (status).") in the dialog; no answer, "Couldn't reach <appName>. Your account is still there." The dialog stays open on both. Needs ui 0.14.0. |
|
|
308
308
|
| `osuAvatarSrc(url)`, `OSU_AVATAR_HOSTS` | Since 0.2.0. An osu! avatar URL on https when it's on a.ppy.sh or osu.ppy.sh (a bare path is osu.ppy.sh's), else null. |
|
|
309
309
|
|
|
310
310
|
### seo
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* (src/components/layout/AccountMenu.tsx), which differed only in the links.
|
|
10
10
|
* @author David @dvhsh (https://dvh.sh)
|
|
11
11
|
* @created Mon Sep 28, 2026
|
|
12
|
-
* @modified Mon
|
|
12
|
+
* @modified Mon Oct 5, 2026
|
|
13
13
|
*/
|
|
14
14
|
import { type HeaderMenuItem } from "@haruhimemoe/ui";
|
|
15
15
|
import type { ReactNode } from "react";
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
* (src/components/layout/AccountMenu.tsx), which differed only in the links.
|
|
10
10
|
* @author David @dvhsh (https://dvh.sh)
|
|
11
11
|
* @created Mon Sep 28, 2026
|
|
12
|
-
* @modified Mon
|
|
12
|
+
* @modified Mon Oct 5, 2026
|
|
13
13
|
*/
|
|
14
14
|
"use client";
|
|
15
15
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
16
|
-
import { HeaderMenu } from "@haruhimemoe/ui";
|
|
16
|
+
import { HeaderMenu, textClasses } from "@haruhimemoe/ui";
|
|
17
17
|
import { usePathname } from "next/navigation.js";
|
|
18
18
|
import { DEFAULT_SIGN_IN_PATH, signInHref } from "../server/safe-next.js";
|
|
19
19
|
import { osuAvatarSrc } from "./avatar.js";
|
|
@@ -29,7 +29,11 @@ export function AccountMenu({ account, items, signOut, onSignedOut, avatarSrc =
|
|
|
29
29
|
if (account.status === "loading")
|
|
30
30
|
return _jsx("span", { "aria-hidden": "true", className: "block h-7 w-16" });
|
|
31
31
|
if (account.status === "signed-out") {
|
|
32
|
-
return (_jsx("a", { href: signInHref(pathname || "/", signInPath), className:
|
|
32
|
+
return (_jsx("a", { href: signInHref(pathname || "/", signInPath), className: textClasses({
|
|
33
|
+
tone: "muted",
|
|
34
|
+
bold: true,
|
|
35
|
+
className: "transition-colors hover:text-c1",
|
|
36
|
+
}), children: signInLabel }));
|
|
33
37
|
}
|
|
34
38
|
const avatar = avatarSrc(account.user.avatarUrl);
|
|
35
39
|
return (_jsx(HeaderMenu, { label: _jsxs(_Fragment, { children: [avatar ? (_jsx("img", { src: avatar, alt: "", width: 28, height: 28, className: "rounded-full" })) : null, _jsx("span", { children: account.user.username })] }), items: items, children: _jsx(SignOutButton, { signOut: signOut, onSignedOut: onSignedOut, redirectTo: signOutRedirect, label: signOutLabel, variant: "ghost", className: "h-auto justify-start rounded px-3 py-2" }) }));
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file src/auth-react/DeleteAccountForm.tsx
|
|
3
|
-
* @desc "Delete my account"
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* couldn't be reached
|
|
3
|
+
* @desc "Delete my account": a button that opens ui's ConfirmDialog (no confirm()), where what
|
|
4
|
+
* goes is the description and the osu! username is typed before "Delete for good" sends one
|
|
5
|
+
* DELETE with it to the app's endpoint. 204 means gone: signed out, the page says so and
|
|
6
|
+
* goes home. Another 2xx is gone too but stays, to show the answer's `notice` (work still
|
|
7
|
+
* waiting elsewhere). A refusal says the server's message in the dialog, no answer says the
|
|
8
|
+
* app couldn't be reached, nothing was deleted and the dialog stays open. Once it's gone
|
|
9
|
+
* the button never comes back and focus lands on the line that says so.
|
|
9
10
|
* Moved from pools and bb (src/components/account/DeleteAccountForm.tsx).
|
|
10
11
|
* @author David @dvhsh (https://dvh.sh)
|
|
11
12
|
* @created Mon Sep 28, 2026
|
|
12
|
-
* @modified Mon
|
|
13
|
+
* @modified Mon Oct 5, 2026
|
|
13
14
|
*/
|
|
14
15
|
import { type ReactNode } from "react";
|
|
15
16
|
/** DeleteAccountForm's props. */
|
|
@@ -35,6 +36,6 @@ export type DeleteAccountFormProps = {
|
|
|
35
36
|
* @function DeleteAccountForm
|
|
36
37
|
* @param props {DeleteAccountFormProps} the username, the app's name, what goes, the endpoint
|
|
37
38
|
* and what to do after
|
|
38
|
-
* @returns {ReactNode} the
|
|
39
|
+
* @returns {ReactNode} the "Delete my account" trigger and its confirm dialog, or what happened
|
|
39
40
|
*/
|
|
40
41
|
export declare function DeleteAccountForm({ username, appName, deletes, onDeleted, endpoint, homeHref, homeLabel, fetcher, }: DeleteAccountFormProps): ReactNode;
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file src/auth-react/DeleteAccountForm.tsx
|
|
3
|
-
* @desc "Delete my account"
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* couldn't be reached
|
|
3
|
+
* @desc "Delete my account": a button that opens ui's ConfirmDialog (no confirm()), where what
|
|
4
|
+
* goes is the description and the osu! username is typed before "Delete for good" sends one
|
|
5
|
+
* DELETE with it to the app's endpoint. 204 means gone: signed out, the page says so and
|
|
6
|
+
* goes home. Another 2xx is gone too but stays, to show the answer's `notice` (work still
|
|
7
|
+
* waiting elsewhere). A refusal says the server's message in the dialog, no answer says the
|
|
8
|
+
* app couldn't be reached, nothing was deleted and the dialog stays open. Once it's gone
|
|
9
|
+
* the button never comes back and focus lands on the line that says so.
|
|
9
10
|
* Moved from pools and bb (src/components/account/DeleteAccountForm.tsx).
|
|
10
11
|
* @author David @dvhsh (https://dvh.sh)
|
|
11
12
|
* @created Mon Sep 28, 2026
|
|
12
|
-
* @modified Mon
|
|
13
|
+
* @modified Mon Oct 5, 2026
|
|
13
14
|
*/
|
|
14
15
|
"use client";
|
|
15
16
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
16
|
-
import { buttonClasses,
|
|
17
|
+
import { buttonClasses, ConfirmDialog } from "@haruhimemoe/ui";
|
|
17
18
|
import { useRouter } from "next/navigation.js";
|
|
18
19
|
import { useId, useState } from "react";
|
|
19
20
|
const messageOf = async (response) => {
|
|
@@ -30,41 +31,40 @@ const messageOf = async (response) => {
|
|
|
30
31
|
* @function DeleteAccountForm
|
|
31
32
|
* @param props {DeleteAccountFormProps} the username, the app's name, what goes, the endpoint
|
|
32
33
|
* and what to do after
|
|
33
|
-
* @returns {ReactNode} the
|
|
34
|
+
* @returns {ReactNode} the "Delete my account" trigger and its confirm dialog, or what happened
|
|
34
35
|
*/
|
|
35
36
|
export function DeleteAccountForm({ username, appName, deletes, onDeleted, endpoint = "/api/account", homeHref = "/", homeLabel = "Go to the home page", fetcher = fetch, }) {
|
|
36
37
|
const router = useRouter();
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
// Once it's gone: what more to say. The form doesn't come back, so no second delete.
|
|
38
|
+
const doneId = `${useId()}-done`;
|
|
39
|
+
// Once it's gone: what more to say. The button doesn't come back, so no second delete.
|
|
40
40
|
const [done, setDone] = useState(null);
|
|
41
|
+
const unreachable = `Couldn't reach ${appName}. Your account is still there.`;
|
|
42
|
+
// Throws to keep the dialog open with what went wrong.
|
|
41
43
|
const remove = async () => {
|
|
42
|
-
|
|
44
|
+
let response;
|
|
43
45
|
try {
|
|
44
|
-
|
|
46
|
+
response = await fetcher(endpoint, {
|
|
45
47
|
method: "DELETE",
|
|
46
48
|
headers: { "Content-Type": "application/json" },
|
|
47
49
|
body: JSON.stringify({ username }),
|
|
48
50
|
});
|
|
49
|
-
if (!response.ok) {
|
|
50
|
-
setError(await messageOf(response));
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
onDeleted();
|
|
54
|
-
if (response.status === 204) {
|
|
55
|
-
setDone("");
|
|
56
|
-
router.push(homeHref);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
const body = (await response.json().catch(() => ({})));
|
|
60
|
-
setDone(typeof body.notice === "string" ? body.notice : "");
|
|
61
51
|
}
|
|
62
52
|
catch {
|
|
63
|
-
|
|
53
|
+
throw new Error(unreachable);
|
|
64
54
|
}
|
|
55
|
+
if (!response.ok)
|
|
56
|
+
throw new Error(await messageOf(response));
|
|
57
|
+
onDeleted();
|
|
58
|
+
if (response.status === 204) {
|
|
59
|
+
setDone("");
|
|
60
|
+
router.push(homeHref);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const body = (await response.json().catch(() => ({})));
|
|
64
|
+
setDone(typeof body.notice === "string" ? body.notice : "");
|
|
65
65
|
};
|
|
66
66
|
if (done !== null) {
|
|
67
|
-
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { role: "status", className: "text-c2 text-sm", children: `Your account is deleted. ${done}`.trim() }), _jsx("a", { href: homeHref, className: buttonClasses({ variant: "secondary"
|
|
67
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { id: doneId, role: "status", tabIndex: -1, className: "text-c2 text-sm outline-none", children: `Your account is deleted. ${done}`.trim() }), _jsx("a", { href: homeHref, className: buttonClasses({ variant: "secondary" }), children: homeLabel })] }));
|
|
68
68
|
}
|
|
69
|
-
return (_jsx(
|
|
69
|
+
return (_jsx(ConfirmDialog, { trigger: "Delete my account", triggerProps: { variant: "danger" }, title: "Delete your account?", description: deletes, tone: "destructive", typeToConfirm: username, confirmLabel: "Delete for good", pendingLabel: "Deleting\u2026", failedMessage: (error) => (error instanceof Error ? error.message : unreachable), returnFocus: () => document.getElementById(doneId), onConfirm: remove }));
|
|
70
70
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* and pools (src/components/auth/RestoreSignedIn.tsx, body-identical).
|
|
8
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
9
9
|
* @created Mon Sep 28, 2026
|
|
10
|
-
* @modified Mon
|
|
10
|
+
* @modified Mon Oct 5, 2026
|
|
11
11
|
*/
|
|
12
12
|
import { type ReactNode } from "react";
|
|
13
13
|
/** RestoreSignedIn's props. */
|
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
* and pools (src/components/auth/RestoreSignedIn.tsx, body-identical).
|
|
8
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
9
9
|
* @created Mon Sep 28, 2026
|
|
10
|
-
* @modified Mon
|
|
10
|
+
* @modified Mon Oct 5, 2026
|
|
11
11
|
*/
|
|
12
12
|
"use client";
|
|
13
13
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
14
|
+
import { Text } from "@haruhimemoe/ui";
|
|
14
15
|
import { useRouter } from "next/navigation.js";
|
|
15
16
|
import { useEffect } from "react";
|
|
16
17
|
const readDocumentCookie = () => document.cookie;
|
|
17
|
-
const SIGNING_IN = _jsx(
|
|
18
|
+
const SIGNING_IN = _jsx(Text, { tone: "muted", children: "Signing you in\u2026" });
|
|
18
19
|
/**
|
|
19
20
|
* @function RestoreSignedIn
|
|
20
21
|
* @param props {RestoreSignedInProps} the store, the marker check, `next` and what to show
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* (src/components/auth/SignInWithOsu.tsx), where only the error reading differed.
|
|
8
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
9
9
|
* @created Mon Sep 28, 2026
|
|
10
|
-
* @modified Mon
|
|
10
|
+
* @modified Mon Oct 5, 2026
|
|
11
11
|
*/
|
|
12
12
|
import { type ReactNode } from "react";
|
|
13
13
|
import { type OsuSignIn } from "./sign-in.js";
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* (src/components/auth/SignInWithOsu.tsx), where only the error reading differed.
|
|
8
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
9
9
|
* @created Mon Sep 28, 2026
|
|
10
|
-
* @modified Mon
|
|
10
|
+
* @modified Mon Oct 5, 2026
|
|
11
11
|
*/
|
|
12
12
|
"use client";
|
|
13
13
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
|
-
import { Button } from "@haruhimemoe/ui";
|
|
14
|
+
import { Button, Text } from "@haruhimemoe/ui";
|
|
15
15
|
import { useState } from "react";
|
|
16
16
|
import { osuSignIn } from "./sign-in.js";
|
|
17
17
|
const FAILED = "Couldn't start osu! sign-in. Try again.";
|
|
@@ -54,5 +54,5 @@ export function SignInWithOsu({ next, signIn, signInPath, label = "Sign in with
|
|
|
54
54
|
}
|
|
55
55
|
setPending(false);
|
|
56
56
|
};
|
|
57
|
-
return (_jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx(Button, { size: "lg", onClick: onClick, disabled: pending, children: pending ? pendingLabel : label }), error ? (_jsx(
|
|
57
|
+
return (_jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx(Button, { size: "lg", onClick: onClick, disabled: pending, children: pending ? pendingLabel : label }), error ? (_jsx(Text, { role: "alert", tone: "error", bold: true, children: error })) : null] }));
|
|
58
58
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haruhimemoe/next-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "The Next.js plumbing the haruhime.moe tools share: JSON route helpers, rate limits and budgets in MongoDB, bearer machine auth, zod env parsing, a connect-once MongoDB client with safe index builds, better-auth with osu! sign-in, the signed-in marker and account store for the browser, SEO: metadata, robots.txt, sitemaps, JSON-LD and llms.txt, and per-app API keys and the /api/v1 guard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nextjs",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
},
|
|
105
105
|
"peerDependencies": {
|
|
106
106
|
"@haruhimemoe/osu": "^0.2.0 || ^0.3.0 || ^0.4.0",
|
|
107
|
-
"@haruhimemoe/ui": "^0.
|
|
107
|
+
"@haruhimemoe/ui": "^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0",
|
|
108
108
|
"@haruhimemoe/vcs": "^0.1.0",
|
|
109
109
|
"better-auth": "^1.7.5",
|
|
110
110
|
"mongodb": "^7.6.0",
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
"devDependencies": {
|
|
155
155
|
"@biomejs/biome": "2.5.14",
|
|
156
156
|
"@haruhimemoe/osu": "0.2.0",
|
|
157
|
-
"@haruhimemoe/ui": "0.
|
|
157
|
+
"@haruhimemoe/ui": "0.14.0",
|
|
158
158
|
"@haruhimemoe/vcs": "0.1.0",
|
|
159
159
|
"@testing-library/dom": "10.4.2",
|
|
160
160
|
"@testing-library/react": "16.3.3",
|