Skip to content

Commit

Permalink
Adds ability for admins to edit navigation toggles (#154)
Browse files Browse the repository at this point in the history
  • Loading branch information
mjanderson1227 authored Jan 9, 2025
1 parent 74498e7 commit f1e6b5c
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 11 deletions.
29 changes: 28 additions & 1 deletion apps/web/src/actions/admin/modify-nav-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import { revalidatePath } from "next/cache";

const metadataSchema = z.object({
name: z.string().min(1),
url: z.string(),
url: z.string().min(1),
});

const editMetadataSchema = metadataSchema.extend({
existingName: z.string().min(1),
enabled: z.boolean(),
});

// Maybe a better way to do this for revalidation? Who knows.
Expand All @@ -26,6 +31,28 @@ export const setItem = adminAction
return { success: true };
});

export const editItem = adminAction
.schema(editMetadataSchema)
.action(async ({ parsedInput: { name, url, existingName } }) => {
const pipe = kv.pipeline();

if (existingName != name) {
pipe.srem("config:navitemslist", encodeURIComponent(existingName));
}

pipe.sadd("config:navitemslist", encodeURIComponent(name));
pipe.hset(`config:navitems:${encodeURIComponent(name)}`, {
url,
name,
enabled: true,
});

await pipe.exec();

revalidatePath(navAdminPage);
return { success: true };
});

export const removeItem = adminAction
.schema(z.string())
.action(async ({ parsedInput: name, ctx: { user, userId } }) => {
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/admin/toggles/landing/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
NavItemsManager,
NavItemDialog,
AddNavItemDialog,
} from "@/components/admin/toggles/NavItemsManager";
import { getAllNavItems } from "@/lib/utils/server/redis";

Expand All @@ -13,7 +13,7 @@ export default async function Page() {
Navbar Items
</h2>
<div className="ml-auto">
<NavItemDialog />
<AddNavItemDialog />
</div>
</div>
<NavItemsManager
Expand Down
138 changes: 130 additions & 8 deletions apps/web/src/components/admin/toggles/NavItemsManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ import {
import { Button } from "@/components/shadcn/ui/button";
import { Input } from "@/components/shadcn/ui/input";
import { Label } from "@/components/shadcn/ui/label";
import { Plus } from "lucide-react";
import { Plus, Loader2 } from "lucide-react";
import { useState } from "react";
import { useAction, useOptimisticAction } from "next-safe-action/hooks";
import {
setItem,
removeItem,
toggleItem,
editItem,
} from "@/actions/admin/modify-nav-item";
import { toast } from "sonner";
import Link from "next/link";
Expand All @@ -41,8 +42,13 @@ interface NavItemsManagerProps {
export function NavItemsManager({ navItems }: NavItemsManagerProps) {
const { execute, result, status } = useAction(removeItem, {
onSuccess: () => {
toast.dismiss();
toast.success("NavItem deleted successfully!");
},
onError: () => {
toast.dismiss();
toast.error("Error deleting NavItem");
},
});

return (
Expand Down Expand Up @@ -79,12 +85,16 @@ export function NavItemsManager({ navItems }: NavItemsManagerProps) {
name={item.name}
/>
</TableCell>
<TableCell className="space-x-2 text-right">
<Button onClick={() => alert("Coming soon...")}>
Edit
</Button>
<TableCell className="space-x-2 space-y-2 text-right">
<EditNavItemDialog
existingName={item.name}
existingUrl={item.url}
existingEnabled={item.enabled}
/>
<Button
onClick={() => {
toast.dismiss();
toast.loading("Deleting NavItem...");
execute(item.name);
}}
>
Expand Down Expand Up @@ -113,6 +123,9 @@ function ToggleSwitch({
updateFn: (state, { statusToSet }) => {
return { itemStatus: statusToSet };
},
onError: () => {
toast.error("Error toggling NavItem");
},
});

return (
Expand All @@ -125,19 +138,28 @@ function ToggleSwitch({
);
}

export function NavItemDialog() {
export function AddNavItemDialog() {
const [name, setName] = useState<string | null>(null);
const [url, setUrl] = useState<string | null>(null);
const [open, setOpen] = useState(false);

const { execute, result, status } = useAction(setItem, {
const {
execute,
result,
status: createStatus,
} = useAction(setItem, {
onSuccess: () => {
console.log("Success");
setOpen(false);
toast.success("NavItem created successfully!");
},
onError: () => {
toast.error("Error creating NavItem");
},
});

const isLoading = createStatus === "executing";

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
Expand Down Expand Up @@ -183,10 +205,110 @@ export function NavItemDialog() {
console.log("Running Action");
if (!name || !url)
return alert("Please fill out all fields.");

execute({ name, url });
}}
>
Create
{isLoading && (
<Loader2
className={"absolute z-50 h-4 w-4 animate-spin"}
/>
)}
<p className={`${isLoading && "invisible"}`}>Create</p>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

interface EditNavItemDialogProps {
existingName: string;
existingUrl: string;
existingEnabled: boolean;
}

function EditNavItemDialog({
existingName,
existingUrl,
existingEnabled,
}: EditNavItemDialogProps) {
const [name, setName] = useState<string>(existingName);
const [url, setUrl] = useState<string>(existingUrl);
const [open, setOpen] = useState(false);

const { execute, status: editStatus } = useAction(editItem, {
onSuccess: () => {
console.log("Success");
setOpen(false);
toast.success("NavItem edited successfully!");
},
onError: () => {
toast.error("Error editing NavItem");
},
});
const isLoading = editStatus === "executing";

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Edit Item</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit Item</DialogTitle>
<DialogDescription>
Edit an existing item shown in the non-dashboard navbar
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input
onChange={(e) => setName(e.target.value)}
id="name"
placeholder="A Cool Hyperlink"
className="col-span-3"
value={name}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
URL
</Label>
<Input
onChange={(e) => setUrl(e.target.value)}
id="name"
placeholder="https://example.com/"
className="col-span-3"
value={url}
/>
</div>
</div>
<DialogFooter>
<Button
className="relative"
onClick={() => {
console.log("Running Action");
if (!name || !url)
return alert("Please fill out all fields.");

execute({
enabled: existingEnabled,
existingName,
name,
url,
});
}}
>
{isLoading && (
<Loader2
className={"absolute z-50 h-4 w-4 animate-spin"}
/>
)}
<p className={`${isLoading && "invisible"}`}>Update</p>
</Button>
</DialogFooter>
</DialogContent>
Expand Down

0 comments on commit f1e6b5c

Please sign in to comment.