Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds ability for admins to edit navigation toggles #154

Merged
merged 4 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading