Dashboard Release 1.0

This commit is contained in:
1tzEmerald committed 2026-07-26 19:44:57 +01:00
1 parent 033fb87269
commit ed57dc2d4f
8 files changed
+143 -47

No files matched your search

+5
View File
@@ -584,6 +584,11 @@ export const api = {
{ method: "POST", body: JSON.stringify(body) },
),
resetDashboard: () =>
request<ApiResponse<unknown>>("/api/dashboard/setup/reset", {
method: "POST",
}),
completeSetup: (body: Record<string, unknown>) =>
request<
ApiResponse<{ setupComplete: boolean; botRestart?: BotRestartResult }>
+1 -1
View File
@@ -80,8 +80,8 @@ const sections: {
{ title: "Ticket Panel", url: "/configs?file=ticket-panel", icon: Ticket01Icon },
{ title: "AI Assistant", url: "/configs?file=supportbot-ai", icon: AiBrain01Icon },
{ title: "Messages", url: "/configs?file=messages", icon: Message01Icon },
{ title: "Commands", url: "/configs?file=commands", icon: CommandIcon },
{ title: "Staff Roles", url: "/configs?file=supportbot&tab=Roles", icon: UserMultiple02Icon },
{ title: "Commands", url: "/configs?file=commands", icon: CommandIcon },
],
},
{
@@ -6,6 +6,15 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useBranding } from "@/context/BrandingContext";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
function readFileAsDataUrl(file: File): Promise<string> {
return new Promise((resolve, reject) => {
@@ -20,22 +29,23 @@ interface DashboardBrandingSettingsProps {
canEdit: boolean;
}
export function DashboardBrandingDangerZone({ canEdit }: DashboardBrandingSettingsProps) {
const { setBrandingLocal } = useBranding();
export function DashboardDangerZone({ canEdit }: DashboardBrandingSettingsProps) {
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState("");
const [open, setOpen] = useState(false);
async function resetAll() {
async function resetDashboard() {
if (!canEdit) return;
setSaving(true);
setMessage("");
try {
const res = await api.resetBranding();
if (res.data) setBrandingLocal(res.data);
setMessage(res.message || "Reset to defaults.");
const res = await api.resetDashboard();
setMessage(res.message || "Dashboard reset successfully.");
setTimeout(() => {
window.location.href = "/setup";
}, 1500);
} catch (err) {
setMessage(err instanceof Error ? err.message : "Reset failed");
} finally {
setSaving(false);
}
}
@@ -45,17 +55,34 @@ export function DashboardBrandingDangerZone({ canEdit }: DashboardBrandingSettin
return (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Reset all branding text and favicon to their original defaults.
Reset the dashboard to factory settings. This clears the configuration and returns you to the initial setup screen.
</p>
<Button
type="button"
variant="destructive"
disabled={saving}
onClick={() => void resetAll()}
>
Reset Branding to Default
</Button>
{message && <p className="text-xs text-muted-foreground">{message}</p>}
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button type="button" variant="destructive">
Reset Dashboard
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will reset the dashboard to setup mode.
You will need to reconnect the Discord OAuth and reconfigure the dashboard owners.
</DialogDescription>
</DialogHeader>
{message && <p className="text-sm font-medium text-destructive">{message}</p>}
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)} disabled={saving}>
Cancel
</Button>
<Button variant="destructive" onClick={() => void resetDashboard()} disabled={saving}>
{saving ? "Resetting..." : "Yes, reset dashboard"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
@@ -126,13 +153,6 @@ export function DashboardBrandingSettings({ canEdit }: DashboardBrandingSettings
}
}
} finally {
setUploading(false);
}
}
}
}
const textDirty =
title.trim() !== branding.title || pageTitle.trim() !== branding.pageTitle;
@@ -253,8 +273,6 @@ export function DashboardBrandingSettings({ canEdit }: DashboardBrandingSettings
</div>
</div>
</div>
</div>
</div>
);
}
@@ -26,6 +26,7 @@ export const SETUP_STEP_IDS = [
"oauth",
"redirectUri",
"owners",
"emeraldApi",
] as const;
export type SetupStepId = (typeof SETUP_STEP_IDS)[number];
@@ -66,6 +67,12 @@ export const SETUP_STEPS: {
subtitle: "Who can access this panel",
icon: UserMultiple02Icon,
},
{
id: "emeraldApi",
title: "Emerald API",
subtitle: "One-click addons",
icon: Key01Icon,
},
];
function validationKeyForStep(id: SetupStepId): string {
@@ -636,3 +643,56 @@ export function SetupNav({
</div>
);
}
export function EmeraldApiKeyStep({
values,
setValues,
validation,
validating,
onValidate,
}: {
values: SetupFormValues;
setValues: (v: SetupFormValues) => void;
validation: Record<string, FieldValidationResult>;
validating: boolean;
onValidate: () => void;
}) {
return (
<div className="flex flex-1 flex-col gap-5">
<p className="text-sm leading-relaxed text-muted-foreground">
An Emerald Services API key is required to use the one-click addon installer in the dashboard. You can get yours from the{" "}
<a
href="https://emeraldsrv.dev"
target="_blank"
rel="noreferrer"
className="font-medium text-primary hover:underline"
>
Emerald Services Marketplace
</a>.
</p>
<div className="space-y-2">
<Label htmlFor="emeraldApiKey">Emerald API key</Label>
<Input
id="emeraldApiKey"
type="password"
autoComplete="off"
value={values.emeraldApiKey}
onChange={(e) => setValues({ ...values, emeraldApiKey: e.target.value })}
placeholder="em_..."
className="h-11 font-mono text-sm"
/>
</div>
<ValidationBanner result={validation.emeraldApi} />
<div className="mt-auto">
<Button
type="button"
variant="secondary"
disabled={validating || !values.emeraldApiKey}
onClick={onValidate}
>
{validating ? "Checking…" : "Save key"}
</Button>
</div>
</div>
);
}
+1
View File
@@ -35,4 +35,5 @@ export interface SetupFormValues {
clientSecret: string;
redirectUri: string;
ownerUserIds: string;
emeraldApiKey: string;
}
+5 -13
View File
@@ -83,24 +83,16 @@ export default function Addons() {
<div>
<h1 className="text-2xl font-bold tracking-tight">Addons</h1>
<p className="text-muted-foreground">
Install addons from the{" "}
Requires an API key from{" "}
<a
href={repository || "https://emeraldsrv.dev"}
href="https://emeraldsrv.dev"
target="_blank"
rel="noreferrer"
className="text-primary underline-offset-4 hover:underline"
>
Emerald Services Marketplace
</a>
. Enable addons under{" "}
<Link
to="/configs?file=supportbot"
className="text-primary underline-offset-4 hover:underline"
>
Bot config → General → Addons
</Link>
. Installed addon configs appear in the left sidebar under{" "}
<strong className="font-medium text-foreground">Addons</strong>.
emeraldsrv.dev
</a>{" "}
to browse and install addons. Configs will appear in the sidebar once installed.
</p>
</div>
<Button variant="outline" size="sm" onClick={load} disabled={loading}>
+18 -6
View File
@@ -27,7 +27,7 @@ import {
saveAccentTheme,
} from "@/lib/accent-theme";
import { normalizeHex } from "@/lib/normalize-hex";
import { DashboardBrandingSettings, DashboardBrandingDangerZone } from "@/components/settings/dashboard-branding-settings";
import { DashboardBrandingSettings, DashboardDangerZone } from "@/components/settings/dashboard-branding-settings";
import { DashboardApiSettings } from "@/components/settings/dashboard-api-settings";
export default function Settings() {
@@ -42,7 +42,7 @@ export default function Settings() {
() => getStoredAccentHex() ?? DEFAULT_ACCENT_HEX,
);
const [accentSaved, setAccentSaved] = useState("");
const [activeTab, setActiveTab] = useState<"appearance" | "api" | "updates">("appearance");
const [activeTab, setActiveTab] = useState<"appearance" | "api" | "updates" | "danger">("appearance");
const loadCheck = useCallback(async () => {
setLoading(true);
@@ -113,6 +113,15 @@ export default function Settings() {
>
Update Checker
</Button>
{canUpdate && (
<Button
variant="ghost"
className={`justify-start w-full text-destructive hover:bg-destructive/10 hover:text-destructive ${activeTab === "danger" ? "bg-destructive/10 border-l-2 border-destructive rounded-l-none" : ""}`}
onClick={() => setActiveTab("danger")}
>
Danger Zone
</Button>
)}
</nav>
</aside>
@@ -190,19 +199,22 @@ export default function Settings() {
</div>
</CardContent>
</Card>
</div>
)}
{canUpdate && (
{activeTab === "danger" && canUpdate && (
<Card className="border-destructive/50 bg-destructive/5">
<CardHeader>
<CardTitle className="text-lg text-destructive">Danger Zone</CardTitle>
<CardDescription className="text-destructive/80">
Destructive actions that reset or permanently alter your dashboard.
</CardDescription>
</CardHeader>
<CardContent>
<DashboardBrandingDangerZone canEdit={canUpdate} />
<DashboardDangerZone canEdit={canUpdate} />
</CardContent>
</Card>
)}
</div>
)}
{activeTab === "api" && (
<Card className="border-border bg-card">
+9 -1
View File
@@ -15,6 +15,7 @@ import {
SetupProgress,
SetupStepCard,
SetupWizardShell,
EmeraldApiKeyStep,
type SetupStepId,
} from "@/components/setup/setup-wizard";
import { useBranding } from "@/context/BrandingContext";
@@ -37,6 +38,7 @@ function parseOwnerIds(raw: string) {
function validationKeyForStep(id: SetupStepId): string {
if (id === "oauth") return "oauth";
if (id === "owners") return "owners";
if (id === "emeraldApi") return "emeraldApi";
return id;
}
@@ -53,6 +55,7 @@ export default function Setup() {
clientSecret: "",
redirectUri: "",
ownerUserIds: "",
emeraldApiKey: "",
});
const [validation, setValidation] = useState<Record<string, FieldValidationResult>>({});
const [validating, setValidating] = useState(false);
@@ -76,6 +79,7 @@ export default function Setup() {
clientSecret: values.clientSecret,
redirectUri: values.redirectUri,
ownerUserIds: parseOwnerIds(values.ownerUserIds),
emeraldApiKey: values.emeraldApiKey,
}),
[values],
);
@@ -163,6 +167,10 @@ export default function Setup() {
return (
<OwnersStep {...common} onValidate={() => void validateCurrentStep()} />
);
case "emeraldApi":
return (
<EmeraldApiKeyStep {...common} onValidate={() => void validateCurrentStep()} />
);
}
}
@@ -184,7 +192,7 @@ export default function Setup() {
<SetupWizardShell
brandingTitle={branding.title}
faviconUrl={branding.faviconUrl}
faviconVersion={branding.updatedAt}
faviconVersion={branding.updatedAt ?? undefined}
hasFavicon={branding.hasFavicon}
>
{statusError ? (