chore: add all the code from original repo

This commit is contained in:
Riccardo
2024-05-23 16:55:29 +02:00
parent d8f9a215eb
commit 85d66215a7
66 changed files with 16668 additions and 122 deletions

View File

@@ -0,0 +1,68 @@
import { TagsAction } from '@/components/Settings/Tags/actions/TagsAction';
import { Button, Dialog, DialogContent, DialogTitle } from '@mui/material';
import { useEffect, useState } from 'react';
import { useFormState } from 'react-dom';
import { Tags } from '../../../../data/types';
import { CreateItemAction } from './actions/CreateItemAction';
import { CreateItemForm } from './CreateItemForm';
interface CreateItemProps {
open: boolean;
close: (created: boolean) => void;
profile: string | undefined;
}
export default function CreateItemDialog({
open,
close,
profile
}: CreateItemProps) {
const [tags, setTags] = useState<Tags>([]);
const [formState, formAction] = useFormState(CreateItemAction, {
open: true,
profile
});
useEffect(() => {
const updateTags = async () => {
if (profile) {
try {
const updatedTags = await TagsAction({ selectedProfile: profile });
setTags(updatedTags);
} catch (error) {
console.error("Couldn't fetch tags.");
}
}
};
updateTags();
}, [profile]);
useEffect(() => {
if (!formState.open) {
close(true);
}
}, [open, close, formState]);
const handleClose = async () => {
close(false);
};
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitle sx={{ alignSelf: 'center' }}>Create a new item</DialogTitle>
<DialogContent sx={{ width: '400px' }}>
<CreateItemForm profile={profile} formAction={formAction} tags={tags} />
<Button
variant="outlined"
color="primary"
fullWidth
onClick={handleClose}
>
Close
</Button>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,89 @@
import { CreateButton } from '@/components/UI/CreateButton';
import {
Checkbox,
FormControl,
FormControlLabel,
InputLabel,
MenuItem,
Select,
SelectChangeEvent,
Stack,
TextField
} from '@mui/material';
import { Currency } from '@prisma/client';
import { useState } from 'react';
import { Tag } from '../../../../data/types';
interface CreateItemProps {
profile: string | undefined;
formAction: (payload: FormData) => void;
tags: Tag[];
}
export function CreateItemForm({ profile, formAction, tags }: CreateItemProps) {
const [selectedTag, setSelectedTag] = useState('');
const handleTagChange = (event: SelectChangeEvent<string>) => {
setSelectedTag(event.target.value as string);
};
return (
<form action={formAction}>
<Stack spacing={2} my={2}>
<TextField name="name" label="Item" variant="outlined" required />
<TextField name="description" label="Description" variant="outlined" />
<TextField
name="price"
label="Price"
variant="outlined"
required
type="number"
inputProps={{ defaultValue: 0, step: 0.01, min: 0 }}
/>
<FormControl variant="outlined">
<InputLabel id="currency-label">Currency</InputLabel>
<Select
name="currency"
label="Currency"
labelId="currency-label"
value={Currency.DKK}
>
{Object.values(Currency).map((value) => (
<MenuItem key={value} value={value}>
{value}
</MenuItem>
))}
</Select>
</FormControl>
{profile && (
<FormControl variant="outlined">
<InputLabel id="tag-label">Tag</InputLabel>
<Select
name="tag"
label="Tag"
labelId="tag-label"
value={selectedTag} // Use the state here
onChange={handleTagChange}
>
{tags.map((tag) => (
<MenuItem key={tag.id} value={tag.id}>
{tag.name}
</MenuItem>
))}
</Select>
</FormControl>
)}
<TextField name="body" label="Comment" variant="outlined" />
<TextField
name="score"
label="Score"
variant="outlined"
type="number"
inputProps={{ step: 1, min: 0, max: 10 }}
/>
<FormControlLabel control={<Checkbox name="regret" />} label="Regret" />
<CreateButton />
</Stack>
</form>
);
}

View File

@@ -0,0 +1,76 @@
'use server';
import { nanoid } from 'nanoid';
import { initializeUser } from '../../../../../data/initializeUser';
import { CreateItemFormSchema } from '../../../../../data/types';
import prisma from '../../../../../prisma/prisma';
interface CreateItemActionProps {
open: boolean;
profile?: string;
error?: string;
}
export async function CreateItemAction(
{ profile }: CreateItemActionProps,
formData: FormData
) {
await initializeUser();
const profileId = profile ?? (await prisma.profile.findFirst())?.id;
const formDataObj = Object.fromEntries(formData.entries());
const validatedBody = CreateItemFormSchema.safeParse(formDataObj);
if (!validatedBody.success || !profileId) {
throw new Error('Bad request');
}
const { name, description, price, currency, tag, body, score, regret } =
validatedBody.data;
const newId = nanoid();
try {
await prisma.$transaction([
prisma.item.create({
data: {
id: newId,
name,
description,
price,
currency,
Profile: {
connect: {
id: profileId
}
},
Tag: tag
? {
connect: {
id: tag
}
}
: undefined
}
}),
prisma.itemComment.create({
data: {
body,
score,
regret,
Item: {
connect: {
id: newId
}
}
}
})
]);
} catch (error) {
throw new Error(`Failed to create item`);
}
return { open: false, profile: profileId };
}

View File

@@ -0,0 +1,5 @@
import ItemsTable from './Items/ItemsTable';
export default function Dashboard() {
return <ItemsTable />;
}

View File

@@ -0,0 +1,44 @@
import { CreateButton } from '@/components/UI/CreateButton';
import { Checkbox, FormControlLabel, Stack, TextField } from '@mui/material';
import { nanoid } from 'nanoid';
import { useEffect } from 'react';
import { useFormState } from 'react-dom';
import { CreateCommentAction } from './actions/CreateCommentAction';
interface CreateCommentProps {
itemId: string;
setFormKey: (key: string) => void;
}
export default function CreateComment({
itemId,
setFormKey
}: CreateCommentProps) {
const [formState, formAction] = useFormState(CreateCommentAction, {
itemId,
clear: false
});
useEffect(() => {
if (formState.clear) {
setFormKey(nanoid());
}
}, [setFormKey, formState]);
return (
<form action={formAction}>
<Stack spacing={2} marginY={2}>
<TextField name="body" label="Comment" variant="outlined" required />
<TextField
name="score"
label="Score"
variant="outlined"
type="number"
inputProps={{ step: 1, min: 0, max: 10 }}
/>
<FormControlLabel name="regret" control={<Checkbox />} label="Regret" />
<CreateButton />
</Stack>
</form>
);
}

View File

@@ -0,0 +1,44 @@
'use server';
import { CreateCommentFormSchema } from '../../../../../../data/types';
import prisma from '../../../../../../prisma/prisma';
interface ItemDialogActionProps {
clear: boolean;
itemId: string;
error?: string;
}
export async function CreateCommentAction(
{ itemId }: ItemDialogActionProps,
formData: FormData
) {
const formDataObj = Object.fromEntries(formData.entries());
const validatedBody = CreateCommentFormSchema.safeParse(formDataObj);
if (!validatedBody.success) {
throw new Error('Bad request');
}
const { body, score, regret } = validatedBody.data;
try {
await prisma.itemComment.create({
data: {
body,
score,
regret,
Item: {
connect: {
id: itemId
}
}
}
});
} catch (error) {
throw new Error(`Failed to create comment`);
}
return { clear: true, itemId };
}

View File

@@ -0,0 +1,39 @@
import { Card, Typography } from '@mui/material';
import { Box } from '@mui/system';
import { Item, ItemComment } from '../../../../data/types';
interface ItemDataProps {
item: Item;
comments: ItemComment[];
}
export default function ItemData({ item, comments }: ItemDataProps) {
return (
<Box>
<Typography>Description: {item.description}</Typography>
<Typography>
Price: {Number(item.price).toFixed(2)} {item.currency}
</Typography>
<Box
sx={{
height: 250,
overflowY: 'auto'
}}
>
{comments &&
comments.map((comment) => (
<Card key={comment.id} sx={{ margin: '5px', padding: '10px' }}>
<Typography>Comment: {comment.body}</Typography>
<Typography>Score: {comment.score}</Typography>
<Typography>Regret: {comment.regret ? 'Yes' : 'No'}</Typography>
<Typography>
Created:{' '}
{comment.createdAt &&
new Date(comment.createdAt).toLocaleString()}
</Typography>
</Card>
))}
</Box>
</Box>
);
}

View File

@@ -0,0 +1,53 @@
import { Button, Dialog, DialogContent, DialogTitle } from '@mui/material';
import { nanoid } from 'nanoid';
import { useCallback, useEffect, useState } from 'react';
import { Item, ItemComment } from '../../../../data/types';
import { ItemsTableRowAction } from '../Items/ItemsTableRowAction';
import CreateComment from './CreateComment/CreateComment';
import ItemData from './ItemData';
interface ItemDialogProps {
item: Item;
open: boolean;
handleClose: () => void;
}
export default function ItemDialog({
item,
open,
handleClose
}: ItemDialogProps) {
const [comments, setComments] = useState<ItemComment[]>([]);
const [formKey, setFormKey] = useState(() => nanoid());
const fetchData = useCallback(async () => {
try {
const comments = await ItemsTableRowAction(item.id);
setComments(comments);
} catch (error) {
console.error("Couldn't fetch comments.");
}
}, [item]);
useEffect(() => {
fetchData();
}, [fetchData, formKey]);
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitle sx={{ alignSelf: 'center' }}>Item: {item.name}</DialogTitle>
<DialogContent sx={{ width: 400 }}>
<ItemData item={item} comments={comments} />
<CreateComment key={formKey} itemId={item.id} setFormKey={setFormKey} />
<Button
variant="outlined"
color="primary"
onClick={handleClose}
sx={{ width: '100%' }}
>
Close
</Button>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,202 @@
import { ProfilesAction } from '@/components/Settings/Profiles/actions/ProfilesAction';
import { ProfileContext } from '@/contexts/ProfileContext';
import {
Box,
Button,
MenuItem,
Paper,
Select,
SelectChangeEvent,
Stack,
Table,
TableBody,
TableCell,
TableHead,
TableRow
} from '@mui/material';
import { nanoid } from 'nanoid';
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { Item, Items, Profiles } from '../../../../data/types';
import CreateItemDialog from '../CreateItem/CreateItemDialog';
import ItemDialog from '../ItemDialog/ItemDialog';
import { ItemsTableAction } from './actions/ItemsTableAction';
import ItemsTableRow from './ItemsTableRow';
export default function ItemsTable() {
const [items, setItems] = useState<Items>([]);
const [take, setTake] = useState<number>(20);
const tableRef = useRef<HTMLDivElement>(null);
const [openCreateDialog, setOpenCreateDialog] = useState(false);
const [openItemDialog, setOpenItemDialog] = useState(false);
const [formKey, setFormKey] = useState(() => nanoid());
const [profiles, setProfiles] = useState<Profiles>([]);
const { profile, setProfile } = useContext(ProfileContext);
const [currentItem, setCurrentItem] = useState<Item | null>(null);
const handleUpdateProfiles = useCallback(async () => {
const updatedProfiles = await ProfilesAction();
setProfiles(updatedProfiles);
}, []);
useEffect(() => {
handleUpdateProfiles();
}, [handleUpdateProfiles]);
const handleUpdateItems = useCallback(
async (id: string) => {
try {
const updatedItems = await ItemsTableAction({
take,
profile: id
});
setItems(updatedItems);
} catch (error) {
console.error("Couldn't fetch items.");
}
},
[take]
);
const handleFetchItems = useCallback(
(event: SelectChangeEvent<string>) => {
handleUpdateItems(event.target.value);
setFormKey(nanoid());
setProfile(event.target.value);
},
[handleUpdateItems, setProfile]
);
useEffect(() => {
if (profile) {
handleUpdateItems(profile);
setFormKey(nanoid());
}
}, [profile, handleUpdateItems]);
useEffect(() => {
const handleScroll = (event: Event) => {
const target = event.target as HTMLDivElement;
const bottom =
target.scrollHeight - target.scrollTop === target.clientHeight;
if (bottom) {
setTake((prevTake) => prevTake + 20);
}
};
const tableElement = tableRef.current;
if (tableElement) {
tableElement.addEventListener('scroll', handleScroll);
return () => {
tableElement.removeEventListener('scroll', handleScroll);
};
}
}, [take]);
const handleCreateItemDialog = () => {
setOpenCreateDialog(true);
};
const handleCloseCreateDialog = async (created: boolean) => {
setFormKey(nanoid());
setOpenCreateDialog(false);
if (created) {
const updatedItems = await ItemsTableAction({
take,
profile
});
setItems(updatedItems);
}
};
const handleOpenItemDialog = (item: Item) => {
setCurrentItem(item);
setOpenItemDialog(true);
};
const handleCloseItemDialog = async () => {
setOpenItemDialog(false);
};
return (
<Paper sx={{ width: '100%' }}>
<Stack direction="row" spacing={2} padding={2}>
{profiles.length > 0 && (
<Select
labelId="profile-label"
sx={{ width: '200px' }}
onChange={handleFetchItems}
value={profile ?? ''}
>
{profiles.map((profile) => (
<MenuItem key={profile.id} value={profile.id}>
{profile.name}
</MenuItem>
))}
</Select>
)}
<Button
variant="outlined"
color="primary"
type="submit"
onClick={handleCreateItemDialog}
sx={{ width: '200px' }}
>
Create Item
</Button>
</Stack>
<Box
sx={{
height: 'calc(100vh - 50px)',
overflowY: 'scroll',
padding: 2,
paddingTop: 0
}}
ref={tableRef}
>
<Table
stickyHeader
sx={{
width: '100%'
}}
>
<TableHead>
<TableRow sx={{ '& th': { textAlign: 'center' } }}>
<TableCell>Name</TableCell>
<TableCell>Description</TableCell>
<TableCell>Price</TableCell>
<TableCell>Currency</TableCell>
<TableCell>Tag</TableCell>
<TableCell>Date</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items &&
items.map((item) => (
<ItemsTableRow
key={item.id}
item={item}
onClick={handleOpenItemDialog}
/>
))}
</TableBody>
</Table>
</Box>
{currentItem && (
<ItemDialog
item={currentItem}
open={openItemDialog}
handleClose={handleCloseItemDialog}
/>
)}
<CreateItemDialog
key={formKey}
open={openCreateDialog}
close={handleCloseCreateDialog}
profile={profile}
/>
</Paper>
);
}

View File

@@ -0,0 +1,26 @@
import { TableCell, TableRow } from '@mui/material';
import { Item } from '../../../../data/types';
interface ItemsTableRowProps {
item: Item;
onClick: (item: Item) => void;
}
export default function ItemsTableRow({ item, onClick }: ItemsTableRowProps) {
return (
<TableRow
sx={{ '& td': { textAlign: 'center' } }}
key={item.id}
onClick={() => onClick(item)}
>
<TableCell>{item.name}</TableCell>
<TableCell>{item.description}</TableCell>
<TableCell>{Number(item.price).toFixed(2)}</TableCell>
<TableCell>{item.currency}</TableCell>
<TableCell>{item.tag}</TableCell>
<TableCell>
{item.createdAt && new Date(item.createdAt).toLocaleDateString()}
</TableCell>
</TableRow>
);
}

View File

@@ -0,0 +1,28 @@
'use server';
import { z } from 'zod';
import { ItemComment } from '../../../../data/types';
import prisma from '../../../../prisma/prisma';
export async function ItemsTableRowAction(id: string) {
const validatedId = z.string().safeParse(id);
if (!validatedId.success) {
throw new Error('Bad request');
}
try {
const itemWithComments = await prisma.item.findFirstOrThrow({
where: {
id: validatedId.data
},
include: {
ItemComment: true
}
});
return itemWithComments.ItemComment as ItemComment[];
} catch (error) {
throw new Error('Failed to find item comments');
}
}

View File

@@ -0,0 +1,48 @@
'use server';
import { Items } from '../../../../../data/types';
import prisma from '../../../../../prisma/prisma';
interface ItemsTableActionProps {
take: number;
profile: string | undefined;
}
export async function ItemsTableAction({
take,
profile
}: ItemsTableActionProps) {
if (!profile) {
return [] as Items;
}
try {
const items = await prisma.item.findMany({
where: {
profileId: profile
},
orderBy: {
createdAt: 'desc'
},
include: {
Tag: {
select: {
name: true
}
}
},
take
});
const itemsWithTag = items.map((item) => {
return {
...item,
tag: item?.Tag?.name
};
});
return itemsWithTag as Items;
} catch (error) {
throw new Error('Failed to find items');
}
}

View File

@@ -0,0 +1,101 @@
import {
Box,
Stack,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
TextField,
Typography
} from '@mui/material';
import { nanoid } from 'nanoid';
import { Dispatch, useEffect, useState } from 'react';
import { useFormState } from 'react-dom';
import { Profiles } from '../../../../data/types';
import { CreateProfileAction } from './actions/CreateProfileAction';
import { ProfilesAction } from './actions/ProfilesAction';
import ProfilesTableRow from './ProfilesTableRow';
interface ProfilesTableProps {
selectedProfile: string | undefined;
setSelectedProfile: Dispatch<string | undefined>;
}
export default function ProfilesTable({
selectedProfile,
setSelectedProfile
}: ProfilesTableProps) {
const [profiles, setProfiles] = useState<Profiles>([]);
const [formKey, setFormKey] = useState(() => nanoid());
const [refreshKey, setRefreshKey] = useState(0);
const [formState, formAction] = useFormState(CreateProfileAction, {
clear: false
});
useEffect(() => {
const fetchProfiles = async () => {
try {
const profiles = await ProfilesAction();
setProfiles(profiles);
} catch (error) {
console.error("Couldn't fetch profiles.");
}
};
if (formState.clear) {
setFormKey(nanoid());
}
fetchProfiles();
}, [formState, refreshKey]);
return (
<Stack
sx={{
spacing: 2,
padding: 2
}}
>
<Typography variant="h6">Profiles</Typography>
<Box>
<form key={formKey} action={formAction}>
<TextField fullWidth name="name" placeholder="Add new profile" />
</form>
<Box
sx={{
maxHeight: 'calc(100vh - 200px)',
overflow: 'auto'
}}
>
<Table
stickyHeader
sx={{
width: '100%'
}}
>
<TableHead>
<TableRow sx={{ '& th': { textAlign: 'center' } }}>
<TableCell>Name</TableCell>
<TableCell>Delete</TableCell>
</TableRow>
</TableHead>
<TableBody>
{profiles &&
profiles.map((profile) => (
<ProfilesTableRow
key={profile.id}
profile={profile}
selected={selectedProfile}
setSelected={setSelectedProfile}
onDelete={() => setRefreshKey((prevKey) => prevKey + 1)}
/>
))}
</TableBody>
</Table>
</Box>
</Box>
</Stack>
);
}

View File

@@ -0,0 +1,81 @@
import { Button, TableCell, TableRow, TextField } from '@mui/material';
import { Dispatch, useState } from 'react';
import { Profile } from '../../../../data/types';
import { DeleteProfileAction } from './actions/DeleteProfileAction';
import { UpdateProfileAction } from './actions/UpdateProfileAction';
interface ProfilesTableRowProps {
profile: Profile;
selected: string | undefined;
setSelected: Dispatch<string | undefined>;
onDelete: () => void;
}
export default function ProfilesTableRow({
profile,
selected,
setSelected,
onDelete
}: ProfilesTableRowProps) {
const [isEditing, setIsEditing] = useState(false);
const [name, setName] = useState(profile.name);
const handleClick = () => {
setSelected(profile.id);
};
const handleDoubleClick = () => {
setIsEditing(true);
};
const handleBlur = async () => {
setIsEditing(false);
try {
await UpdateProfileAction({
id: profile.id,
name
});
} catch (error) {
console.error("Couldn't update profile.");
}
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
};
const handleDelete = async () => {
try {
await DeleteProfileAction(profile.id);
onDelete();
} catch (error) {
console.error("Couldn't delete profile.");
}
};
return (
<TableRow key={profile.id}>
<TableCell
align="center"
onClick={handleClick}
onDoubleClick={handleDoubleClick}
sx={{ bgcolor: profile.id === selected ? 'lightblue' : 'inherit' }}
>
{isEditing ? (
<TextField
value={name}
onChange={handleChange}
onBlur={handleBlur}
autoFocus
/>
) : (
name
)}
</TableCell>
<TableCell align="center">
<Button onClick={handleDelete}>Delete</Button>
</TableCell>
</TableRow>
);
}

View File

@@ -0,0 +1,53 @@
'use server';
import { initializeUser } from '../../../../../data/initializeUser';
import { CreateProfileFormSchema } from '../../../../../data/types';
import prisma from '../../../../../prisma/prisma';
interface CreateItemActionProps {
clear: boolean;
error?: string;
}
export async function CreateProfileAction(
_: CreateItemActionProps,
formData: FormData
) {
const formDataObj = Object.fromEntries(formData.entries());
const validatedBody = CreateProfileFormSchema.safeParse(formDataObj);
if (!validatedBody.success) {
throw new Error('Bad request');
}
const user = await initializeUser();
try {
const existingProfile = await prisma.profile.findFirst({
where: {
userId: user.id,
name: validatedBody.data.name
}
});
if (existingProfile) {
return { clear: false };
}
} catch (error) {
throw new Error(`Failed to find profile`);
}
try {
await prisma.profile.create({
data: {
userId: user.id,
name: validatedBody.data.name
}
});
return { clear: true };
} catch (error) {
throw new Error(`Failed to create profile`);
}
}

View File

@@ -0,0 +1,22 @@
'use server';
import { z } from 'zod';
import prisma from '../../../../../prisma/prisma';
export async function DeleteProfileAction(id: string) {
const validatedBody = z.string().safeParse(id);
if (!validatedBody.success) {
throw new Error('Bad request');
}
try {
await prisma.profile.delete({
where: {
id
}
});
} catch (error) {
throw new Error(`Failed to delete profile`);
}
}

View File

@@ -0,0 +1,23 @@
'use server';
import { initializeUser } from '../../../../../data/initializeUser';
import prisma from '../../../../../prisma/prisma';
export async function ProfilesAction() {
const user = await initializeUser();
try {
const profiles = await prisma.profile.findMany({
where: {
userId: user.id
},
orderBy: {
createdAt: 'desc'
}
});
return profiles;
} catch (error) {
throw new Error('Failed to find profiles');
}
}

View File

@@ -0,0 +1,31 @@
'use server';
import { z } from 'zod';
import prisma from '../../../../../prisma/prisma';
interface ProfileActionProps {
id: string;
name: string;
error?: string;
}
export async function UpdateProfileAction({ id, name }: ProfileActionProps) {
const validatedBody = z.string().safeParse(name);
if (!validatedBody.success) {
throw new Error('Bad request');
}
try {
await prisma.profile.update({
where: {
id
},
data: {
name: validatedBody.data
}
});
} catch (error) {
throw new Error(`Failed to update profile`);
}
}

View File

@@ -0,0 +1,33 @@
import { ProfileContext } from '@/contexts/ProfileContext';
import { Card, Paper, Stack } from '@mui/material';
import { nanoid } from 'nanoid';
import { useContext, useEffect, useState } from 'react';
import ProfilesTable from './Profiles/ProfilesTable';
import TagsTable from './Tags/TagsTable';
export default function Settings() {
const { profile, setProfile } = useContext(ProfileContext);
const [formKey, setFormKey] = useState(() => nanoid());
useEffect(() => {
if (profile) {
setFormKey(nanoid());
}
}, [profile]);
return (
<Paper sx={{ width: '100%', height: 'calc(100vh - 50px)' }}>
<Stack direction="row" spacing={2} padding={2}>
<Card sx={{ width: '50%', height: '100%' }}>
<ProfilesTable
selectedProfile={profile}
setSelectedProfile={setProfile}
/>
</Card>
<Card sx={{ width: '50%', height: '100%' }}>
<TagsTable key={formKey} selectedProfile={profile} />
</Card>
</Stack>
</Paper>
);
}

View File

@@ -0,0 +1,103 @@
import {
Box,
Stack,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
TextField,
Typography
} from '@mui/material';
import { nanoid } from 'nanoid';
import { useEffect, useState } from 'react';
import { useFormState } from 'react-dom';
import { Tags } from '../../../../data/types';
import { CreateTagAction } from './actions/CreateTagAction';
import { TagsAction } from './actions/TagsAction';
import TagsTableRow from './TagsTableRow';
interface TagsTableProps {
selectedProfile: string | undefined;
}
export default function TagsTable({ selectedProfile }: TagsTableProps) {
const [tags, setTags] = useState<Tags>([]);
const [formKey, setFormKey] = useState(() => nanoid());
const [refreshKey, setRefreshKey] = useState(0);
const [formState, formAction] = useFormState(CreateTagAction, {
clear: false,
profileId: selectedProfile
});
useEffect(() => {
const fetchTags = async () => {
if (selectedProfile) {
try {
const tags = await TagsAction({ selectedProfile });
setTags(tags);
} catch (error) {
console.error("Couldn't fetch tags.");
}
}
};
if (formState.clear) {
setFormKey(nanoid());
}
fetchTags();
}, [formState, selectedProfile, refreshKey]);
return (
<Stack
sx={{
spacing: 2,
padding: 2
}}
>
<Typography variant="h6">Tags</Typography>
<Box>
<form key={formKey} action={formAction}>
<TextField
fullWidth
disabled={!selectedProfile}
name="name"
placeholder="Add new tag"
/>
</form>
<Box
sx={{
maxHeight: 'calc(100vh - 200px)',
overflow: 'auto'
}}
>
<Table
stickyHeader
sx={{
width: '100%'
}}
>
<TableHead>
<TableRow sx={{ '& th': { textAlign: 'center' } }}>
<TableCell>Name</TableCell>
<TableCell>Delete</TableCell>
</TableRow>
</TableHead>
<TableBody>
{tags &&
tags.map((tag) => (
<TagsTableRow
key={tag.id}
tag={tag}
onDelete={() => setRefreshKey((prevKey) => prevKey + 1)}
/>
))}
</TableBody>
</Table>
</Box>
</Box>
</Stack>
);
}

View File

@@ -0,0 +1,65 @@
import { Button, TableCell, TableRow, TextField } from '@mui/material';
import { useState } from 'react';
import { Tag } from '../../../../data/types';
import { DeleteTagAction } from './actions/DeleteTagAction';
import { UpdateTagAction } from './actions/UpdateTagAction';
interface TagsTableRowProps {
tag: Tag;
onDelete: () => void;
}
export default function TagsTableRow({ tag, onDelete }: TagsTableRowProps) {
const [isEditing, setIsEditing] = useState(false);
const [name, setName] = useState(tag.name);
const handleDoubleClick = () => {
setIsEditing(true);
};
const handleBlur = async () => {
setIsEditing(false);
try {
await UpdateTagAction({
id: tag.id,
name
});
} catch (error) {
console.error("Couldn't update tag.");
}
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
};
const handleDelete = async () => {
try {
await DeleteTagAction(tag.id);
onDelete();
} catch (error) {
console.error("Couldn't delete tag.");
}
};
return (
<TableRow sx={{ '& td': { textAlign: 'center' } }} key={tag.id}>
<TableCell onDoubleClick={handleDoubleClick}>
{isEditing ? (
<TextField
value={name}
onChange={handleChange}
onBlur={handleBlur}
autoFocus
/>
) : (
name
)}
</TableCell>
<TableCell>
<Button onClick={handleDelete}>Delete</Button>
</TableCell>
</TableRow>
);
}

View File

@@ -0,0 +1,53 @@
'use server';
import { CreateProfileFormSchema } from '../../../../../data/types';
import prisma from '../../../../../prisma/prisma';
interface CreateItemActionProps {
clear: boolean;
profileId: string | undefined;
error?: string;
}
export async function CreateTagAction(
{ profileId }: CreateItemActionProps,
formData: FormData
) {
const formDataObj = Object.fromEntries(formData.entries());
const validatedBody = CreateProfileFormSchema.safeParse(formDataObj);
if (!validatedBody.success || !profileId) {
throw new Error('Bad request');
}
const { name } = validatedBody.data;
try {
const existingTag = await prisma.tag.findFirst({
where: {
profileId,
name
}
});
if (existingTag) {
return { clear: false, profileId };
}
} catch (error) {
throw new Error(`Failed to find tag`);
}
try {
await prisma.tag.create({
data: {
profileId,
name
}
});
return { clear: true, profileId };
} catch (error) {
throw new Error(`Failed to create tag`);
}
}

View File

@@ -0,0 +1,22 @@
'use server';
import { z } from 'zod';
import prisma from '../../../../../prisma/prisma';
export async function DeleteTagAction(id: string) {
const validatedBody = z.string().safeParse(id);
if (!validatedBody.success) {
throw new Error('Bad request');
}
try {
await prisma.tag.delete({
where: {
id
}
});
} catch (error) {
throw new Error(`Failed to delete tag`);
}
}

View File

@@ -0,0 +1,40 @@
'use server';
import { z } from 'zod';
import { initializeUser } from '../../../../../data/initializeUser';
import { Profiles } from '../../../../../data/types';
import prisma from '../../../../../prisma/prisma';
interface TagsActionProps {
selectedProfile: string | undefined;
}
export async function TagsAction({ selectedProfile }: TagsActionProps) {
const validatedBody = z.string().safeParse(selectedProfile);
if (!validatedBody.success) {
throw new Error('Bad request');
}
const user = await initializeUser();
try {
const profiles = await prisma.tag.findMany({
where: {
Profile: {
User: {
id: user.id
},
id: selectedProfile
}
},
orderBy: {
createdAt: 'desc'
}
});
return profiles as Profiles;
} catch (error) {
throw new Error('Failed to find tags');
}
}

View File

@@ -0,0 +1,31 @@
'use server';
import { z } from 'zod';
import prisma from '../../../../../prisma/prisma';
interface TagActionProps {
id: string;
name: string;
error?: string;
}
export async function UpdateTagAction({ id, name }: TagActionProps) {
const validatedBody = z.string().safeParse(name);
if (!validatedBody.success) {
throw new Error('Bad request');
}
try {
await prisma.tag.update({
where: {
id
},
data: {
name: validatedBody.data
}
});
} catch (error) {
throw new Error(`Failed to update tag`);
}
}

View File

@@ -0,0 +1,14 @@
'use client';
import { Button } from '@mui/material';
import { useFormStatus } from 'react-dom';
export const CreateButton = () => {
const { pending } = useFormStatus();
return (
<Button variant="outlined" color="primary" type="submit">
{pending ? 'Please wait...' : 'Submit'}
</Button>
);
};