From 194b541143e3fe61777b88590533ca1404b5856c Mon Sep 17 00:00:00 2001 From: Sonny Date: Sun, 14 Apr 2024 16:01:12 +0200 Subject: [PATCH] feat: add category visibility --- .../migration.sql | 2 ++ prisma/schema.prisma | 12 ++++++--- public/locales/en/common.json | 1 + public/locales/fr/common.json | 1 + src/components/Links/Links.tsx | 11 +++++--- src/components/Links/links.module.scss | 10 +++++-- .../Categories/CategoryItem.tsx | 15 +++++++++-- .../Categories/categories.module.scss | 15 ++++++++--- src/components/Visibility/Visibility.tsx | 18 +++++++++++++ .../Visibility/visibility.module.scss | 14 ++++++++++ src/lib/category/categoryValidationSchema.ts | 4 ++- src/pages/api/category/[cid].ts | 6 ++--- src/pages/api/category/index.ts | 6 +++-- src/pages/category/create.tsx | 13 ++++++++- src/pages/category/edit/[cid].tsx | 27 ++++++++++++++++--- 15 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 prisma/migrations/20240413174109_add_category_visibility/migration.sql create mode 100644 src/components/Visibility/Visibility.tsx create mode 100644 src/components/Visibility/visibility.module.scss diff --git a/prisma/migrations/20240413174109_add_category_visibility/migration.sql b/prisma/migrations/20240413174109_add_category_visibility/migration.sql new file mode 100644 index 0000000..397a8a4 --- /dev/null +++ b/prisma/migrations/20240413174109_add_category_visibility/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `category` ADD COLUMN `visibility` ENUM('private', 'public') NOT NULL DEFAULT 'private'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b61b57a..34c34a9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,10 +29,11 @@ model User { } model Category { - id Int @id @default(autoincrement()) - name String @db.VarChar(255) - description String? @db.VarChar(255) + id Int @id @default(autoincrement()) + name String @db.VarChar(255) + description String? @db.VarChar(255) links Link[] + visibility Visibility @default(private) author User @relation(fields: [authorId], references: [id]) authorId Int @@ -63,3 +64,8 @@ model Link { @@map("link") } + +enum Visibility { + private + public +} diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 7a94314..e1b6bff 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -21,6 +21,7 @@ "name": "Category name", "description": "Category description", "no-description": "No description", + "visibility": "Public", "create": "Create a category", "edit": "Edit a category", "remove": "Delete a category", diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index 5bbfc44..c71bde4 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -20,6 +20,7 @@ "category": "Catégorie", "name": "Nom de la catégorie", "description": "Description de la catégorie", + "visibility": "Public", "no-description": "Aucune description", "create": "Créer une catégorie", "edit": "Modifier une catégorie", diff --git a/src/components/Links/Links.tsx b/src/components/Links/Links.tsx index f1135e0..7ae7db5 100644 --- a/src/components/Links/Links.tsx +++ b/src/components/Links/Links.tsx @@ -4,6 +4,7 @@ import Footer from 'components/Footer/Footer'; import CreateItem from 'components/QuickActions/CreateItem'; import EditItem from 'components/QuickActions/EditItem'; import RemoveItem from 'components/QuickActions/RemoveItem'; +import VisibilityBadge from 'components/Visibility/Visibility'; import { motion } from 'framer-motion'; import useActiveCategory from 'hooks/useActiveCategory'; import { useTranslation } from 'next-i18next'; @@ -45,12 +46,16 @@ export default function Links({ )} - - {name} +
+
{name}
{links.length > 0 && ( — {links.length} )} - + +
svg { display: flex; } diff --git a/src/components/SideNavigation/Categories/CategoryItem.tsx b/src/components/SideNavigation/Categories/CategoryItem.tsx index 0024603..bbc33bf 100644 --- a/src/components/SideNavigation/Categories/CategoryItem.tsx +++ b/src/components/SideNavigation/Categories/CategoryItem.tsx @@ -1,10 +1,12 @@ import clsx from 'clsx'; +import VisibilityBadge from 'components/Visibility/Visibility'; import PATHS from 'constants/paths'; import { motion } from 'framer-motion'; import useActiveCategory from 'hooks/useActiveCategory'; import useCategories from 'hooks/useCategories'; import sortCategoriesByNextId from 'lib/category/sortCategoriesByNextId'; import { makeRequest } from 'lib/request'; +import { useTranslation } from 'next-i18next'; import { useCallback, useEffect, useRef } from 'react'; import { useDrag, useDrop } from 'react-dnd'; import { AiFillFolderOpen, AiOutlineFolder } from 'react-icons/ai'; @@ -28,6 +30,7 @@ export default function CategoryItem({ }: Readonly): JSX.Element { const { activeCategory, setActiveCategory } = useActiveCategory(); const { categories, setCategories } = useCategories(); + const { t } = useTranslation('common'); const ref = useRef(); @@ -155,8 +158,16 @@ export default function CategoryItem({ )}
- {category.name} - — {category.links.length} +
+ {category.name} + + — {category.links.length} + +
+
); diff --git a/src/components/SideNavigation/Categories/categories.module.scss b/src/components/SideNavigation/Categories/categories.module.scss index 166e3be..c1ee4d1 100644 --- a/src/components/SideNavigation/Categories/categories.module.scss +++ b/src/components/SideNavigation/Categories/categories.module.scss @@ -37,13 +37,22 @@ } & .content { - width: calc(100% - 42px); + width: 100%; display: flex; - flex: 1; + gap: 0.35em; align-items: center; + & .name-wrapper { + min-width: 0; + width: 100%; + display: flex; + gap: 0.35em; + flex: 1; + align-items: center; + } + & .name { - margin-right: 5px; + min-width: 0; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; diff --git a/src/components/Visibility/Visibility.tsx b/src/components/Visibility/Visibility.tsx new file mode 100644 index 0000000..9ed969a --- /dev/null +++ b/src/components/Visibility/Visibility.tsx @@ -0,0 +1,18 @@ +import { Visibility } from '@prisma/client'; +import { IoEarthOutline } from 'react-icons/io5'; +import styles from './visibility.module.scss'; + +const VisibilityBadge = ({ + label, + visibility, +}: { + label: string; + visibility: Visibility; +}) => + visibility === Visibility.public && ( + + {label} + + ); + +export default VisibilityBadge; diff --git a/src/components/Visibility/visibility.module.scss b/src/components/Visibility/visibility.module.scss new file mode 100644 index 0000000..45d1573 --- /dev/null +++ b/src/components/Visibility/visibility.module.scss @@ -0,0 +1,14 @@ +@import 'styles/colors.scss'; + +.visibility { + font-weight: 300; + font-size: 0.6em; + color: rgba($color: $blue, $alpha: 0.75); + border: 1px solid rgba($color: $blue, $alpha: 0.75); + border-radius: 50px; + padding: 0.15em 0.65em; + margin-left: 0.5em; + display: flex; + gap: 0.35em; + align-items: center; +} diff --git a/src/lib/category/categoryValidationSchema.ts b/src/lib/category/categoryValidationSchema.ts index ea044be..76d5ab1 100644 --- a/src/lib/category/categoryValidationSchema.ts +++ b/src/lib/category/categoryValidationSchema.ts @@ -1,4 +1,5 @@ -import { number, object, string } from 'yup'; +import { Visibility } from '@prisma/client'; +import { mixed, number, object, string } from 'yup'; const CategoryBodySchema = object({ name: string() @@ -6,6 +7,7 @@ const CategoryBodySchema = object({ .required('Category name is required') .max(128, 'Category name is too long'), description: string().trim().max(255, 'Category description is too long'), + visibility: mixed().oneOf(Object.values(Visibility)).required(), nextId: number().required().nullable(), }).typeError('Missing request Body'); diff --git a/src/pages/api/category/[cid].ts b/src/pages/api/category/[cid].ts index c7f2b97..bd9b63f 100644 --- a/src/pages/api/category/[cid].ts +++ b/src/pages/api/category/[cid].ts @@ -16,9 +16,8 @@ export default apiHandler({ async function editCategory({ req, res, user }) { const { cid } = await CategoryQuerySchema.validate(req.query); - const { name, description, nextId } = await CategoryBodySchema.validate( - req.body, - ); + const { name, description, visibility, nextId } = + await CategoryBodySchema.validate(req.body); const userId = user.id as User['id']; const category = await getUserCategory(user, cid); @@ -107,6 +106,7 @@ async function editCategory({ req, res, user }) { data: { name, description, + visibility, nextId: category.nextId, }, }); diff --git a/src/pages/api/category/index.ts b/src/pages/api/category/index.ts index 3443cf0..583e19e 100644 --- a/src/pages/api/category/index.ts +++ b/src/pages/api/category/index.ts @@ -18,7 +18,9 @@ async function getCategories({ res, user }) { } async function createCategory({ req, res, user }) { - const { name, description } = await CategoryBodySchema.validate(req.body); + const { name, description, visibility } = await CategoryBodySchema.validate( + req.body, + ); const category = await getUserCategoryByName(user, name); if (category) { @@ -36,7 +38,7 @@ async function createCategory({ req, res, user }) { }); const categoryCreated = await prisma.category.create({ - data: { name, description, authorId: user.id }, + data: { name, description, visibility, authorId: user.id }, }); if (lastCategory) { diff --git a/src/pages/category/create.tsx b/src/pages/category/create.tsx index c1d2b07..46ebaf9 100644 --- a/src/pages/category/create.tsx +++ b/src/pages/category/create.tsx @@ -1,3 +1,5 @@ +import { Visibility } from '@prisma/client'; +import Checkbox from 'components/Checkbox'; import FormLayout from 'components/FormLayout'; import PageTransition from 'components/PageTransition'; import TextBox from 'components/TextBox'; @@ -24,6 +26,7 @@ export default function PageCreateCategory({ const [name, setName] = useState(''); const [description, setDescription] = useState(''); + const [visibility, setVisibility] = useState(Visibility.private); const [error, setError] = useState(null); const [submitted, setSubmitted] = useState(false); @@ -41,7 +44,7 @@ export default function PageCreateCategory({ makeRequest({ url: PATHS.API.CATEGORY, method: 'POST', - body: { name, description, nextId: null }, + body: { name, description, visibility, nextId: null }, }) .then((data) => router.push(`${PATHS.APP}?categoryId=${data?.categoryId}`), @@ -78,6 +81,14 @@ export default function PageCreateCategory({ fieldClass={styles['input-field']} placeholder={t('common:category.description')} /> + + setVisibility(!!value ? Visibility.public : Visibility.private) + } + label={t('common:category.visibility')} + /> ); diff --git a/src/pages/category/edit/[cid].tsx b/src/pages/category/edit/[cid].tsx index b19ce80..b0b89c3 100644 --- a/src/pages/category/edit/[cid].tsx +++ b/src/pages/category/edit/[cid].tsx @@ -1,3 +1,5 @@ +import { Visibility } from '@prisma/client'; +import Checkbox from 'components/Checkbox'; import FormLayout from 'components/FormLayout'; import PageTransition from 'components/PageTransition'; import TextBox from 'components/TextBox'; @@ -24,16 +26,27 @@ export default function PageEditCategory({ const [name, setName] = useState(category.name); const [description, setDescription] = useState(category.description); + const [visibility, setVisibility] = useState(category.visibility); const [error, setError] = useState(null); const [submitted, setSubmitted] = useState(false); const canSubmit = useMemo(() => { const isFormEdited = - name !== category.name || description !== category.description; + name !== category.name || + description !== category.description || + visibility !== category.visibility; const isFormValid = name !== ''; return isFormEdited && isFormValid && !submitted; - }, [category.description, category.name, description, name, submitted]); + }, [ + category.description, + category.name, + category.visibility, + description, + name, + submitted, + visibility, + ]); const handleSubmit = async (event: FormEvent) => { event.preventDefault(); @@ -43,7 +56,7 @@ export default function PageEditCategory({ makeRequest({ url: `${PATHS.API.CATEGORY}/${category.id}`, method: 'PUT', - body: { name, description, nextId: category.nextId }, + body: { name, description, visibility, nextId: category.nextId }, }) .then((data) => router.push(`${PATHS.APP}?categoryId=${data?.categoryId}`), @@ -78,6 +91,14 @@ export default function PageEditCategory({ fieldClass={styles['input-field']} placeholder={t('common:category.description')} /> + + setVisibility(!!value ? Visibility.public : Visibility.private) + } + label={t('common:category.visibility')} + /> );