mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-10 15:35:35 +00:00
feat: create formlayout and create collection form
This commit is contained in:
27
inertia/components/common/form/_button.tsx
Normal file
27
inertia/components/common/form/_button.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const Button = styled.button(({ theme }) => ({
|
||||||
|
cursor: 'pointer',
|
||||||
|
width: '100%',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
fontSize: '14px',
|
||||||
|
color: theme.colors.white,
|
||||||
|
background: theme.colors.primary,
|
||||||
|
padding: '0.75em',
|
||||||
|
border: `1px solid ${theme.colors.primary}`,
|
||||||
|
borderRadius: theme.border.radius,
|
||||||
|
transition: theme.transition.delay,
|
||||||
|
|
||||||
|
'&:disabled': {
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
opacity: '0.75',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:not(:disabled):hover': {
|
||||||
|
boxShadow: `${theme.colors.darkBlue} 0 0 3px 1px`,
|
||||||
|
background: theme.colors.darkBlue,
|
||||||
|
color: theme.colors.white,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default Button;
|
||||||
10
inertia/components/common/form/_form.tsx
Normal file
10
inertia/components/common/form/_form.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const Form = styled.form({
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.5em',
|
||||||
|
flexDirection: 'column',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Form;
|
||||||
25
inertia/components/common/form/_form_field.tsx
Normal file
25
inertia/components/common/form/_form_field.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const FormField = styled('div', {
|
||||||
|
shouldForwardProp: (propName) => propName !== 'required',
|
||||||
|
})<{ required?: boolean }>(({ required, theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.25em',
|
||||||
|
flexDirection: 'column',
|
||||||
|
|
||||||
|
'& label': {
|
||||||
|
position: 'relative',
|
||||||
|
userSelect: 'none',
|
||||||
|
width: 'fit-content',
|
||||||
|
},
|
||||||
|
|
||||||
|
'& label::after': {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
right: '-0.75em',
|
||||||
|
color: theme.colors.red,
|
||||||
|
content: (required ? '"*"' : '""') as any,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default FormField;
|
||||||
23
inertia/components/common/form/_input.tsx
Normal file
23
inertia/components/common/form/_input.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const Input = styled.input(({ theme }) => ({
|
||||||
|
width: '100%',
|
||||||
|
color: theme.colors.font,
|
||||||
|
backgroundColor: theme.colors.white,
|
||||||
|
padding: '0.75em',
|
||||||
|
border: `1px solid ${theme.colors.lightestGrey}`,
|
||||||
|
borderBottom: `2px solid ${theme.colors.lightestGrey}`,
|
||||||
|
borderRadius: theme.border.radius,
|
||||||
|
transition: theme.transition.delay,
|
||||||
|
|
||||||
|
'&:focus': {
|
||||||
|
borderBottom: `2px solid ${theme.colors.primary}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
'&::placeholder': {
|
||||||
|
fontStyle: 'italic',
|
||||||
|
color: theme.colors.lightestGrey,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default Input;
|
||||||
44
inertia/components/common/form/textbox.tsx
Normal file
44
inertia/components/common/form/textbox.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { ChangeEvent, InputHTMLAttributes, useState } from 'react';
|
||||||
|
import FormField from '~/components/common/form/_form_field';
|
||||||
|
import Input from '~/components/common/form/_input';
|
||||||
|
|
||||||
|
interface InputProps
|
||||||
|
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
onChange?: (name: string, value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TextBox({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
value = '',
|
||||||
|
onChange,
|
||||||
|
required = false,
|
||||||
|
...props
|
||||||
|
}: InputProps): JSX.Element {
|
||||||
|
const [inputValue, setInputValue] = useState<string>(value);
|
||||||
|
|
||||||
|
function _onChange({ target }: ChangeEvent<HTMLInputElement>) {
|
||||||
|
setInputValue(target.value);
|
||||||
|
if (onChange) {
|
||||||
|
onChange(target.name, target.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField required={required}>
|
||||||
|
<label htmlFor={name} title={label}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
{...props}
|
||||||
|
name={name}
|
||||||
|
onChange={_onChange}
|
||||||
|
value={inputValue}
|
||||||
|
placeholder={props.placeholder ?? 'Type something...'}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
inertia/components/layouts/form_layout.tsx
Normal file
55
inertia/components/layouts/form_layout.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import PATHS from '#constants/paths';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Link } from '@inertiajs/react';
|
||||||
|
import { FormEvent, ReactNode } from 'react';
|
||||||
|
import Button from '~/components/common/form/_button';
|
||||||
|
import Form from '~/components/common/form/_form';
|
||||||
|
import BaseLayout from './_base_layout';
|
||||||
|
|
||||||
|
const FormLayoutStyle = styled.div(({ theme }) => ({
|
||||||
|
height: 'fit-content',
|
||||||
|
width: theme.media.mobile,
|
||||||
|
maxWidth: '100%',
|
||||||
|
marginTop: '10em',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.75em',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface FormLayoutProps {
|
||||||
|
title: string;
|
||||||
|
children: ReactNode;
|
||||||
|
|
||||||
|
canSubmit: boolean;
|
||||||
|
handleSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
||||||
|
textSubmitButton?: string;
|
||||||
|
|
||||||
|
disableHomeLink?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormLayout = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
canSubmit,
|
||||||
|
handleSubmit,
|
||||||
|
textSubmitButton = 'Confirm',
|
||||||
|
disableHomeLink = false,
|
||||||
|
}: FormLayoutProps) => (
|
||||||
|
<BaseLayout>
|
||||||
|
<FormLayoutStyle>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
{children}
|
||||||
|
<Button type="submit" disabled={!canSubmit}>
|
||||||
|
{textSubmitButton}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
{!disableHomeLink && (
|
||||||
|
// <Link href={categoryId ? `/?categoryId=${categoryId}` : '/'}>{t('common:back-home')}</Link>
|
||||||
|
<Link href={PATHS.APP}>← Revenir à l'accueil</Link>
|
||||||
|
)}
|
||||||
|
</FormLayoutStyle>
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default FormLayout;
|
||||||
@@ -17,13 +17,15 @@ const Nav = styled.nav({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
});
|
});
|
||||||
|
|
||||||
const NavList = styled(UnstyledList)<NavbarListDirection>(({ theme, right }) => ({
|
const NavList = styled(UnstyledList)<NavbarListDirection>(
|
||||||
|
({ theme, right }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
gap: '1.5em',
|
gap: '1.5em',
|
||||||
justifyContent: right ? 'flex-end' : 'flex-start',
|
justifyContent: right ? 'flex-end' : 'flex-start',
|
||||||
transition: theme.transition.delay,
|
transition: theme.transition.delay,
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const UserCard = styled.div({
|
const UserCard = styled.div({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -53,14 +55,23 @@ export default function Navbar() {
|
|||||||
<ExternalLink href={PATHS.REPO_GITHUB}>GitHub</ExternalLink>
|
<ExternalLink href={PATHS.REPO_GITHUB}>GitHub</ExternalLink>
|
||||||
</li>
|
</li>
|
||||||
{isAuthenticated && !!user ? (
|
{isAuthenticated && !!user ? (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<Link href={PATHS.APP}>Dashboard</Link>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href={PATHS.AUTH.LOGOUT}>
|
<a href={PATHS.AUTH.LOGOUT}>
|
||||||
<UserCard>
|
<UserCard>
|
||||||
<RoundedImage src={user.avatarUrl} width={22} referrerPolicy="no-referrer" />
|
<RoundedImage
|
||||||
|
src={user.avatarUrl}
|
||||||
|
width={22}
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
/>
|
||||||
{user.nickName}
|
{user.nickName}
|
||||||
</UserCard>
|
</UserCard>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<li>
|
<li>
|
||||||
<Link href={PATHS.AUTH.LOGIN}>Login</Link>
|
<Link href={PATHS.AUTH.LOGIN}>Login</Link>
|
||||||
|
|||||||
9
inertia/pages/app.tsx
Normal file
9
inertia/pages/app.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Link } from '@inertiajs/react';
|
||||||
|
|
||||||
|
export default function AppPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Link href="/collections/create">Add collection</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
61
inertia/pages/collection/create.tsx
Normal file
61
inertia/pages/collection/create.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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 FormLayout from '~/components/layouts/form_layout';
|
||||||
|
import { Visibility } from '../../../app/enums/visibility';
|
||||||
|
|
||||||
|
export default function CreateCollectionPage() {
|
||||||
|
const { data, setData, post, processing, errors } = useForm({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
visibility: Visibility.PRIVATE,
|
||||||
|
});
|
||||||
|
const isFormDisabled = useMemo(
|
||||||
|
() => processing || data.name.length === 0,
|
||||||
|
[processing, data]
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleOnCheck({ target }: ChangeEvent<HTMLInputElement>) {
|
||||||
|
setData(
|
||||||
|
'visibility',
|
||||||
|
target.checked ? Visibility.PUBLIC : Visibility.PRIVATE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault();
|
||||||
|
post('/collections');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormLayout
|
||||||
|
title="Create a collection"
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
canSubmit={!isFormDisabled}
|
||||||
|
>
|
||||||
|
<TextBox
|
||||||
|
label="Collection name"
|
||||||
|
placeholder="Collection name"
|
||||||
|
name="name"
|
||||||
|
onChange={setData}
|
||||||
|
value={data.name}
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{errors.name && <div>{errors.name}</div>}
|
||||||
|
<TextBox
|
||||||
|
label="Collection description"
|
||||||
|
placeholder="Collection description"
|
||||||
|
name="description"
|
||||||
|
onChange={setData}
|
||||||
|
value={data.name}
|
||||||
|
/>
|
||||||
|
{errors.description && <div>{errors.description}</div>}
|
||||||
|
<FormField>
|
||||||
|
<label htmlFor="visibility">Public</label>
|
||||||
|
<input type="checkbox" onChange={handleOnCheck} id="visibility" />
|
||||||
|
</FormField>
|
||||||
|
</FormLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,24 +1,7 @@
|
|||||||
import { InferPageProps } from '@adonisjs/inertia/types';
|
import type { InferPageProps } from '@adonisjs/inertia/types';
|
||||||
import ContentLayout from '~/components/layouts/content_layout';
|
import ContentLayout from '~/components/layouts/content_layout';
|
||||||
import useUser from '~/hooks/use_user';
|
|
||||||
import type AppsController from '../../app/controllers/apps_controller';
|
import type AppsController from '../../app/controllers/apps_controller';
|
||||||
|
|
||||||
export default function Home(_: InferPageProps<AppsController, 'index'>) {
|
export default function Home(_: InferPageProps<AppsController, 'index'>) {
|
||||||
const { isAuthenticated, user } = useUser();
|
return <ContentLayout>blablabla welcome to MyLinks</ContentLayout>;
|
||||||
|
|
||||||
return (
|
|
||||||
<ContentLayout>
|
|
||||||
<div className="container">
|
|
||||||
<div className="title">AdonisJS x Inertia x React</div>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
Learn more about AdonisJS and Inertia.js by visiting the{' '}
|
|
||||||
<a href="https://docs.adonisjs.com/guides/inertia">AdonisJS documentation</a>.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>{isAuthenticated ? 'Authenticated' : 'Not authenticated'}</span>
|
|
||||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
|
||||||
</div>
|
|
||||||
</ContentLayout>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
|
import { theme } from '~/styles/theme';
|
||||||
|
|
||||||
export const cssReset = css({
|
export const cssReset = css({
|
||||||
'*': {
|
'*': {
|
||||||
@@ -17,20 +18,20 @@ export const cssReset = css({
|
|||||||
border: 0,
|
border: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
'a': {
|
a: {
|
||||||
width: 'fit-content',
|
width: 'fit-content',
|
||||||
color: '#3f88c5',
|
color: '#3f88c5',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
borderBottom: '1px solid transparent',
|
borderBottom: '1px solid transparent',
|
||||||
},
|
},
|
||||||
|
|
||||||
'b': {
|
b: {
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
letterSpacing: '0.5px',
|
letterSpacing: '0.5px',
|
||||||
},
|
},
|
||||||
|
|
||||||
'h1, h2, h3, h4, h5, h6': {
|
'h1, h2, h3, h4, h5, h6': {
|
||||||
fontWeight: '400',
|
fontWeight: '500',
|
||||||
margin: '1em 0',
|
color: theme.colors.primary,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,14 +5,28 @@ const black = '#333';
|
|||||||
const darkBlack = '#111';
|
const darkBlack = '#111';
|
||||||
|
|
||||||
const white = '#fff';
|
const white = '#fff';
|
||||||
const gray = '#7c7c7c';
|
|
||||||
|
|
||||||
|
const lightestGrey = '#dadce0';
|
||||||
|
const lightGrey = '#f0eef6';
|
||||||
|
const grey = '#aaa';
|
||||||
|
const darkGrey = '#4b5563';
|
||||||
|
|
||||||
|
const lightestBlue = '#d3e8fa';
|
||||||
|
const lightBlue = '#82c5fede';
|
||||||
const blue = '#3f88c5';
|
const blue = '#3f88c5';
|
||||||
|
const darkBlue = '#005aa5';
|
||||||
|
const darkestBlue = '#1f2937';
|
||||||
|
|
||||||
|
const lightRed = '#ffbabab9';
|
||||||
|
const red = '#d8000c';
|
||||||
|
|
||||||
|
const lightGreen = '#c1ffbab9';
|
||||||
|
const green = 'green';
|
||||||
|
|
||||||
export const theme: Theme = {
|
export const theme: Theme = {
|
||||||
colors: {
|
colors: {
|
||||||
font: black,
|
font: black,
|
||||||
background: white,
|
background: lightGrey,
|
||||||
primary: blue,
|
primary: blue,
|
||||||
|
|
||||||
lightBlack,
|
lightBlack,
|
||||||
@@ -20,9 +34,23 @@ export const theme: Theme = {
|
|||||||
darkBlack,
|
darkBlack,
|
||||||
|
|
||||||
white,
|
white,
|
||||||
gray,
|
|
||||||
|
|
||||||
|
lightestGrey,
|
||||||
|
lightGrey,
|
||||||
|
grey,
|
||||||
|
darkGrey,
|
||||||
|
|
||||||
|
lightestBlue,
|
||||||
|
lightBlue,
|
||||||
blue,
|
blue,
|
||||||
|
darkBlue,
|
||||||
|
darkestBlue,
|
||||||
|
|
||||||
|
lightRed,
|
||||||
|
red,
|
||||||
|
|
||||||
|
lightGreen,
|
||||||
|
green,
|
||||||
},
|
},
|
||||||
|
|
||||||
border: {
|
border: {
|
||||||
|
|||||||
16
inertia/types/emotion.d.ts
vendored
16
inertia/types/emotion.d.ts
vendored
@@ -12,9 +12,23 @@ declare module '@emotion/react' {
|
|||||||
darkBlack: string;
|
darkBlack: string;
|
||||||
|
|
||||||
white: string;
|
white: string;
|
||||||
gray: string;
|
|
||||||
|
|
||||||
|
lightestGrey: string;
|
||||||
|
lightGrey: string;
|
||||||
|
grey: string;
|
||||||
|
darkGrey: string;
|
||||||
|
|
||||||
|
lightestBlue: string;
|
||||||
|
lightBlue: string;
|
||||||
blue: string;
|
blue: string;
|
||||||
|
darkBlue: string;
|
||||||
|
darkestBlue: string;
|
||||||
|
|
||||||
|
lightRed: string;
|
||||||
|
red: string;
|
||||||
|
|
||||||
|
lightGreen: string;
|
||||||
|
green: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
border: {
|
border: {
|
||||||
|
|||||||
Reference in New Issue
Block a user