diff --git a/bun.lockb b/bun.lockb index 31f7911..e267079 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index f943043..61b48a7 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,12 @@ "@ckeditor/ckeditor5-react": "^9.3.0", "@tanstack/react-form": "^0.33.0", "@tanstack/react-router": "^1.62.0", + "@tanstack/yup-form-adapter": "^0.33.0", "appwrite": "^16.0.2", "classnames": "^2.5.1", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "yup": "^1.4.0" }, "devDependencies": { "@eslint/js": "^9.11.1", diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 0d3b5a9..3574d56 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -16,25 +16,28 @@ const Button: React.FC = ({ children, color = ButtonColor.Primary, fullWidth = false, + disabled = false, ...props }) => { const buttonClass = classNames( - 'flex justify-center rounded-md px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2', + 'flex justify-center rounded-md px-3 py-1.5 text-sm font-semibold leading-6 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2', { 'bg-indigo-600 hover:bg-indigo-500 focus-visible:outline-indigo-600': - color === ButtonColor.Primary, + color === ButtonColor.Primary && !disabled, 'bg-gray-600 hover:bg-gray-500 focus-visible:outline-gray-600': - color === ButtonColor.Secondary, + color === ButtonColor.Secondary && !disabled, 'bg-red-600 hover:bg-red-500 focus-visible:outline-red-600': - color === ButtonColor.Error, + color === ButtonColor.Error && !disabled, 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600': - color === ButtonColor.Success, + color === ButtonColor.Success && !disabled, + 'bg-gray-300 text-gray-500 cursor-not-allowed dark:bg-nero-600': disabled, + 'text-white': !disabled, 'w-full': fullWidth, }, ); return ( - ); diff --git a/src/components/Input.tsx b/src/components/Input.tsx index d7a30db..f4e7ee2 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -1,11 +1,15 @@ +import { ValidationError } from '@tanstack/react-form'; + const Input = ({ label, + errors, ...props }: React.DetailedHTMLProps< React.InputHTMLAttributes, HTMLInputElement > & { label?: string; + errors?: ValidationError[]; }) => { return (
@@ -18,6 +22,11 @@ const Input = ({ {...props} />
+ {errors?.map((error, key) => ( + + {error} + + ))} ); }; diff --git a/src/components/forms/CreateEstimationSessionForm.tsx b/src/components/forms/CreateEstimationSessionForm.tsx index 5ebc6fe..c6857c5 100644 --- a/src/components/forms/CreateEstimationSessionForm.tsx +++ b/src/components/forms/CreateEstimationSessionForm.tsx @@ -2,6 +2,8 @@ import { useForm } from '@tanstack/react-form'; import { useEstimationsList } from '../../lib/context/estimationsList'; import Input from '../Input'; import Button from '../Button'; +import { yupValidator } from '@tanstack/yup-form-adapter'; +import * as yup from 'yup'; interface CreateEstimationSessionFormProps { onCreated: () => void; @@ -21,6 +23,12 @@ const CreateEstimationSessionForm: React.FC< }); onCreated(); }, + validators: { + onChange: yup.object({ + name: yup.string().label('Name').max(200).required(), + }), + }, + validatorAdapter: yupValidator(), }); return ( @@ -36,21 +44,25 @@ const CreateEstimationSessionForm: React.FC< form.handleSubmit(); }} > - ( + + {(field) => ( field.handleChange(e.target.value)} + errors={field.state.meta.errors} /> )} - /> - + + [state.canSubmit]}> + {([canSubmit]) => ( + + )} + ); diff --git a/src/pages/Estimation/Estimation.tsx b/src/pages/Estimation/Estimation.tsx index b89c891..4f56c1c 100644 --- a/src/pages/Estimation/Estimation.tsx +++ b/src/pages/Estimation/Estimation.tsx @@ -16,7 +16,7 @@ const route = getRouteApi('/_authenticated/estimate/session/$sessionId'); const Estimation: React.FC = () => { const { sessionId } = route.useParams(); const estimationState = useEstimationContext(); - const [isDrawerOpen, setDrawerOpen] = useState(false); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [editingTicketId, setEditingTicketId] = useState(''); useEffect(() => estimationState?.setSessionId(sessionId), [sessionId]); @@ -32,10 +32,10 @@ const Estimation: React.FC = () => { createTicket, updateTicket, currentSessionData: { - tickets: tickets, + tickets, sessionState: { votesRevealed: revealed, - votes: votes, + votes, currentPlayerVote, currentTicket, }, @@ -49,10 +49,10 @@ const Estimation: React.FC = () => { className="w-64 overflow-y-scroll bg-gray-50 p-4 dark:bg-nero-800" tickets={tickets} onSelectTicket={(ticket) => setActiveTicket(ticket.id)} - onAddTicket={() => setDrawerOpen(true)} + onAddTicket={() => setIsDrawerOpen(true)} onEditTicket={(ticketId) => { setEditingTicketId(ticketId); - setDrawerOpen(true); + setIsDrawerOpen(true); }} /> @@ -95,7 +95,7 @@ const Estimation: React.FC = () => { { - setDrawerOpen(false); + setIsDrawerOpen(false); setEditingTicketId(''); }} > @@ -110,7 +110,7 @@ const Estimation: React.FC = () => { } else { await createTicket(ticket); } - setDrawerOpen(false); + setIsDrawerOpen(false); }} /> diff --git a/src/pages/Estimation/components/EditTicketForm.tsx b/src/pages/Estimation/components/EditTicketForm.tsx index adf7b8b..153b96d 100644 --- a/src/pages/Estimation/components/EditTicketForm.tsx +++ b/src/pages/Estimation/components/EditTicketForm.tsx @@ -1,6 +1,8 @@ import { useForm } from '@tanstack/react-form'; import { Button, Input } from '../../../components'; import RichEditor from '../../../components/RichEditor'; +import { yupValidator } from '@tanstack/yup-form-adapter'; +import * as yup from 'yup'; interface EditTicketFormData { name: string; @@ -16,7 +18,7 @@ const EditTicketForm: React.FC = ({ initialData, onSave, }) => { - const form = useForm({ + const form = useForm({ defaultValues: initialData ?? { name: '', content: '', @@ -24,6 +26,12 @@ const EditTicketForm: React.FC = ({ onSubmit: async ({ value }) => { await onSave(value); }, + validators: { + onChange: yup.object({ + name: yup.string().label('Name').max(200).required(), + }), + }, + validatorAdapter: yupValidator(), }); return ( @@ -37,9 +45,8 @@ const EditTicketForm: React.FC = ({ form.handleSubmit(); }} > - ( + + {(field) => ( = ({ value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} + errors={field.state.meta.errors} /> )} - /> - ( + + + {(field) => ( field.handleChange(value)} /> )} - /> - + + [state.canSubmit]}> + {([canSubmit]) => ( + + )} + ); diff --git a/src/pages/Estimation/components/PlayerList.tsx b/src/pages/Estimation/components/PlayerList.tsx index 79dc6f4..a95698d 100644 --- a/src/pages/Estimation/components/PlayerList.tsx +++ b/src/pages/Estimation/components/PlayerList.tsx @@ -21,9 +21,9 @@ const PlayerList: React.FC = ({
    {players.length > 0 ? ( - players.map((player, index) => ( + players.map((player) => (
  • {player.name} diff --git a/src/pages/Home.css b/src/pages/Home.css deleted file mode 100644 index 1d6fa2b..0000000 --- a/src/pages/Home.css +++ /dev/null @@ -1,27 +0,0 @@ -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 0d64590..4dcf6ae 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,4 +1,3 @@ -import './Home.css'; import { getRouteApi, Link } from '@tanstack/react-router'; import { useUser } from '../lib/context/user'; import { useEstimationsList } from '../lib/context/estimationsList'; @@ -16,7 +15,7 @@ function Home() { const user = useUser(); const navigate = route.useNavigate(); const estimationsList = useEstimationsList(); - const [isDrawerOpen, setDrawerOpen] = useState(false); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); return ( <> @@ -47,12 +46,12 @@ function Home() { }} /> )} - onAddItem={() => setDrawerOpen(true)} + onAddItem={() => setIsDrawerOpen(true)} /> - setDrawerOpen(false)}> - setDrawerOpen(false)} /> + setIsDrawerOpen(false)}> + setIsDrawerOpen(false)} /> ); diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index d43987d..f7deafd 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,6 +1,8 @@ import { useForm } from '@tanstack/react-form'; import { useUser } from '../lib/context/user'; import { Button, ButtonColor, Input } from '../components'; +import { yupValidator } from '@tanstack/yup-form-adapter'; +import * as yup from 'yup'; const Login = () => { const user = useUser(); @@ -12,96 +14,110 @@ const Login = () => { onSubmit: async ({ value }) => { console.log({ value }); }, + validators: { + onChange: yup.object({ + email: yup.string().label('Email').email().required(), + password: yup.string().label('Password').min(8).max(256).required(), + }), + }, + validatorAdapter: yupValidator(), }); return ( - <> -
    -
    -

    - Sign in to your account -

    -
    - -
    -
    - ( - field.handleChange(e.target.value)} - /> - )} - /> - - ( - field.handleChange(e.target.value)} - /> - )} - /> - -
    -
    - - - -
    -
    - - -

    - Don't want to create an account?{' '} - user.loginAsGuest()} - > - Sign in as a guest - -

    -
    +
    +
    +

    + Sign in to your account +

    - + +
    +
    + + {(field) => ( + field.handleChange(e.target.value)} + errors={field.state.meta.errors} + /> + )} + + + + {(field) => ( + field.handleChange(e.target.value)} + errors={field.state.meta.errors} + /> + )} + + + [state.canSubmit, state.isSubmitting]} + > + {([canSubmit, isSubmitting]) => ( +
    + {/* TODO: Add loader when submitting */} +
    + + + +
    +
    + )} +
    +
    + +

    + Don't want to create an account?{' '} + user.loginAsGuest()} + > + Sign in as a guest + +

    +
    +
    ); }; diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 364c91a..9217bcc 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -1,6 +1,8 @@ import { useForm } from '@tanstack/react-form'; import { Button, Input } from '../components'; import { useUser } from '../lib/context/user'; +import { yupValidator } from '@tanstack/yup-form-adapter'; +import * as yup from 'yup'; const Profile = () => { const user = useUser(); @@ -12,6 +14,12 @@ const Profile = () => { await user.updateUsername(value.name); updateUsernameForm.reset(); }, + validators: { + onChange: yup.object({ + name: yup.string().label('Name').max(128).required(), + }), + }, + validatorAdapter: yupValidator(), }); return ( @@ -28,21 +36,27 @@ const Profile = () => { updateUsernameForm.handleSubmit(); }} > - ( - field.handleChange(e.target.value)} - /> + + {(field) => { + return ( + field.handleChange(e.target.value)} + errors={field.state.meta.errors} + /> + ); + }} + + [state.canSubmit]}> + {([canSubmit]) => ( + )} - /> - +