Skip to content

Commit

Permalink
Feat: Better error handling and Live logs #56
Browse files Browse the repository at this point in the history
  • Loading branch information
ItsNik committed Feb 9, 2025
1 parent 7f858a0 commit 23a580c
Show file tree
Hide file tree
Showing 10 changed files with 600 additions and 403 deletions.
300 changes: 300 additions & 0 deletions app/components/customisation-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
import { useState, useEffect, useRef } from "react";
import { Card, CardContent, CardHeader } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Switch } from "~/components/ui/switch";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "~/components/ui/hover-card";
import { createFrontendHandler } from "~/lib/frontend";
import { Select, SelectTrigger, SelectItem, SelectValue, SelectContent } from "~/components/ui/select";
import { FeedbackModal } from "~/components/feedback-modal";
import { Check, Clipboard } from "lucide-react";
import type { FrontendConfig, Container } from "~/lib/typings/frontendConfig";

type FeedbackType = {
type: "success" | "error";
message: string;
};

export function ConfigForm({
containerNames,
initialConfig,
}: {
containerNames: string[];
initialConfig: FrontendConfig;
}) {
const [currentConfig, setCurrentConfig] = useState<FrontendConfig>(initialConfig);
const [selectedContainer, setSelectedContainer] = useState("");
const [hiddenState, setHiddenState] = useState(false);
const [pinnedState, setPinnedState] = useState(false);
const [tagsState, setTagsState] = useState("");
const [linkState, setLinkState] = useState("");
const [iconState, setIconState] = useState("");
const [useCustomIconState, setUseCustomIconState] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [feedback, setFeedback] = useState<FeedbackType | null>(null);
const [copied, setCopied] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
const [contentHeight, setContentHeight] = useState(0);
const handler = createFrontendHandler();
const current = currentConfig.find((c: Container) => c.name === selectedContainer);

useEffect(() => {
if (contentRef.current) {
setContentHeight(contentRef.current.scrollHeight);
}
}, [selectedContainer, currentConfig]);

useEffect(() => {
if (current) {
setHiddenState(current.hidden ?? false);
setPinnedState(current.pinned ?? false);
setTagsState((current.tags || []).join(", "));
setLinkState(current.link || "");
setIconState(current.icon || "");
setUseCustomIconState(false);
} else {
setHiddenState(false);
setPinnedState(false);
setTagsState("");
setLinkState("");
setIconState("");
setUseCustomIconState(false);
}
}, [selectedContainer, current]);

async function refreshConfig() {
const newConfig = await handler.getFrontendConfig();
setCurrentConfig(newConfig);
}

async function handleSubmit() {
if (!selectedContainer) return;
setIsSubmitting(true);
try {
const ops = [];
if (hiddenState !== (current?.hidden ?? false)) {
if (hiddenState) {
ops.push(handler.hide(selectedContainer));
} else {
ops.push(handler.show(selectedContainer));
}
}
if (pinnedState !== (current?.pinned ?? false)) {
if (pinnedState) {
ops.push(handler.pin(selectedContainer));
} else {
ops.push(handler.unpin(selectedContainer));
}
}
const currentTags = current?.tags || [];
const newTags = tagsState
.split(",")
.map((t) => t.trim())
.filter((t) => t.length > 0);
const tagsToAdd = newTags.filter((tag) => !currentTags.includes(tag));
const tagsToRemove = currentTags.filter((tag: string) => !newTags.includes(tag));
tagsToAdd.forEach((tag: string) => ops.push(handler.tag(selectedContainer, tag)));
tagsToRemove.forEach((tag: string) =>
ops.push(handler.removeTag(selectedContainer, tag)
));
const currentLink = current?.link || "";
if (linkState.trim() !== currentLink) {
if (linkState.trim() === "") {
ops.push(handler.removeLink(selectedContainer));
} else {
ops.push(handler.addLink(selectedContainer, linkState.trim()));
}
}
const currentIcon = current?.icon || "";
if (iconState.trim() !== currentIcon || useCustomIconState) {
if (iconState.trim() === "") {
ops.push(handler.removeIcon(selectedContainer));
} else {
ops.push(
handler.addIcon(
selectedContainer,
iconState.trim(),
useCustomIconState
)
);
}
}
await Promise.all(ops);
await refreshConfig();
setFeedback({
type: "success",
message: "Changes saved successfully!",
});
} catch (error: unknown) {
console.error("Error updating config:", error);
setFeedback({
type: "error",
message: (error as Error).message || "Error updating configuration",
});
} finally {
setIsSubmitting(false);
setTimeout(() => {
setFeedback(null);
}, 5000);
}
}

const handleCopy = async () => {
await navigator.clipboard.writeText(JSON.stringify(currentConfig, null, 2));
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};

return (
<div className="p-8 space-y-6">
<Card className="p-6">
<h1 className="text-2xl font-bold mb-4">Frontend Configuration</h1>
<div className="space-y-2">
<Label className="block text-xl font-medium">Container</Label>
<Select value={selectedContainer} onValueChange={setSelectedContainer}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select container" />
</SelectTrigger>
<SelectContent>
{containerNames.map((name: string) => (
<SelectItem key={name} value={name}>
{name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div
className={`overflow-hidden transition-all duration-300 ease-in-out`}
style={{
maxHeight: selectedContainer ? `${contentHeight + 32}px` : "0px",
opacity: selectedContainer ? 1 : 0,
marginTop: selectedContainer ? "1rem" : "0"
}}
>
<div ref={contentRef}>
{selectedContainer && (
<div className="space-y-4">
<Card className="p-4">
<CardHeader>
<h2 className="block text-xl font-medium">Container View Options</h2>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 gap-4">
<div className="flex items-center justify-between">
<Label>Hidden</Label>
<Switch
checked={hiddenState}
onCheckedChange={setHiddenState}
disabled={isSubmitting}
aria-label="Toggle Hidden"
/>
</div>
<div className="flex items-center justify-between">
<Label>Pinned</Label>
<Switch
checked={pinnedState}
onCheckedChange={setPinnedState}
disabled={isSubmitting}
aria-label="Toggle Pinned"
/>
</div>
</div>
</CardContent>
</Card>
<div className="space-y-4">
<Label className="block text-xl font-medium">Tags</Label>
<Input
value={tagsState}
onChange={(e) => setTagsState(e.target.value)}
placeholder="Enter tags separated by commas"
disabled={isSubmitting}
/>
<p className="text-sm text-muted-foreground">
Separate multiple tags with commas
</p>
</div>
<div className="space-y-4">
<Label className="block text-xl font-medium">Container Link</Label>
<Input
value={linkState}
onChange={(e) => setLinkState(e.target.value)}
placeholder="Enter URL for container"
disabled={isSubmitting}
/>
<p className="text-sm text-muted-foreground">
Leave blank to remove existing link
</p>
</div>
<div className="space-y-4">
<Label className="block text-xl font-medium">Icon Settings</Label>
<div className="flex gap-4 items-center">
<Input
value={iconState}
onChange={(e) => setIconState(e.target.value)}
placeholder="Icon name or URL"
className="flex-1"
disabled={isSubmitting}
/>
<div className="flex items-center gap-2 px-4 rounded-lg">
<Switch
checked={useCustomIconState}
onCheckedChange={setUseCustomIconState}
disabled={isSubmitting}
/>
<span className="text-sm text-muted-foreground">
<HoverCard>
<HoverCardTrigger asChild>
<Button variant="link" className="text-accent">Custom</Button>
</HoverCardTrigger>
<HoverCardContent className="w-80">
<div className="flex justify-between space-x-4">
<div className="space-y-1">
<h4 className="text-sm font-semibold">Custom Icons</h4>
<p className="text-sm">
Enable if using an icon uploaded to the custom icons folder
</p>
</div>
</div>
</HoverCardContent>
</HoverCard>
</span>
</div>
</div>
</div>
<Button onClick={handleSubmit} disabled={isSubmitting} className="w-full mt-4">
{isSubmitting ? "Submitting..." : "Submit Changes"}
</Button>
</div>
)}
</div>
</div>
</Card>
<Card className="p-6">
<h2 className="block text-xl font-medium mb-2">Current Configuration</h2>
<div className="relative">
<pre className="bg-muted/50 p-4 rounded-lg text-sm overflow-auto max-h-[400px]">
<code className="break-words whitespace-pre-wrap">
{JSON.stringify(currentConfig, null, 2)}
</code>
</pre>
<Button
onClick={handleCopy}
className="absolute top-1 right-1 p-2 rounded bg-background hover:bg-muted/50 transition-colors"
title="Copy to clipboard"
>
{copied ? (
<Check className="w-5 h-5 text-green-500 transition-all duration-300" />
) : (
<Clipboard className="w-5 h-5 text-foreground transition-all duration-300" />
)}
</Button>
</div>
</Card>
{feedback && (
<FeedbackModal feedback={feedback} onDismiss={() => setFeedback(null)} />
)}
</div>
);
}
60 changes: 60 additions & 0 deletions app/components/customisation-skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Card, CardContent, CardHeader } from "~/components/ui/card";
import { Skeleton } from "~/components/ui/skeleton";

export function ConfigPageSkeleton() {
return (
<div className="p-8 space-y-6">
<Card className="p-6">
<Skeleton className="h-8 w-48 mb-4" />
<div className="space-y-2">
<Skeleton className="h-5 w-32" />
<Skeleton className="h-10 w-full" />
</div>
<div className="space-y-4 mt-4">
<Card className="p-4">
<CardHeader>
<Skeleton className="h-6 w-48" />
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 gap-4">
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-6 w-12" />
</div>
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-6 w-12" />
</div>
</div>
</CardContent>
</Card>
<div className="space-y-4">
<Skeleton className="h-5 w-32" />
<Skeleton className="h-10 w-full" />
<Skeleton className="h-4 w-64" />
</div>
<div className="space-y-4">
<Skeleton className="h-5 w-48" />
<Skeleton className="h-10 w-full" />
<Skeleton className="h-4 w-64" />
</div>
<div className="space-y-4">
<Skeleton className="h-5 w-32" />
<div className="flex gap-4 items-center">
<Skeleton className="h-10 w-full" />
<Skeleton className="h-6 w-24" />
</div>
</div>
<Skeleton className="h-10 w-full mt-4" />
</div>
</Card>
<Card className="p-6">
<Skeleton className="h-6 w-64 mb-2" />
<div className="relative">
<Skeleton className="h-64 w-full" />
<Skeleton className="h-10 w-10 absolute top-1 right-1" />
</div>
</Card>
</div>
);
}
Loading

0 comments on commit 23a580c

Please sign in to comment.