-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: Better error handling and Live logs #56
- Loading branch information
ItsNik
committed
Feb 9, 2025
1 parent
7f858a0
commit 23a580c
Showing
10 changed files
with
600 additions
and
403 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
Oops, something went wrong.