@gigamusic/admin 0.1.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/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @gigamusic/admin
2
+
3
+ Source-shipped React admin for the gigamusic platform: releases CRUD,
4
+ settings, links, link-pages, and orders view — plus per-route handler
5
+ factories the consumer wires into `/api/admin/*`.
6
+
7
+ ```ts
8
+ // app/admin/page.tsx (consumer)
9
+ export { AdminHomePage as default } from "@gigamusic/admin";
10
+
11
+ // app/api/admin/auth/route.ts (consumer)
12
+ import { createAdminLoginHandler } from "@gigamusic/admin/server";
13
+ export const POST = createAdminLoginHandler({ adminPasswordHash, adminSessionSecret });
14
+ ```
15
+
16
+ Every page is auth-gated by the shared admin session cookie. Override a slot
17
+ via the standard `<GigamusicProvider components={{ Button: MyButton }}>` from
18
+ `@gigamusic/ui`, or replace a page wholesale by simply not mounting it.
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@gigamusic/admin",
3
+ "version": "0.1.0",
4
+ "description": "Admin UI (releases CRUD, tracks, settings, links, link-pages, orders) and admin API handler factories for the gigamusic platform. Fully replaceable by the consumer — mount the pages directly or override individual slots.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/gigamesh/gigamusic.git",
9
+ "directory": "packages/admin"
10
+ },
11
+ "type": "module",
12
+ "main": "./src/index.ts",
13
+ "types": "./src/index.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./src/index.ts",
17
+ "default": "./src/index.ts"
18
+ },
19
+ "./client": {
20
+ "types": "./src/client.ts",
21
+ "default": "./src/client.ts"
22
+ },
23
+ "./server": {
24
+ "types": "./src/server.ts",
25
+ "default": "./src/server.ts"
26
+ }
27
+ },
28
+ "files": [
29
+ "src",
30
+ "README.md"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "dependencies": {
36
+ "zod": "^3.24.1",
37
+ "@gigamusic/email": "0.1.0",
38
+ "@gigamusic/config": "0.1.0",
39
+ "@gigamusic/storage": "0.1.0",
40
+ "@gigamusic/core": "0.1.0",
41
+ "@gigamusic/audio": "0.1.0",
42
+ "@gigamusic/db": "0.1.0",
43
+ "@gigamusic/ui": "0.1.0"
44
+ },
45
+ "peerDependencies": {
46
+ "next": ">=15",
47
+ "react": ">=18 <20",
48
+ "react-dom": ">=18 <20"
49
+ },
50
+ "devDependencies": {
51
+ "@testing-library/dom": "^10.4.0",
52
+ "@testing-library/react": "^16.1.0",
53
+ "@types/node": "^22.10.5",
54
+ "@types/react": "^19.0.0",
55
+ "@types/react-dom": "^19.0.0",
56
+ "jsdom": "^25.0.1",
57
+ "next": "^16.0.0",
58
+ "react": "^19.0.0",
59
+ "react-dom": "^19.0.0",
60
+ "typescript": "^5.7.3",
61
+ "vitest": "^2.1.8"
62
+ },
63
+ "scripts": {
64
+ "lint": "eslint src",
65
+ "test": "vitest run",
66
+ "typecheck": "tsc --noEmit",
67
+ "clean": "rm -rf .turbo *.tsbuildinfo"
68
+ }
69
+ }
package/src/client.ts ADDED
@@ -0,0 +1,18 @@
1
+ "use client";
2
+
3
+ // Client-only re-exports — every consumer-side page component lives here so
4
+ // importing them from a server-component context never accidentally pulls in
5
+ // server-only modules.
6
+
7
+ export { AdminHomePage } from "./pages/AdminHomePage";
8
+ export { AdminLoginPage } from "./pages/AdminLoginPage";
9
+ export { AdminReleasesPage } from "./pages/AdminReleasesPage";
10
+ export { AdminNewReleasePage } from "./pages/AdminNewReleasePage";
11
+ export { AdminEditReleasePage } from "./pages/AdminEditReleasePage";
12
+ export { AdminLinksPage } from "./pages/AdminLinksPage";
13
+ export { AdminSettingsPage } from "./pages/AdminSettingsPage";
14
+ export { AdminOrdersPage } from "./pages/AdminOrdersPage";
15
+ export { ReleaseForm } from "./components/ReleaseForm";
16
+ export type { ReleaseFormProps } from "./components/ReleaseForm";
17
+ export { AdminLoginForm } from "./components/AdminLoginForm";
18
+ export { AdminNav } from "./components/AdminNav";
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter, useSearchParams } from "next/navigation";
5
+ import { useSlot } from "@gigamusic/ui/client";
6
+
7
+ /** Block off-site redirects via the `?next=` param. */
8
+ function safeNext(value: string | null): string | null {
9
+ if (!value) return null;
10
+ if (!value.startsWith("/")) return null;
11
+ if (value.startsWith("//") || value.startsWith("/\\")) return null;
12
+ return value;
13
+ }
14
+
15
+ export function AdminLoginForm() {
16
+ const Button = useSlot("Button");
17
+ const [password, setPassword] = useState("");
18
+ const [error, setError] = useState("");
19
+ const [loading, setLoading] = useState(false);
20
+ const router = useRouter();
21
+ const searchParams = useSearchParams();
22
+ const next = safeNext(searchParams.get("next"));
23
+
24
+ async function handleSubmit(e: React.FormEvent) {
25
+ e.preventDefault();
26
+ setLoading(true);
27
+ setError("");
28
+ const res = await fetch("/api/admin/auth", {
29
+ method: "POST",
30
+ headers: { "Content-Type": "application/json" },
31
+ body: JSON.stringify({ password }),
32
+ });
33
+ if (res.ok) {
34
+ if (next) router.push(next);
35
+ else router.refresh();
36
+ } else {
37
+ setError("Invalid password");
38
+ setLoading(false);
39
+ }
40
+ }
41
+
42
+ return (
43
+ <form onSubmit={handleSubmit} className="gm-admin-login space-y-4">
44
+ <div className="space-y-2">
45
+ <label htmlFor="password" className="text-sm font-medium">
46
+ Password
47
+ </label>
48
+ <input
49
+ id="password"
50
+ type="password"
51
+ value={password}
52
+ onChange={(e) => setPassword(e.target.value)}
53
+ required
54
+ className="block w-full rounded border border-border bg-background px-3 py-2 text-sm"
55
+ />
56
+ </div>
57
+ {error && <p className="text-sm text-destructive">{error}</p>}
58
+ <Button type="submit" className="w-full" disabled={loading}>
59
+ {loading ? "Logging in..." : "Log In"}
60
+ </Button>
61
+ </form>
62
+ );
63
+ }
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
5
+
6
+ interface NavLink {
7
+ href: string;
8
+ label: string;
9
+ exact?: boolean;
10
+ }
11
+
12
+ const DEFAULT_LINKS: NavLink[] = [
13
+ { href: "/admin", label: "Releases", exact: true },
14
+ { href: "/admin/orders", label: "Orders" },
15
+ { href: "/admin/links", label: "Links" },
16
+ { href: "/admin/settings", label: "Settings" },
17
+ ];
18
+
19
+ /**
20
+ * Horizontal admin nav. Consumers can extend the link set by overriding the
21
+ * `Navigation` slot if they need additional sections; the default set covers
22
+ * everything this package ships.
23
+ */
24
+ export function AdminNav({ extraLinks }: { extraLinks?: NavLink[] }) {
25
+ const pathname = usePathname();
26
+ const links = [...DEFAULT_LINKS, ...(extraLinks ?? [])];
27
+
28
+ return (
29
+ <nav className="gm-admin-nav flex items-center gap-4 overflow-x-auto border-b border-border pb-4 mb-6">
30
+ {links.map(({ href, label, exact }) => {
31
+ const active = exact
32
+ ? pathname === href
33
+ : pathname === href || pathname.startsWith(`${href}/`);
34
+ return (
35
+ <Link
36
+ key={href}
37
+ href={href}
38
+ className={
39
+ active
40
+ ? "text-sm font-semibold whitespace-nowrap"
41
+ : "text-sm text-muted-foreground hover:underline whitespace-nowrap"
42
+ }
43
+ >
44
+ {label}
45
+ </Link>
46
+ );
47
+ })}
48
+ </nav>
49
+ );
50
+ }