mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-09 07:03:25 +00:00
feat: add optionnal link description
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `link` ADD COLUMN `description` VARCHAR(255) NULL;
|
||||||
@@ -45,9 +45,10 @@ model Category {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Link {
|
model Link {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @db.VarChar(255)
|
name String @db.VarChar(255)
|
||||||
url String @db.Text
|
description String? @db.VarChar(255)
|
||||||
|
url String @db.Text
|
||||||
|
|
||||||
category Category @relation(fields: [categoryId], references: [id])
|
category Category @relation(fields: [categoryId], references: [id])
|
||||||
categoryId Int
|
categoryId Int
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"links": "Links",
|
"links": "Links",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
"name": "Link name",
|
"name": "Link name",
|
||||||
|
"description": "Link description",
|
||||||
"create": "Create a link",
|
"create": "Create a link",
|
||||||
"edit": "Edit a link",
|
"edit": "Edit a link",
|
||||||
"remove": "Delete a link",
|
"remove": "Delete a link",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"links": "Liens",
|
"links": "Liens",
|
||||||
"link": "Lien",
|
"link": "Lien",
|
||||||
"name": "Nom du lien",
|
"name": "Nom du lien",
|
||||||
|
"description": "Description du lien",
|
||||||
"create": "Créer un lien",
|
"create": "Créer un lien",
|
||||||
"edit": "Modifier un lien",
|
"edit": "Modifier un lien",
|
||||||
"remove": "Supprimer un lien",
|
"remove": "Supprimer un lien",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import ExternalLink from 'components/ExternalLink';
|
||||||
import EditItem from 'components/QuickActions/EditItem';
|
import EditItem from 'components/QuickActions/EditItem';
|
||||||
import FavoriteItem from 'components/QuickActions/FavoriteItem';
|
import FavoriteItem from 'components/QuickActions/FavoriteItem';
|
||||||
import RemoveItem from 'components/QuickActions/RemoveItem';
|
import RemoveItem from 'components/QuickActions/RemoveItem';
|
||||||
@@ -5,7 +6,6 @@ import PATHS from 'constants/paths';
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import useCategories from 'hooks/useCategories';
|
import useCategories from 'hooks/useCategories';
|
||||||
import { makeRequest } from 'lib/request';
|
import { makeRequest } from 'lib/request';
|
||||||
import LinkTag from 'next/link';
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { AiFillStar } from 'react-icons/ai';
|
import { AiFillStar } from 'react-icons/ai';
|
||||||
import { LinkWithCategory } from 'types';
|
import { LinkWithCategory } from 'types';
|
||||||
@@ -19,7 +19,7 @@ export default function LinkItem({
|
|||||||
link: LinkWithCategory;
|
link: LinkWithCategory;
|
||||||
index: number;
|
index: number;
|
||||||
}) {
|
}) {
|
||||||
const { id, name, url, favorite } = link;
|
const { id, name, url, description, favorite } = link;
|
||||||
const { categories, setCategories } = useCategories();
|
const { categories, setCategories } = useCategories();
|
||||||
|
|
||||||
const toggleFavorite = useCallback(
|
const toggleFavorite = useCallback(
|
||||||
@@ -73,32 +73,35 @@ export default function LinkItem({
|
|||||||
delay: index * 0.05,
|
delay: index * 0.05,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LinkFavicon url={url} />
|
<div className={styles['link-header']}>
|
||||||
<LinkTag
|
<LinkFavicon url={url} />
|
||||||
href={url}
|
<ExternalLink
|
||||||
target={'_blank'}
|
href={url}
|
||||||
rel='noreferrer'
|
className='reset'
|
||||||
className='reset'
|
>
|
||||||
>
|
<span className={styles['link-name']}>
|
||||||
<span className={styles['link-name']}>
|
{name} {favorite && <AiFillStar color='#ffc107' />}
|
||||||
{name} {favorite && <AiFillStar color='#ffc107' />}
|
</span>
|
||||||
</span>
|
<LinkItemURL url={url} />
|
||||||
<LinkItemURL url={url} />
|
</ExternalLink>
|
||||||
</LinkTag>
|
<div className={styles['controls']}>
|
||||||
<div className={styles['controls']}>
|
<FavoriteItem
|
||||||
<FavoriteItem
|
isFavorite={favorite}
|
||||||
isFavorite={favorite}
|
onClick={onFavorite}
|
||||||
onClick={onFavorite}
|
/>
|
||||||
/>
|
<EditItem
|
||||||
<EditItem
|
type='link'
|
||||||
type='link'
|
id={id}
|
||||||
id={id}
|
/>
|
||||||
/>
|
<RemoveItem
|
||||||
<RemoveItem
|
type='link'
|
||||||
type='link'
|
id={id}
|
||||||
id={id}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{description && (
|
||||||
|
<div className={styles['link-description']}>{description}</div>
|
||||||
|
)}
|
||||||
</motion.li>
|
</motion.li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,8 +91,18 @@
|
|||||||
border: 1px solid $lightest-grey;
|
border: 1px solid $lightest-grey;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
outline: 3px solid transparent;
|
outline: 3px solid transparent;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
& .link-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .link-description {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
color: $black;
|
||||||
|
font-size: 0.8em;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
@@ -109,7 +119,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.link > a {
|
.link-header > a {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: calc(100% - 125px); // TODO: faut fix ça, c'est pas beau
|
max-width: calc(100% - 125px); // TODO: faut fix ça, c'est pas beau
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const LinkBodySchema = object({
|
|||||||
.trim()
|
.trim()
|
||||||
.required('Link name is required')
|
.required('Link name is required')
|
||||||
.max(128, 'Link name is too long'),
|
.max(128, 'Link name is too long'),
|
||||||
|
description: string().trim().max(255, 'Link description is too long'),
|
||||||
url: string()
|
url: string()
|
||||||
.trim()
|
.trim()
|
||||||
.required('URl is required')
|
.required('URl is required')
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ export default apiHandler({
|
|||||||
|
|
||||||
async function editLink({ req, res, user }) {
|
async function editLink({ req, res, user }) {
|
||||||
const { lid } = await LinkQuerySchema.validate(req.query);
|
const { lid } = await LinkQuerySchema.validate(req.query);
|
||||||
const { name, url, favorite, categoryId } = await LinkBodySchema.validate(
|
const { name, url, description, favorite, categoryId } =
|
||||||
req.body,
|
await LinkBodySchema.validate(req.body);
|
||||||
);
|
|
||||||
|
|
||||||
const link = await getUserLink(user, lid);
|
const link = await getUserLink(user, lid);
|
||||||
if (!link) {
|
if (!link) {
|
||||||
@@ -22,6 +21,7 @@ async function editLink({ req, res, user }) {
|
|||||||
if (
|
if (
|
||||||
link.name === name &&
|
link.name === name &&
|
||||||
link.url === url &&
|
link.url === url &&
|
||||||
|
link.description === description &&
|
||||||
link.favorite === favorite &&
|
link.favorite === favorite &&
|
||||||
link.categoryId === categoryId
|
link.categoryId === categoryId
|
||||||
) {
|
) {
|
||||||
@@ -33,6 +33,7 @@ async function editLink({ req, res, user }) {
|
|||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
url,
|
url,
|
||||||
|
description,
|
||||||
favorite,
|
favorite,
|
||||||
categoryId,
|
categoryId,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ export default apiHandler({
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function createLink({ req, res, user }) {
|
async function createLink({ req, res, user }) {
|
||||||
const { name, url, favorite, categoryId } = await LinkBodySchema.validate(
|
const { name, url, description, favorite, categoryId } =
|
||||||
req.body,
|
await LinkBodySchema.validate(req.body);
|
||||||
);
|
|
||||||
|
|
||||||
const link = await getUserLinkByName(user, name, categoryId);
|
const link = await getUserLinkByName(user, name, categoryId);
|
||||||
if (link) {
|
if (link) {
|
||||||
@@ -28,6 +27,7 @@ async function createLink({ req, res, user }) {
|
|||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
url,
|
url,
|
||||||
|
description,
|
||||||
categoryId,
|
categoryId,
|
||||||
favorite,
|
favorite,
|
||||||
authorId: user.id,
|
authorId: user.id,
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export default function PageCreateLink({
|
|||||||
|
|
||||||
const [name, setName] = useState<LinkWithCategory['name']>('');
|
const [name, setName] = useState<LinkWithCategory['name']>('');
|
||||||
const [url, setUrl] = useState<LinkWithCategory['url']>('');
|
const [url, setUrl] = useState<LinkWithCategory['url']>('');
|
||||||
|
const [description, setDescription] =
|
||||||
|
useState<LinkWithCategory['description']>('');
|
||||||
const [favorite, setFavorite] = useState<LinkWithCategory['favorite']>(false);
|
const [favorite, setFavorite] = useState<LinkWithCategory['favorite']>(false);
|
||||||
const [categoryId, setCategoryId] = useState<
|
const [categoryId, setCategoryId] = useState<
|
||||||
LinkWithCategory['category']['id']
|
LinkWithCategory['category']['id']
|
||||||
@@ -54,7 +56,7 @@ export default function PageCreateLink({
|
|||||||
makeRequest({
|
makeRequest({
|
||||||
url: PATHS.API.LINK,
|
url: PATHS.API.LINK,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { name, url, favorite, categoryId },
|
body: { name, url, description, favorite, categoryId },
|
||||||
})
|
})
|
||||||
.then((data) =>
|
.then((data) =>
|
||||||
router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`),
|
router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`),
|
||||||
@@ -75,19 +77,29 @@ export default function PageCreateLink({
|
|||||||
<TextBox
|
<TextBox
|
||||||
name='name'
|
name='name'
|
||||||
label={t('common:link.name')}
|
label={t('common:link.name')}
|
||||||
onChangeCallback={(value) => setName(value)}
|
onChangeCallback={setName}
|
||||||
value={name}
|
value={name}
|
||||||
fieldClass={styles['input-field']}
|
fieldClass={styles['input-field']}
|
||||||
placeholder={t('common:link.name')}
|
placeholder={t('common:link.name')}
|
||||||
innerRef={autoFocusRef}
|
innerRef={autoFocusRef}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<TextBox
|
<TextBox
|
||||||
name='url'
|
name='url'
|
||||||
label={t('common:link.link')}
|
label={t('common:link.link')}
|
||||||
onChangeCallback={(value) => setUrl(value)}
|
onChangeCallback={setUrl}
|
||||||
value={url}
|
value={url}
|
||||||
fieldClass={styles['input-field']}
|
fieldClass={styles['input-field']}
|
||||||
placeholder='https://www.example.com/'
|
placeholder='https://www.example.com/'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextBox
|
||||||
|
name='description'
|
||||||
|
label={t('common:link.description')}
|
||||||
|
onChangeCallback={setDescription}
|
||||||
|
value={description}
|
||||||
|
fieldClass={styles['input-field']}
|
||||||
|
placeholder={t('common:link.description')}
|
||||||
/>
|
/>
|
||||||
<Selector
|
<Selector
|
||||||
name='category'
|
name='category'
|
||||||
@@ -98,11 +110,12 @@ export default function PageCreateLink({
|
|||||||
label: name,
|
label: name,
|
||||||
value: id,
|
value: id,
|
||||||
}))}
|
}))}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name='favorite'
|
name='favorite'
|
||||||
isChecked={favorite}
|
isChecked={favorite}
|
||||||
onChangeCallback={(value) => setFavorite(value)}
|
onChangeCallback={setFavorite}
|
||||||
label={t('common:favorite')}
|
label={t('common:favorite')}
|
||||||
/>
|
/>
|
||||||
</FormLayout>
|
</FormLayout>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default function PageEditLink({
|
|||||||
|
|
||||||
const [name, setName] = useState<string>(link.name);
|
const [name, setName] = useState<string>(link.name);
|
||||||
const [url, setUrl] = useState<string>(link.url);
|
const [url, setUrl] = useState<string>(link.url);
|
||||||
|
const [description, setDescription] = useState<string>(link.description);
|
||||||
const [favorite, setFavorite] = useState<boolean>(link.favorite);
|
const [favorite, setFavorite] = useState<boolean>(link.favorite);
|
||||||
const [categoryId, setCategoryId] = useState<number | null>(
|
const [categoryId, setCategoryId] = useState<number | null>(
|
||||||
link.category?.id || null,
|
link.category?.id || null,
|
||||||
@@ -42,6 +43,7 @@ export default function PageEditLink({
|
|||||||
const isFormEdited =
|
const isFormEdited =
|
||||||
name !== link.name ||
|
name !== link.name ||
|
||||||
url !== link.url ||
|
url !== link.url ||
|
||||||
|
description !== link.description ||
|
||||||
favorite !== link.favorite ||
|
favorite !== link.favorite ||
|
||||||
categoryId !== link.category.id;
|
categoryId !== link.category.id;
|
||||||
const isFormValid =
|
const isFormValid =
|
||||||
@@ -50,17 +52,7 @@ export default function PageEditLink({
|
|||||||
favorite !== null &&
|
favorite !== null &&
|
||||||
categoryId !== null;
|
categoryId !== null;
|
||||||
return isFormEdited && isFormValid && !submitted;
|
return isFormEdited && isFormValid && !submitted;
|
||||||
}, [
|
}, [categoryId, description, favorite, link, name, submitted, url]);
|
||||||
categoryId,
|
|
||||||
favorite,
|
|
||||||
link.category.id,
|
|
||||||
link.favorite,
|
|
||||||
link.name,
|
|
||||||
link.url,
|
|
||||||
name,
|
|
||||||
submitted,
|
|
||||||
url,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -70,7 +62,7 @@ export default function PageEditLink({
|
|||||||
makeRequest({
|
makeRequest({
|
||||||
url: `${PATHS.API.LINK}/${link.id}`,
|
url: `${PATHS.API.LINK}/${link.id}`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: { name, url, favorite, categoryId },
|
body: { name, url, description, favorite, categoryId },
|
||||||
})
|
})
|
||||||
.then((data) =>
|
.then((data) =>
|
||||||
router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`),
|
router.push(`${PATHS.HOME}?categoryId=${data?.categoryId}`),
|
||||||
@@ -90,19 +82,29 @@ export default function PageEditLink({
|
|||||||
<TextBox
|
<TextBox
|
||||||
name='name'
|
name='name'
|
||||||
label={t('common:link.name')}
|
label={t('common:link.name')}
|
||||||
onChangeCallback={(value) => setName(value)}
|
onChangeCallback={setName}
|
||||||
value={name}
|
value={name}
|
||||||
fieldClass={styles['input-field']}
|
fieldClass={styles['input-field']}
|
||||||
placeholder={`${t('common:link.name')} : ${link.name}`}
|
placeholder={`${t('common:link.name')} : ${link.name}`}
|
||||||
innerRef={autoFocusRef}
|
innerRef={autoFocusRef}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<TextBox
|
<TextBox
|
||||||
name='url'
|
name='url'
|
||||||
label={t('common:link.link')}
|
label={t('common:link.link')}
|
||||||
onChangeCallback={(value) => setUrl(value)}
|
onChangeCallback={setUrl}
|
||||||
value={url}
|
value={url}
|
||||||
fieldClass={styles['input-field']}
|
fieldClass={styles['input-field']}
|
||||||
placeholder='https://example.com/'
|
placeholder='https://example.com/'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextBox
|
||||||
|
name='description'
|
||||||
|
label={t('common:link.description')}
|
||||||
|
onChangeCallback={setDescription}
|
||||||
|
value={description}
|
||||||
|
fieldClass={styles['input-field']}
|
||||||
|
placeholder={`${t('common:link.description')} : ${link.description}`}
|
||||||
/>
|
/>
|
||||||
<Selector
|
<Selector
|
||||||
name='category'
|
name='category'
|
||||||
@@ -113,11 +115,12 @@ export default function PageEditLink({
|
|||||||
label: name,
|
label: name,
|
||||||
value: id,
|
value: id,
|
||||||
}))}
|
}))}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name='favorite'
|
name='favorite'
|
||||||
isChecked={favorite}
|
isChecked={favorite}
|
||||||
onChangeCallback={(value) => setFavorite(value)}
|
onChangeCallback={setFavorite}
|
||||||
label={t('common:favorite')}
|
label={t('common:favorite')}
|
||||||
/>
|
/>
|
||||||
</FormLayout>
|
</FormLayout>
|
||||||
|
|||||||
@@ -69,6 +69,13 @@ export default function PageRemoveLink({
|
|||||||
fieldClass={styles['input-field']}
|
fieldClass={styles['input-field']}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
/>
|
/>
|
||||||
|
<TextBox
|
||||||
|
name='description'
|
||||||
|
label={t('common:link.description')}
|
||||||
|
value={link.description}
|
||||||
|
fieldClass={styles['input-field']}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
<TextBox
|
<TextBox
|
||||||
name='category'
|
name='category'
|
||||||
label={t('common:category.category')}
|
label={t('common:category.category')}
|
||||||
|
|||||||
Reference in New Issue
Block a user