chore: add all the code from original repo
This commit is contained in:
101
app/components/Settings/Profiles/ProfilesTable.tsx
Normal file
101
app/components/Settings/Profiles/ProfilesTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
81
app/components/Settings/Profiles/ProfilesTableRow.tsx
Normal file
81
app/components/Settings/Profiles/ProfilesTableRow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
23
app/components/Settings/Profiles/actions/ProfilesAction.ts
Normal file
23
app/components/Settings/Profiles/actions/ProfilesAction.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
33
app/components/Settings/Settings.tsx
Normal file
33
app/components/Settings/Settings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
103
app/components/Settings/Tags/TagsTable.tsx
Normal file
103
app/components/Settings/Tags/TagsTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
65
app/components/Settings/Tags/TagsTableRow.tsx
Normal file
65
app/components/Settings/Tags/TagsTableRow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
53
app/components/Settings/Tags/actions/CreateTagAction.ts
Normal file
53
app/components/Settings/Tags/actions/CreateTagAction.ts
Normal 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`);
|
||||
}
|
||||
}
|
||||
22
app/components/Settings/Tags/actions/DeleteTagAction.ts
Normal file
22
app/components/Settings/Tags/actions/DeleteTagAction.ts
Normal 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`);
|
||||
}
|
||||
}
|
||||
40
app/components/Settings/Tags/actions/TagsAction.ts
Normal file
40
app/components/Settings/Tags/actions/TagsAction.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
31
app/components/Settings/Tags/actions/UpdateTagAction.ts
Normal file
31
app/components/Settings/Tags/actions/UpdateTagAction.ts
Normal 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`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user