Skip to content

Commit d419fbe

Browse files
committed
- you can now update or delete your own configs
- on the development mode you can also delete or update any config
1 parent f4a141d commit d419fbe

File tree

7 files changed

+394
-7
lines changed

7 files changed

+394
-7
lines changed

components/Sections/configs/ConfigCard.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,24 @@
33
import {
44
Card,
55
CardDescription,
6-
CardHeader,
6+
CardContent,
77
CardTitle,
88
} from '@/components/ui/card';
99
import { Badge } from '@/components/ui/badge';
1010
import { Avatar, AvatarImage, AvatarFallback } from '@radix-ui/react-avatar';
1111
import { JsonValue } from '@prisma/client/runtime/library';
12+
import ConfigCardControls from "./ConfigCardControls.tsx";
1213

13-
type Config = {
14-
id?: number | string;
14+
export type Config = {
15+
id: number;
1516
title: string;
1617
description: string;
1718
user: any;
1819
categories: string[];
1920
config: JsonValue;
2021
};
2122

22-
export default function ConfigCard({ config }: { config: Config }) {
23+
export default async function ConfigCard({ config }: { config: Config }) {
2324
const handleDownload = () => {
2425
const blob = new Blob([JSON.stringify(config.config, null, 2)], {
2526
type: 'application/json',
@@ -40,9 +41,8 @@ export default function ConfigCard({ config }: { config: Config }) {
4041
<div>
4142
<Card
4243
className="cursor-pointer hover:bg-slate-100/60 transition-colors flex flex-col"
43-
onClick={handleDownload}
4444
>
45-
<CardHeader className="flex-grow space-y-2 p-4">
45+
<CardContent className="flex-grow space-y-2 p-4">
4646
<CardTitle className="text-lg">{config.title}</CardTitle>
4747
<div className="flex flex-wrap gap-2 -ml-1">
4848
{config.categories.map((category, index) => (
@@ -78,7 +78,8 @@ export default function ConfigCard({ config }: { config: Config }) {
7878
</>
7979
)}
8080
</div>
81-
</CardHeader>
81+
</CardContent>
82+
<ConfigCardControls downloadAction={handleDownload} config={config} />
8283
</Card>
8384
</div>
8485
);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { CardFooter } from "@/components/ui/card";
2+
import { useSession } from "next-auth/react"
3+
import UpdateAction from "./UpdateActionButton";
4+
import { Config } from "./ConfigCard";
5+
import DownloadButton from "./DownloadButton";
6+
7+
export default function ConfigCardControls({ config, downloadAction }: { config: Config, downloadAction: () => void }) {
8+
const { data: session } = useSession()
9+
10+
const userId = config?.user?.id.toString();
11+
const sessionUserId = (session?.user?.image ?? "").match(/avatars\/(\d+)\//)?.[1] ?? "0";
12+
13+
return (
14+
<CardFooter className="p-0">
15+
<DownloadButton downloadAction={downloadAction} />
16+
{
17+
(process.env.NODE_ENV === "development" || userId === sessionUserId) && (
18+
<UpdateAction config={config} />
19+
)
20+
}
21+
</CardFooter>
22+
)
23+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Button } from '@/components/ui/button';
2+
import { deleteConfig } from './DeleteServerAction';
3+
4+
const DeleteAction = ({ configId }: { configId: number }) => {
5+
return (
6+
<>
7+
<Button
8+
variant="destructive"
9+
onClick={() => deleteConfig(configId)}
10+
className='w-full m-0 rounded-tl-none rounded-br-none rounded-tr-none hover:opacity-80 transition-all duration-100'
11+
>
12+
Delete
13+
</Button>
14+
</>
15+
)
16+
}
17+
18+
export default DeleteAction;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use server';
2+
3+
import prisma from '@/lib/db';
4+
import { revalidatePath } from 'next/cache';
5+
6+
type ConfigID = number;
7+
8+
export async function deleteConfig(configId: ConfigID) {
9+
if (!configId) {
10+
throw new Error('Config ID is required');
11+
}
12+
13+
try {
14+
await prisma.config.delete({
15+
where: {
16+
id: configId,
17+
},
18+
});
19+
20+
revalidatePath('/configs');
21+
22+
return true;
23+
} catch (error) {
24+
console.error('Error deleting config:', error);
25+
return false;
26+
}
27+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use client'
2+
3+
import { Button } from "@/components/ui/button"
4+
5+
export default function DownloadButton({ downloadAction }: { downloadAction: () => void }) {
6+
return (
7+
<Button
8+
onClick={downloadAction}
9+
className='w-full m-0 rounded-tl-none rounded-tr-none hover:opacity-80 transition-all duration-100 bg-[#2b65ca] text-white hover:bg-[#2359b6] rounded-bl-none rounded-br-none'
10+
>
11+
Download
12+
</Button>
13+
)
14+
}
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
'use client'
2+
3+
import { useState, useEffect } from 'react';
4+
import { Button } from '@/components/ui/button';
5+
import { Input } from '@/components/ui/input';
6+
import { Label } from '@/components/ui/label';
7+
import { useToast } from '@/components/ui/use-toast';
8+
import {
9+
Dialog,
10+
DialogContent,
11+
DialogDescription,
12+
DialogHeader,
13+
DialogTitle,
14+
DialogTrigger,
15+
} from '@/components/ui/dialog';
16+
import {
17+
Drawer,
18+
DrawerClose,
19+
DrawerContent,
20+
DrawerDescription,
21+
DrawerFooter,
22+
DrawerHeader,
23+
DrawerTitle,
24+
DrawerTrigger,
25+
} from '@/components/ui/drawer';
26+
import { useSession } from 'next-auth/react';
27+
import { updateConfig } from './UpdateConfigAction';
28+
import { deleteConfig } from './DeleteServerAction';
29+
import { type Config } from "./ConfigCard";
30+
31+
const UpdateAction = ({ config }: { config: Config }) => {
32+
const { toast } = useToast();
33+
const { data: session } = useSession();
34+
const [open, setOpen] = useState(false);
35+
const [isDesktop, setIsDesktop] = useState(true);
36+
const [formData, setFormData] = useState({
37+
title: config.title,
38+
description: config.description,
39+
categories: config.categories,
40+
userId: config.user.id,
41+
config: config.config,
42+
});
43+
44+
useEffect(() => {
45+
const desktopQuery = window.matchMedia('(min-width: 768px)');
46+
setIsDesktop(desktopQuery.matches);
47+
48+
const handleMediaChange = (e: MediaQueryListEvent) => {
49+
setIsDesktop(e.matches);
50+
};
51+
52+
desktopQuery.addEventListener('change', handleMediaChange);
53+
54+
return () => {
55+
desktopQuery.removeEventListener('change', handleMediaChange);
56+
};
57+
}, []);
58+
59+
const handleFormChange = (e: any) => {
60+
const { name, value } = e.target;
61+
setFormData({ ...formData, [name]: value });
62+
};
63+
64+
const handleSubmit = async (e: React.FormEvent) => {
65+
e.preventDefault();
66+
67+
const trimmedTitle = formData.title.trim();
68+
const trimmedDescription = formData.description.trim();
69+
const trimmedCategories = formData.categories
70+
.map((cat: string) => cat.trim())
71+
.filter((cat: string) => cat !== '');
72+
73+
if (!trimmedTitle || !trimmedDescription) {
74+
toast({
75+
title: 'Required Fields Missing',
76+
description: 'Title and Description cannot be empty.',
77+
variant: 'destructive',
78+
});
79+
return;
80+
}
81+
82+
const updatedData = {
83+
...formData,
84+
title: trimmedTitle,
85+
description: trimmedDescription,
86+
categories: trimmedCategories,
87+
};
88+
89+
try {
90+
const success = await updateConfig(config.id, updatedData);
91+
if (success) {
92+
toast({
93+
title: 'Successfully updated config!',
94+
description: 'The config has been updated successfully.',
95+
variant: 'passive',
96+
});
97+
} else {
98+
toast({
99+
title: 'Failed to update config',
100+
description: 'There was an error while updating your config. Please try again!',
101+
variant: 'destructive',
102+
});
103+
}
104+
} catch (error) {
105+
toast({
106+
title: 'Error',
107+
description: 'An error occurred while updating the config.',
108+
variant: 'destructive',
109+
});
110+
} finally {
111+
setOpen(false);
112+
}
113+
};
114+
115+
const renderForm = () => (
116+
<form className="grid items-start gap-4 max-md:px-4" onSubmit={handleSubmit}>
117+
<div className="grid gap-2">
118+
<Label htmlFor="title">
119+
Title <span className="text-red-500">*</span>
120+
</Label>
121+
<Input
122+
id="title"
123+
name="title"
124+
value={formData.title}
125+
onChange={handleFormChange}
126+
required
127+
/>
128+
</div>
129+
<div className="grid gap-2">
130+
<Label htmlFor="description">
131+
Description <span className="text-red-500">*</span>
132+
</Label>
133+
<Input
134+
id="description"
135+
name="description"
136+
value={formData.description}
137+
onChange={handleFormChange}
138+
required
139+
/>
140+
</div>
141+
<div className="grid gap-2">
142+
<Label htmlFor="categories">Categories (comma separated)</Label>
143+
<Input
144+
id="categories"
145+
name="categories"
146+
value={formData.categories}
147+
onChange={(e) =>
148+
setFormData({
149+
...formData,
150+
categories: e.target.value
151+
.split(',')
152+
.map((cat: string) => cat.trim())
153+
})
154+
}
155+
/>
156+
</div>
157+
<Button type="submit">
158+
Submit
159+
</Button>
160+
<Button
161+
variant="destructive"
162+
onClick={async (e) => {
163+
e.stopPropagation();
164+
e.preventDefault();
165+
try {
166+
setOpen(false);
167+
const success = await deleteConfig(config.id);
168+
if (success) {
169+
toast({
170+
title: 'Successfully deleted config!',
171+
description: 'The config has been deleted successfully.',
172+
variant: 'passive',
173+
});
174+
setOpen(false);
175+
} else {
176+
toast({
177+
title: 'Failed to delete config',
178+
description: 'There was an error while deleting your config. Please try again!',
179+
variant: 'destructive',
180+
});
181+
}
182+
} catch (error) {
183+
toast({
184+
title: 'Error',
185+
description: 'An error occurred while deleting the config.',
186+
variant: 'destructive',
187+
});
188+
}
189+
}}
190+
>
191+
Delete
192+
</Button>
193+
</form>
194+
);
195+
196+
const renderDialogDrawer = () => {
197+
if (isDesktop) {
198+
return (
199+
<Dialog open={open} onOpenChange={setOpen}>
200+
<DialogTrigger asChild>
201+
<Button className="hidden">Open</Button>
202+
</DialogTrigger>
203+
<DialogContent>
204+
<DialogHeader>
205+
<DialogTitle>Update Config Details</DialogTitle>
206+
<DialogDescription>
207+
Update the details of your config!
208+
</DialogDescription>
209+
</DialogHeader>
210+
{renderForm()}
211+
</DialogContent>
212+
</Dialog>
213+
);
214+
}
215+
return (
216+
<Drawer open={open} onOpenChange={setOpen}>
217+
<DrawerTrigger asChild>
218+
<Button className="hidden">Open</Button>
219+
</DrawerTrigger>
220+
<DrawerContent>
221+
<DrawerHeader>
222+
<DrawerTitle>Update Config Details</DrawerTitle>
223+
<DrawerDescription>
224+
Update the details of your config!
225+
</DrawerDescription>
226+
</DrawerHeader>
227+
{renderForm()}
228+
<DrawerFooter>
229+
<DrawerClose asChild>
230+
<Button variant="outline">Cancel</Button>
231+
</DrawerClose>
232+
</DrawerFooter>
233+
</DrawerContent>
234+
</Drawer>
235+
);
236+
};
237+
238+
return (
239+
<>
240+
<Button
241+
variant="secondary"
242+
onClick={() => setOpen(true)}
243+
className='w-full m-0 rounded-tl-none rounded-tr-none hover:opacity-80 transition-all duration-100 bg-[#4482ef] text-white hover:bg-[#4471efe6] rounded-br-none rounded-bl-none'
244+
>
245+
Update
246+
</Button>
247+
{renderDialogDrawer()}
248+
</>
249+
);
250+
};
251+
252+
export default UpdateAction;

0 commit comments

Comments
 (0)