@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 +18 -0
- package/package.json +69 -0
- package/src/client.ts +18 -0
- package/src/components/AdminLoginForm.tsx +63 -0
- package/src/components/AdminNav.tsx +50 -0
- package/src/components/ReleaseForm.tsx +556 -0
- package/src/handlers/auth.ts +58 -0
- package/src/handlers/links.ts +117 -0
- package/src/handlers/orders.ts +21 -0
- package/src/handlers/releases.ts +209 -0
- package/src/handlers/settings.ts +54 -0
- package/src/handlers/upload.ts +286 -0
- package/src/index.ts +24 -0
- package/src/lib/rate-limit.ts +31 -0
- package/src/lib/responses.ts +19 -0
- package/src/lib/session.ts +89 -0
- package/src/lib/types.ts +45 -0
- package/src/pages/AdminEditReleasePage.tsx +39 -0
- package/src/pages/AdminHomePage.tsx +38 -0
- package/src/pages/AdminLayout.tsx +20 -0
- package/src/pages/AdminLinksPage.tsx +187 -0
- package/src/pages/AdminLoginPage.tsx +11 -0
- package/src/pages/AdminNewReleasePage.tsx +12 -0
- package/src/pages/AdminOrdersPage.tsx +127 -0
- package/src/pages/AdminReleasesPage.tsx +87 -0
- package/src/pages/AdminSettingsPage.tsx +84 -0
- package/src/server.ts +36 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useSlot } from "@gigamusic/ui/client";
|
|
6
|
+
|
|
7
|
+
interface ReleaseSummary {
|
|
8
|
+
id: number;
|
|
9
|
+
name: string;
|
|
10
|
+
slug: string;
|
|
11
|
+
price: number;
|
|
12
|
+
type: string;
|
|
13
|
+
isPublished: boolean;
|
|
14
|
+
tracks: Array<{ id: number }>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Lists every release with quick edit + delete. Fetches from `/api/admin/releases`. */
|
|
18
|
+
export function AdminReleasesPage() {
|
|
19
|
+
const Button = useSlot("Button");
|
|
20
|
+
const [releases, setReleases] = useState<ReleaseSummary[] | null>(null);
|
|
21
|
+
const [error, setError] = useState("");
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
let cancelled = false;
|
|
25
|
+
fetch("/api/admin/releases")
|
|
26
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error("Load failed"))))
|
|
27
|
+
.then((data) => {
|
|
28
|
+
if (!cancelled) setReleases(data as ReleaseSummary[]);
|
|
29
|
+
})
|
|
30
|
+
.catch((err) => !cancelled && setError(String(err)));
|
|
31
|
+
return () => {
|
|
32
|
+
cancelled = true;
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div>
|
|
38
|
+
<div className="mb-6 flex items-center justify-between">
|
|
39
|
+
<h1 className="text-2xl font-semibold">Releases</h1>
|
|
40
|
+
<Button href="/admin/releases/new">New Release</Button>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
44
|
+
|
|
45
|
+
<table className="w-full text-sm">
|
|
46
|
+
<thead>
|
|
47
|
+
<tr className="border-b border-border text-left">
|
|
48
|
+
<th className="py-2">Name</th>
|
|
49
|
+
<th className="py-2 text-right">Type</th>
|
|
50
|
+
<th className="py-2 text-right">Price</th>
|
|
51
|
+
<th className="py-2 text-right">Tracks</th>
|
|
52
|
+
<th className="py-2 text-right">Status</th>
|
|
53
|
+
</tr>
|
|
54
|
+
</thead>
|
|
55
|
+
<tbody>
|
|
56
|
+
{releases?.map((release) => (
|
|
57
|
+
<tr key={release.id} className="border-b border-border/40">
|
|
58
|
+
<td className="py-2">
|
|
59
|
+
<Link
|
|
60
|
+
href={`/admin/releases/${release.id}/edit`}
|
|
61
|
+
className="hover:underline"
|
|
62
|
+
>
|
|
63
|
+
{release.name}
|
|
64
|
+
</Link>
|
|
65
|
+
</td>
|
|
66
|
+
<td className="py-2 text-right">{release.type}</td>
|
|
67
|
+
<td className="py-2 text-right">
|
|
68
|
+
${(release.price / 100).toFixed(2)}
|
|
69
|
+
</td>
|
|
70
|
+
<td className="py-2 text-right">{release.tracks.length}</td>
|
|
71
|
+
<td className="py-2 text-right">
|
|
72
|
+
{release.isPublished ? "Published" : "Draft"}
|
|
73
|
+
</td>
|
|
74
|
+
</tr>
|
|
75
|
+
))}
|
|
76
|
+
{releases && releases.length === 0 && (
|
|
77
|
+
<tr>
|
|
78
|
+
<td colSpan={5} className="py-8 text-center text-muted-foreground">
|
|
79
|
+
No releases yet.
|
|
80
|
+
</td>
|
|
81
|
+
</tr>
|
|
82
|
+
)}
|
|
83
|
+
</tbody>
|
|
84
|
+
</table>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useSlot } from "@gigamusic/ui/client";
|
|
5
|
+
|
|
6
|
+
const DISCOUNT_KEY = "catalog_discount_percent";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Site-wide settings editor. Currently exposes the catalog-discount percent;
|
|
10
|
+
* any other consumer-specific tunables should get their own settings pages
|
|
11
|
+
* so each editor can render the right form for its value shape.
|
|
12
|
+
*/
|
|
13
|
+
export function AdminSettingsPage() {
|
|
14
|
+
const Button = useSlot("Button");
|
|
15
|
+
const [discount, setDiscount] = useState("");
|
|
16
|
+
const [loading, setLoading] = useState(true);
|
|
17
|
+
const [saving, setSaving] = useState(false);
|
|
18
|
+
const [saved, setSaved] = useState(false);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
let cancelled = false;
|
|
22
|
+
fetch(`/api/admin/settings?key=${encodeURIComponent(DISCOUNT_KEY)}`)
|
|
23
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error("Load failed"))))
|
|
24
|
+
.then((data: { value: unknown }) => {
|
|
25
|
+
if (cancelled) return;
|
|
26
|
+
const value =
|
|
27
|
+
typeof data.value === "number"
|
|
28
|
+
? String(data.value)
|
|
29
|
+
: typeof data.value === "string"
|
|
30
|
+
? data.value
|
|
31
|
+
: "15";
|
|
32
|
+
setDiscount(value);
|
|
33
|
+
setLoading(false);
|
|
34
|
+
})
|
|
35
|
+
.catch(() => !cancelled && setLoading(false));
|
|
36
|
+
return () => {
|
|
37
|
+
cancelled = true;
|
|
38
|
+
};
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
async function handleSave() {
|
|
42
|
+
setSaving(true);
|
|
43
|
+
setSaved(false);
|
|
44
|
+
await fetch("/api/admin/settings", {
|
|
45
|
+
method: "PUT",
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
body: JSON.stringify({ key: DISCOUNT_KEY, value: Number(discount) }),
|
|
48
|
+
});
|
|
49
|
+
setSaving(false);
|
|
50
|
+
setSaved(true);
|
|
51
|
+
setTimeout(() => setSaved(false), 3000);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (loading) return <p className="text-sm text-muted-foreground">Loading…</p>;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="max-w-md space-y-6">
|
|
58
|
+
<h1 className="text-2xl font-semibold">Settings</h1>
|
|
59
|
+
<div className="space-y-2">
|
|
60
|
+
<label htmlFor="discount" className="text-sm font-medium">
|
|
61
|
+
Catalog Discount (%)
|
|
62
|
+
</label>
|
|
63
|
+
<div className="flex items-center gap-3">
|
|
64
|
+
<input
|
|
65
|
+
id="discount"
|
|
66
|
+
type="number"
|
|
67
|
+
min="0"
|
|
68
|
+
max="100"
|
|
69
|
+
value={discount}
|
|
70
|
+
onChange={(e) => setDiscount(e.target.value)}
|
|
71
|
+
className="w-24 rounded border border-border bg-background px-3 py-2 text-sm"
|
|
72
|
+
/>
|
|
73
|
+
<Button onClick={handleSave} disabled={saving}>
|
|
74
|
+
{saving ? "Saving..." : "Save"}
|
|
75
|
+
</Button>
|
|
76
|
+
{saved && <span className="text-sm">Saved</span>}
|
|
77
|
+
</div>
|
|
78
|
+
<p className="text-xs text-muted-foreground">
|
|
79
|
+
Discount applied when customers buy the entire catalog.
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Server-side surface: every admin API route-handler factory + the session
|
|
2
|
+
// verification helper for the consumer's `proxy.ts`.
|
|
3
|
+
//
|
|
4
|
+
// The consumer wires each factory's return value into the matching route file:
|
|
5
|
+
// // app/api/admin/auth/route.ts
|
|
6
|
+
// export const POST = createAdminLoginHandler({ adminPasswordHash, adminSessionSecret });
|
|
7
|
+
//
|
|
8
|
+
// `verifyAdminSession(req, secret)` is the request-aware variant the
|
|
9
|
+
// consumer's `proxy.ts` calls before redirecting unauthenticated users.
|
|
10
|
+
|
|
11
|
+
export { createAdminLoginHandler, createAdminLogoutHandler } from "./handlers/auth";
|
|
12
|
+
export {
|
|
13
|
+
createAdminReleasesHandlers,
|
|
14
|
+
createAdminReleaseByIdHandlers,
|
|
15
|
+
} from "./handlers/releases";
|
|
16
|
+
export {
|
|
17
|
+
createAdminUploadPresignHandler,
|
|
18
|
+
createAdminUploadProcessHandler,
|
|
19
|
+
} from "./handlers/upload";
|
|
20
|
+
export { createAdminSettingsHandlers } from "./handlers/settings";
|
|
21
|
+
export { createAdminLinksHandlers } from "./handlers/links";
|
|
22
|
+
export { createAdminOrdersHandler } from "./handlers/orders";
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
verifyAdminSession,
|
|
26
|
+
isAdminAuthenticated,
|
|
27
|
+
writeAdminSessionCookie,
|
|
28
|
+
clearAdminSessionCookie,
|
|
29
|
+
ADMIN_SESSION_COOKIE,
|
|
30
|
+
} from "./lib/session";
|
|
31
|
+
|
|
32
|
+
export type {
|
|
33
|
+
AdminDeps,
|
|
34
|
+
AdminLogger,
|
|
35
|
+
RouteHandler,
|
|
36
|
+
} from "./lib/types";
|