feat: add create link form

This commit is contained in:
Sonny
2024-05-09 23:14:42 +02:00
committed by Sonny
parent 2cf8c5ae02
commit 73f8c0c513
16 changed files with 244 additions and 64 deletions

View File

@@ -1,7 +1,10 @@
import styled from '@emotion/styled';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { TbLoader3 } from 'react-icons/tb';
import { TfiWorld } from 'react-icons/tfi';
import { rotate } from '~/styles/keyframes';
const IMG_LOAD_TIMEOUT = 7_500;
interface LinkFaviconProps {
url: string;
@@ -22,7 +25,7 @@ const FaviconLoader = styled.div(({ theme }) => ({
backgroundColor: theme.colors.white,
'& > *': {
animation: 'rotate 1s both reverse infinite linear',
animation: `${rotate} 1s both reverse infinite linear`,
},
}));
@@ -35,25 +38,27 @@ export default function LinkFavicon({
}: LinkFaviconProps) {
const [isFailed, setFailed] = useState<boolean>(false);
const [isLoading, setLoading] = useState<boolean>(true);
const baseUrlApi =
(process.env.NEXT_PUBLIC_SITE_URL ||
(typeof window !== 'undefined' && window?.location?.origin)) + '/api';
if (!baseUrlApi) {
console.warn('Missing API URL');
}
const setFallbackFavicon = () => setFailed(true);
const handleStopLoading = () => setLoading(false);
const handleErrorLoading = () => {
setFallbackFavicon();
handleStopLoading();
};
useEffect(() => {
if (!isLoading) return;
const id = setTimeout(() => handleErrorLoading(), IMG_LOAD_TIMEOUT);
return () => clearTimeout(id);
}, [isLoading]);
return (
<Favicon style={{ marginRight: !noMargin ? '1em' : '0' }}>
{!isFailed && baseUrlApi ? (
{!isFailed ? (
<img
src={`${baseUrlApi}/favicon?urlParam=${url}`}
onError={() => {
setFallbackFavicon();
handleStopLoading();
}}
src={`/favicon?urlParam=${url}`}
onError={handleErrorLoading}
onLoad={handleStopLoading}
height={size}
width={size}

View File

@@ -1,4 +1,4 @@
import Collection from '#models/collection';
import type Collection from '#models/collection';
export const appendCollectionId = (
url: string,
@@ -7,3 +7,15 @@ export const appendCollectionId = (
export const appendResourceId = (url: string, resourceId?: string) =>
`${url}${resourceId && `/${resourceId}`}}`;
export function isValidHttpUrl(urlParam: string) {
let url;
try {
url = new URL(urlParam);
} catch (_) {
return false;
}
return url.protocol === 'http:' || url.protocol === 'https:';
}

View File

@@ -7,7 +7,7 @@ import FormLayout from '~/components/layouts/form_layout';
import { Visibility } from '../../../app/enums/visibility';
export default function CreateCollectionPage() {
const { data, setData, post, processing, errors } = useForm({
const { data, setData, post, processing } = useForm({
name: '',
description: '',
visibility: Visibility.PRIVATE,
@@ -45,7 +45,6 @@ export default function CreateCollectionPage() {
required
autoFocus
/>
{errors.name && <div>{errors.name}</div>}
<TextBox
label="Collection description"
placeholder="Collection description"
@@ -53,10 +52,14 @@ export default function CreateCollectionPage() {
onChange={setData}
value={data.name}
/>
{errors.description && <div>{errors.description}</div>}
<FormField>
<label htmlFor="visibility">Public</label>
<input type="checkbox" onChange={handleOnCheck} id="visibility" />
<input
type="checkbox"
onChange={handleOnCheck}
value={data.visibility}
id="visibility"
/>
</FormField>
</BackToDashboard>
</FormLayout>

View File

@@ -0,0 +1,97 @@
import type Collection from '#models/collection';
import { useForm } from '@inertiajs/react';
import { ChangeEvent, FormEvent, useMemo } from 'react';
import FormField from '~/components/common/form/_form_field';
import TextBox from '~/components/common/form/textbox';
import BackToDashboard from '~/components/common/navigation/bask_to_dashboard';
import FormLayout from '~/components/layouts/form_layout';
import useSearchParam from '~/hooks/use_search_param';
import { isValidHttpUrl } from '~/lib/navigation';
export default function CreateLinkPage({
collections,
}: {
collections: Collection[];
}) {
const collectionId = useSearchParam('collectionId') ?? collections[0].id;
const { data, setData, post, processing } = useForm({
name: '',
description: '',
url: '',
favorite: false,
collectionId: collectionId,
});
const canSubmit = useMemo<boolean>(
() =>
data.name !== '' &&
isValidHttpUrl(data.url) &&
data.favorite !== null &&
data.collectionId !== null &&
!processing,
[data, processing]
);
const handleOnCheck = ({ target }: ChangeEvent<HTMLInputElement>) => {
setData('favorite', !!target.checked);
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
post('/links');
};
return (
<FormLayout
title="Create a link"
handleSubmit={handleSubmit}
canSubmit={canSubmit}
>
<BackToDashboard>
<TextBox
label="Link name"
placeholder="Link name"
name="name"
onChange={setData}
value={data.name}
required
autoFocus
/>
<TextBox
label="Link url"
placeholder="Link url"
name="url"
onChange={setData}
value={data.url}
required
/>
<TextBox
label="Link description"
placeholder="Link description"
name="description"
onChange={setData}
value={data.description}
/>
<select
onChange={({ target }) => setData('collectionId', target.value)}
defaultValue={data.collectionId}
>
{collections.map((collection) => (
<option key={collection.id} value={collection.id}>
{collection.name}
</option>
))}
</select>
<FormField required>
<label htmlFor="favorite">Favorite</label>
<input
type="checkbox"
onChange={handleOnCheck}
checked={data.favorite}
id="favorite"
/>
</FormField>
</BackToDashboard>
</FormLayout>
);
}

View File

@@ -8,3 +8,12 @@ export const fadeIn = keyframes({
opacity: 1,
},
});
export const rotate = keyframes({
to: {
transform: 'rotate(0deg)',
},
from: {
transform: 'rotate(360deg)',
},
});

View File

@@ -46,4 +46,25 @@ export const cssReset = css({
boxShadow: '0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset',
display: 'inline-block',
},
/* width */
'::-webkit-scrollbar': {
height: '0.45em',
width: '0.45em',
},
/* Track */
'::-webkit-scrollbar-track': {
borderRadius: theme.border.radius,
},
/* Handle */
'::-webkit-scrollbar-thumb': {
background: theme.colors.blue,
borderRadius: theme.border.radius,
'&:hover': {
background: theme.colors.darkBlue,
},
},
});

View File

@@ -58,7 +58,7 @@ export const theme: Theme = {
},
border: {
radius: '5px',
radius: '3px',
},
media: {