Code cleanup

This commit is contained in:
Pijus Kamandulis 2024-10-20 13:40:33 +03:00
parent 048f8e53cc
commit f0803ca6ec
31 changed files with 269 additions and 206 deletions

5
.gitignore vendored
View File

@ -11,6 +11,8 @@ node_modules
dist dist
dist-ssr dist-ssr
*.local *.local
tsconfig.app.tsbuildinfo
tsconfig.node.tsbuildinfo
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
@ -22,3 +24,6 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Test sandbox
playground

View File

@ -4,6 +4,8 @@ import { PencilIcon } from './icons';
interface CardProps extends React.PropsWithChildren { interface CardProps extends React.PropsWithChildren {
title: string; title: string;
description?: string; description?: string;
className?: string;
transparent?: boolean;
onClick?: () => void; onClick?: () => void;
onEdit?: () => void; onEdit?: () => void;
} }
@ -11,22 +13,25 @@ interface CardProps extends React.PropsWithChildren {
const Card: React.FC<CardProps> = ({ const Card: React.FC<CardProps> = ({
title, title,
description, description,
className,
transparent = false,
onClick, onClick,
onEdit, onEdit,
children, children,
}) => { }) => {
const className = classNames( const containerClassName = classNames(
className,
'p-4 border rounded-lg shadow-sm transition', 'p-4 border rounded-lg shadow-sm transition',
{ {
'hover:bg-gray-100 dark:hover:bg-nero-800 cursor-pointer': onClick, 'hover:bg-gray-100 dark:hover:bg-nero-800 cursor-pointer': onClick,
'bg-white dark:bg-nero-900': !transparent,
}, },
'border-gray-300 dark:border-nero-700', 'border-gray-300 dark:border-nero-700',
'bg-white dark:bg-nero-900',
'text-gray-800 dark:text-nero-200', 'text-gray-800 dark:text-nero-200',
); );
return ( return (
<div className={className} onClick={onClick} title={title}> <div className={containerClassName} onClick={onClick} title={title}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-lg font-bold">{title}</h3> <h3 className="text-lg font-bold">{title}</h3>
{onEdit && ( {onEdit && (

View File

@ -1,4 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import Input from './Input';
import Button from './Button';
interface CopyInputProps { interface CopyInputProps {
value: string; value: string;
@ -19,19 +21,8 @@ const CopyInput: React.FC<CopyInputProps> = ({ value }) => {
return ( return (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input <Input type="text" value={value} readOnly />
type="text" <Button onClick={handleCopy}>{copied ? 'Copied!' : 'Copy'}</Button>
value={value}
readOnly
className="w-full rounded-md border border-gray-300 bg-gray-100 px-4 py-2 text-gray-900 dark:border-gray-600 dark:bg-nero-800 dark:text-gray-100"
/>
<button
onClick={handleCopy}
className="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-indigo-500"
>
{copied ? 'Copied!' : 'Copy'}
</button>
</div> </div>
); );
}; };

View File

@ -2,13 +2,25 @@ import React, { useEffect } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import Button, { ButtonColor } from './Button'; import Button, { ButtonColor } from './Button';
export enum DrawerSize {
Small,
Medium,
Big,
}
interface DrawerProps { interface DrawerProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
children: React.ReactNode; children: React.ReactNode;
size?: DrawerSize;
} }
const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, children }) => { const Drawer: React.FC<DrawerProps> = ({
isOpen,
onClose,
children,
size = DrawerSize.Medium,
}) => {
useEffect(() => { useEffect(() => {
const handleEsc = (event: KeyboardEvent) => { const handleEsc = (event: KeyboardEvent) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
@ -23,6 +35,18 @@ const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, children }) => {
if (!isOpen) return null; if (!isOpen) return null;
const containerClassName = classNames(
'relative h-full transform space-y-6 overflow-auto bg-white p-6 shadow-lg transition-transform dark:bg-nero-900',
{
'translate-x-0': isOpen,
'translate-x-full': !isOpen,
'w-1/4': size === DrawerSize.Small,
'w-3/6': size === DrawerSize.Medium,
'w-4/6': size === DrawerSize.Big,
},
);
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-end"> <div className="fixed inset-0 z-50 flex items-center justify-end">
<div <div
@ -30,15 +54,7 @@ const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, children }) => {
onClick={onClose} onClick={onClose}
/> />
<div <div className={containerClassName}>
className={classNames(
'relative h-full w-2/3 transform space-y-6 overflow-auto bg-white p-6 shadow-lg transition-transform dark:bg-nero-900',
{
'translate-x-0': isOpen,
'translate-x-full': !isOpen,
},
)}
>
{children} {children}
<Button onClick={onClose} color={ButtonColor.Secondary} fullWidth> <Button onClick={onClose} color={ButtonColor.Secondary} fullWidth>

View File

@ -6,7 +6,7 @@ interface GridListProps<T> {
className?: string; className?: string;
addItemLabel?: string; addItemLabel?: string;
onAddItem?: () => void; onAddItem?: () => void;
itemComponent: React.ComponentType<{ item: T }>; itemComponent: React.FC<{ item: T }>;
} }
const AddItemButton = ({ const AddItemButton = ({

View File

@ -1,6 +1,6 @@
import { Link } from '@tanstack/react-router'; import { Link } from '@tanstack/react-router';
import { useState } from 'react'; import { useState } from 'react';
import { useUser } from '../lib/context/user'; import { useUser } from 'src/lib/context/user';
const Header = () => { const Header = () => {
const { current, isLoading, logout } = useUser(); const { current, isLoading, logout } = useUser();

View File

@ -14,14 +14,14 @@ const Input = ({
return ( return (
<div> <div>
{label && ( {label && (
<label className="block text-sm font-medium leading-6">{label}</label> <label className="mb-2 block text-sm font-medium leading-6">
{label}
</label>
)} )}
<div className="mt-2">
<input <input
className="w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm hover:border-slate-300 focus:border-slate-50 focus:shadow focus:outline-none" className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm hover:border-slate-300 focus:border-slate-50 focus:shadow focus:outline-none dark:border-gray-600 dark:bg-nero-800 dark:text-gray-100"
{...props} {...props}
/> />
</div>
{errors?.map((error, key) => ( {errors?.map((error, key) => (
<em key={`${error}-${key}`} role="alert"> <em key={`${error}-${key}`} role="alert">
{error} {error}

View File

@ -1,18 +1,26 @@
import Button, { ButtonColor } from './Button'; import Button, { ButtonColor } from './Button';
import Card from './Card'; import Card from './Card';
import CreateEstimationSessionForm from './forms/CreateEstimationSessionForm'; import CopyInput from './CopyInput';
import Drawer from './Drawer'; import Drawer, { DrawerSize } from './Drawer';
import GridList from './GridList'; import GridList from './GridList';
import Header from './Header';
import HtmlEmbed from './HtmlEmbed';
import Input from './Input'; import Input from './Input';
import Loader from './Loader'; import Loader, { LoaderSize } from './Loader';
import RichEditor from './RichEditor';
export { export {
Button, Button,
ButtonColor, ButtonColor,
Card, Card,
CreateEstimationSessionForm, CopyInput,
Drawer, Drawer,
DrawerSize,
GridList, GridList,
Header,
HtmlEmbed,
Input, Input,
Loader, Loader,
LoaderSize,
RichEditor,
}; };

View File

@ -17,6 +17,10 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.ck .ck-editor__main {
color: #1a1a1a;
}
a { a {
font-weight: 500; font-weight: 500;
color: #646cff; color: #646cff;

View File

@ -10,21 +10,21 @@ import {
DATABASE_ID, DATABASE_ID,
databases, databases,
ESTIMATION_SESSION_COLLECTION_ID, ESTIMATION_SESSION_COLLECTION_ID,
} from '../appwrite'; } from 'src/lib/appwrite';
import { DatabaseModels, EntityModels } from '../types';
import { useUser } from './user'; import { useUser } from './user';
import { mapDatabaseToEntity } from '../mappers/estimationSession'; import { mapDatabaseToEntity } from 'src/lib/mappers/estimationSession';
import { EditTicketRequest, CreateTicketRequest } from '../types/requestModels'; import { RequestModels, DatabaseModels, EntityModels } from 'src/lib/types';
import { EstimationSessionTicket } from '../types/entityModels';
interface EstimationContextType { interface EstimationContextType {
setSessionId: (sessionId: string) => void; setSessionId: (sessionId: string) => void;
setActiveTicket: (ticketId: string) => Promise<void>; setActiveTicket: (ticketId: string) => Promise<void>;
setRevealed: (revealed: boolean) => Promise<void>; setRevealed: (revealed: boolean) => Promise<void>;
setVote: (estimate: string) => Promise<void>; setVote: (estimate: string) => Promise<void>;
createTicket: (ticket: CreateTicketRequest) => Promise<void>; createTicket: (ticket: RequestModels.CreateTicketRequest) => Promise<void>;
createTickets: (tickets: CreateTicketRequest[]) => Promise<void>; createTickets: (
updateTicket: (ticket: EditTicketRequest) => Promise<void>; tickets: RequestModels.CreateTicketRequest[],
) => Promise<void>;
updateTicket: (ticket: RequestModels.EditTicketRequest) => Promise<void>;
currentSessionData?: EntityModels.EstimationSession; currentSessionData?: EntityModels.EstimationSession;
} }
@ -121,10 +121,13 @@ export const EstimationContextProvider = (props: PropsWithChildren) => {
}); });
}; };
const createTicket = (ticket: CreateTicketRequest) => createTickets([ticket]); const createTicket = (ticket: RequestModels.CreateTicketRequest) =>
createTickets([ticket]);
const createTickets = async (tickets: CreateTicketRequest[]) => { const createTickets = async (
const newTickets = tickets.map<EstimationSessionTicket>( tickets: RequestModels.CreateTicketRequest[],
) => {
const newTickets = tickets.map<EntityModels.EstimationSessionTicket>(
({ content, name, estimate }) => ({ ({ content, name, estimate }) => ({
id: crypto.randomUUID(), id: crypto.randomUUID(),
name, name,
@ -152,7 +155,7 @@ export const EstimationContextProvider = (props: PropsWithChildren) => {
name, name,
content, content,
estimate, estimate,
}: EditTicketRequest) => { }: RequestModels.EditTicketRequest) => {
const editedTicket = currentSessionData?.tickets.find((x) => x.id === id); const editedTicket = currentSessionData?.tickets.find((x) => x.id === id);
if (!editedTicket) { if (!editedTicket) {
return; return;

View File

@ -1,4 +1,4 @@
import { EntityModels } from '../types'; import { EntityModels } from 'src/lib/types';
import Papa from 'papaparse'; import Papa from 'papaparse';
import Showdown from 'showdown'; import Showdown from 'showdown';
@ -56,7 +56,7 @@ const parseJiraCSV = (
return data.map<EntityModels.EstimationSessionTicket>((row) => ({ return data.map<EntityModels.EstimationSessionTicket>((row) => ({
id: crypto.randomUUID(), id: crypto.randomUUID(),
name: row['Summary'], name: row['Summary'],
estimate: row['Story point estimate'] || '', estimate: row['Custom field (Story point estimate)'],
content: converter.makeHtml(row['Description'] || ''), content: converter.makeHtml(row['Description'] || ''),
})); }));
} catch (error) { } catch (error) {

View File

@ -1,2 +1,3 @@
export * as DatabaseModels from './databaseModels'; export * as DatabaseModels from './databaseModels';
export * as EntityModels from './entityModels'; export * as EntityModels from './entityModels';
export * as RequestModels from './requestModels';

View File

@ -9,16 +9,11 @@ import {
redirect, redirect,
RouterProvider, RouterProvider,
} from '@tanstack/react-router'; } from '@tanstack/react-router';
import Home from './pages/Home'; import { UserContextType, UserProvider, useUser } from 'src/lib/context/user';
import { UserContextType, UserProvider, useUser } from './lib/context/user'; import { EstimationsListContextProvider } from 'src/lib/context/estimationsList';
import Login from './pages/Login'; import { EstimationContextProvider } from 'src/lib/context/estimation';
import { EstimationsListContextProvider } from './lib/context/estimationsList'; import { Header, Loader } from 'src/components';
import { EstimationContextProvider } from './lib/context/estimation'; import { Estimation, Home, Join, Login, Profile } from 'src/pages';
import Estimation from './pages/Estimation/Estimation';
import Header from './components/Header';
import Profile from './pages/Profile';
import Join from './pages/Join';
import { Loader } from './components';
interface RouterContext { interface RouterContext {
userContext: UserContextType; userContext: UserContextType;
@ -120,7 +115,6 @@ createRoot(document.getElementById('root')!).render(
<UserProvider> <UserProvider>
<EstimationsListContextProvider> <EstimationsListContextProvider>
<EstimationContextProvider> <EstimationContextProvider>
{/* TODO: Move ctx providers to layout */}
<InnerApp /> <InnerApp />
</EstimationContextProvider> </EstimationContextProvider>
</EstimationsListContextProvider> </EstimationsListContextProvider>

View File

@ -4,11 +4,10 @@ import { getRouteApi } from '@tanstack/react-router';
import TaskSidebar from './components/TaskSidebar'; import TaskSidebar from './components/TaskSidebar';
import VoteSelection from './components/VoteSelection'; import VoteSelection from './components/VoteSelection';
import VoteList from './components/VoteList'; import VoteList from './components/VoteList';
import { Drawer, Loader } from '../../components';
import EditTicketForm from './components/EditTicketForm'; import EditTicketForm from './components/EditTicketForm';
import PlayerList from './components/PlayerList'; import PlayerList from './components/PlayerList';
import HtmlEmbed from '../../components/HtmlEmbed';
import EstimationResult from './components/EstimationResult'; import EstimationResult from './components/EstimationResult';
import { Drawer, HtmlEmbed, Loader } from 'src/components';
const fibonacciSequence = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 100]; const fibonacciSequence = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 100];

View File

@ -1,6 +1,5 @@
import { useForm } from '@tanstack/react-form'; import { useForm } from '@tanstack/react-form';
import { Button, Input } from '../../../components'; import { Button, Input, RichEditor } from 'src/components';
import RichEditor from '../../../components/RichEditor';
import { yupValidator } from '@tanstack/yup-form-adapter'; import { yupValidator } from '@tanstack/yup-form-adapter';
import * as yup from 'yup'; import * as yup from 'yup';

View File

@ -1,6 +1,6 @@
import { useForm } from '@tanstack/react-form'; import { useForm } from '@tanstack/react-form';
import { Button, ButtonColor, Input } from '../../../components'; import { Button, ButtonColor, Input } from 'src/components';
import { PlayerVote } from '../../../lib/types/entityModels'; import { PlayerVote } from 'src/lib/types/entityModels';
import { yupValidator } from '@tanstack/yup-form-adapter'; import { yupValidator } from '@tanstack/yup-form-adapter';
import * as yup from 'yup'; import * as yup from 'yup';
@ -77,7 +77,6 @@ const EstimationResult: React.FC<VoteListProps> = ({
value={field.state.value} value={field.state.value}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) => field.handleChange(e.target.value)}
errors={field.state.meta.errors}
/> />
)} )}
</form.Field> </form.Field>

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { EntityModels } from '../../../lib/types'; import { EntityModels } from 'src/lib/types';
import CopyInput from '../../../components/CopyInput'; import { CopyInput } from 'src/components';
interface PlayerListProps { interface PlayerListProps {
sessionId: string; sessionId: string;
@ -14,7 +14,7 @@ const PlayerList: React.FC<PlayerListProps> = ({
title = 'Players', title = 'Players',
}) => { }) => {
return ( return (
<div className="flex w-full max-w-sm flex-col justify-between rounded-lg bg-white p-6 shadow-lg dark:bg-nero-800"> <div className="flex w-full max-w-sm flex-col justify-between bg-white p-6 shadow-lg dark:bg-nero-800">
<h2 className="mb-4 text-xl font-semibold text-gray-900 dark:text-gray-100"> <h2 className="mb-4 text-xl font-semibold text-gray-900 dark:text-gray-100">
{title} {title}
</h2> </h2>

View File

@ -1,6 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { Button, Card, Drawer, GridList } from '../../../components'; import { Button, Card, Drawer, GridList } from 'src/components';
import { EstimationSessionTicket } from '../../../lib/types/entityModels'; import { EstimationSessionTicket } from 'src/lib/types/entityModels';
import TicketImportForm from './TicketImportForm'; import TicketImportForm from './TicketImportForm';
import { useState } from 'react'; import { useState } from 'react';

View File

@ -2,21 +2,36 @@ import { useState } from 'react';
import { import {
handleTicketFileUpload, handleTicketFileUpload,
TicketFileUploadResponse, TicketFileUploadResponse,
} from '../../../lib/parsers/ticketUpload'; } from 'src/lib/parsers/ticketUpload';
import { EstimationSessionTicket } from '../../../lib/types/entityModels'; import { EntityModels } from 'src/lib/types';
import { Button, Card, GridList, Loader } from '../../../components'; import { Button, Card, GridList, HtmlEmbed, Loader } from 'src/components';
import HtmlEmbed from '../../../components/HtmlEmbed'; import { useEstimationContext } from 'src/lib/context/estimation';
import { useEstimationContext } from '../../../lib/context/estimation';
interface TicketImportFormProps { interface TicketImportFormProps {
onTicketsImported: () => void; onTicketsImported: () => void;
} }
const TicketItem = ({
item,
}: {
item: EntityModels.EstimationSessionTicket;
}) => (
<Card
key={item.id}
title={item.name}
description={`Estimate: ${item.estimate ? item.estimate : 'N/A'}`}
>
<HtmlEmbed className="h-16 w-full" body={item.content} />
</Card>
);
const TicketImportForm: React.FC<TicketImportFormProps> = ({ const TicketImportForm: React.FC<TicketImportFormProps> = ({
onTicketsImported, onTicketsImported,
}) => { }) => {
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [tickets, setTickets] = useState<EstimationSessionTicket[]>([]); const [tickets, setTickets] = useState<
EntityModels.EstimationSessionTicket[]
>([]);
const estimationContext = useEstimationContext(); const estimationContext = useEstimationContext();
if (!estimationContext) { if (!estimationContext) {
@ -65,15 +80,7 @@ const TicketImportForm: React.FC<TicketImportFormProps> = ({
className="no-scrollbar overflow-y-scroll" className="no-scrollbar overflow-y-scroll"
items={tickets} items={tickets}
colNum={1} colNum={1}
itemComponent={({ item }) => ( itemComponent={TicketItem}
<Card
key={item.id}
title={item.name}
description={`Estimate: ${item.estimate || 'N/A'}`}
>
<HtmlEmbed className="h-16 w-full" body={item.content} />
</Card>
)}
/> />
<Button className="mt-4" fullWidth onClick={onCreateTickets}> <Button className="mt-4" fullWidth onClick={onCreateTickets}>
Import Import

View File

@ -1,5 +1,5 @@
import { Card, GridList } from '../../../components'; import { Card, GridList } from 'src/components';
import { PlayerVote } from '../../../lib/types/entityModels'; import { PlayerVote } from 'src/lib/types/entityModels';
interface VoteListProps { interface VoteListProps {
className?: string; className?: string;
@ -8,22 +8,20 @@ interface VoteListProps {
} }
const VoteList: React.FC<VoteListProps> = ({ className, votes, revealed }) => { const VoteList: React.FC<VoteListProps> = ({ className, votes, revealed }) => {
return ( const voteListItem = ({ item }: { item: PlayerVote }, idx: string) => (
<div className={className}>
{votes.length > 0 && (
<h2 className="mb-4 text-xl font-bold">Player Votes</h2>
)}
<GridList
colNum={5}
itemComponent={({ item }, idx) => (
<Card <Card
key={idx} key={idx}
title={item.username} title={item.username}
description={revealed ? item.estimate : 'Hidden'} description={revealed ? item.estimate : 'Hidden'}
/> />
);
return (
<div className={className}>
{votes.length > 0 && (
<h2 className="mb-4 text-xl font-bold">Player Votes</h2>
)} )}
items={votes} <GridList colNum={5} itemComponent={voteListItem} items={votes} />
/>
</div> </div>
); );
}; };

View File

@ -1,4 +1,5 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { Button } from 'src/components';
interface VoteSelectionProps { interface VoteSelectionProps {
className: string; className: string;
@ -15,20 +16,20 @@ const VoteSelection: React.FC<VoteSelectionProps> = ({
}) => { }) => {
const getItemClassName = (option: string) => const getItemClassName = (option: string) =>
classNames('rounded-md px-4 py-2 text-white transition-colors', { classNames('rounded-md px-4 py-2 text-white transition-colors', {
'bg-indigo-800': value !== option, 'bg-indigo-800': value === option,
'bg-indigo-600 hover:bg-indigo-500': value === option, 'bg-indigo-600 hover:bg-indigo-500': value !== option,
}); });
return ( return (
<div className={className}> <div className={className}>
{options.map((option) => ( {options.map((option) => (
<button <Button
key={option} key={option}
className={getItemClassName(option)}
onClick={() => onSelect(option)} onClick={() => onSelect(option)}
className={getItemClassName(option)}
> >
{option} {option}
</button> </Button>
))} ))}
</div> </div>
); );

View File

@ -1,49 +0,0 @@
import { getRouteApi } from '@tanstack/react-router';
import { useEstimationsList } from '../lib/context/estimationsList';
import {
Card,
CreateEstimationSessionForm,
Drawer,
GridList,
} from '../components';
import { useState } from 'react';
const route = getRouteApi('/_authenticated/');
function Home() {
const navigate = route.useNavigate();
const estimationsList = useEstimationsList();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
return (
<div className="p-6">
<h1 className="text-3xl font-bold">Estimation sessions</h1>
<GridList
colNum={2}
className="my-3"
items={estimationsList?.current ?? []}
itemComponent={({ item }) => (
<Card
key={item.id}
title={item.name}
onClick={() => {
navigate({
to: '/estimate/session/$sessionId',
params: { sessionId: item.id },
});
}}
/>
)}
addItemLabel="+ Create Estimation Session"
onAddItem={() => setIsDrawerOpen(true)}
/>
<Drawer isOpen={isDrawerOpen} onClose={() => setIsDrawerOpen(false)}>
<CreateEstimationSessionForm onCreated={() => setIsDrawerOpen(false)} />
</Drawer>
</div>
);
}
export default Home;

35
src/pages/Home/Home.tsx Normal file
View File

@ -0,0 +1,35 @@
import { useEstimationsList } from 'src/lib/context/estimationsList';
import { Drawer, DrawerSize, GridList } from 'src/components';
import { useState } from 'react';
import CreateEstimationSessionForm from './components/CreateEstimationSessionForm';
import EstimationSessionCard from './components/EstimationSessionCard';
function Home() {
const estimationsList = useEstimationsList();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
return (
<div className="p-6">
<h1 className="text-3xl font-bold">Estimation sessions</h1>
<GridList
colNum={2}
className="my-3"
items={estimationsList?.current ?? []}
itemComponent={EstimationSessionCard}
addItemLabel="+ Create Estimation Session"
onAddItem={() => setIsDrawerOpen(true)}
/>
<Drawer
size={DrawerSize.Small}
isOpen={isDrawerOpen}
onClose={() => setIsDrawerOpen(false)}
>
<CreateEstimationSessionForm onCreated={() => setIsDrawerOpen(false)} />
</Drawer>
</div>
);
}
export default Home;

View File

@ -1,9 +1,8 @@
import { useForm } from '@tanstack/react-form'; import { useForm } from '@tanstack/react-form';
import { useEstimationsList } from '../../lib/context/estimationsList'; import { useEstimationsList } from 'src/lib/context/estimationsList';
import Input from '../Input';
import Button from '../Button';
import { yupValidator } from '@tanstack/yup-form-adapter'; import { yupValidator } from '@tanstack/yup-form-adapter';
import * as yup from 'yup'; import * as yup from 'yup';
import { Input, Button } from 'src/components';
interface CreateEstimationSessionFormProps { interface CreateEstimationSessionFormProps {
onCreated: () => void; onCreated: () => void;

View File

@ -0,0 +1,30 @@
import { getRouteApi } from '@tanstack/react-router';
import { Card } from 'src/components';
import { EntityModels } from 'src/lib/types';
interface EstimationSessionCardProps {
item: EntityModels.EstimationSession;
}
const route = getRouteApi('/_authenticated/');
const EstimationSessionCard: React.FC<EstimationSessionCardProps> = ({
item,
}) => {
const navigate = route.useNavigate();
return (
<Card
key={item.id}
title={item.name}
onClick={() => {
navigate({
to: '/estimate/session/$sessionId',
params: { sessionId: item.id },
});
}}
/>
);
};
export default EstimationSessionCard;

View File

@ -3,9 +3,9 @@ import {
getInviteInfo, getInviteInfo,
joinSession, joinSession,
SessionInviteInfo, SessionInviteInfo,
} from '../lib/functions/estimationSessionInvite'; } from 'src/lib/functions/estimationSessionInvite';
import { getRouteApi } from '@tanstack/react-router'; import { getRouteApi } from '@tanstack/react-router';
import { Loader } from '../components'; import { Button, ButtonColor, Card, Loader } from 'src/components';
const route = getRouteApi('/_authenticated/join/$sessionId'); const route = getRouteApi('/_authenticated/join/$sessionId');
@ -50,28 +50,21 @@ const Join = () => {
return ( return (
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 transition-colors dark:bg-nero-900"> <div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 transition-colors dark:bg-nero-900">
<div className="max-w-lg rounded-lg bg-white p-8 text-center shadow-lg dark:bg-nero-800"> <Card
<h1 className="mb-4 text-2xl font-semibold text-gray-900 dark:text-gray-100"> title="You have been invited to join a new estimation session!"
You have been invited to join a new estimation session! className="bg-white shadow-lg dark:bg-nero-800"
</h1> transparent
>
<p className="mb-6 text-lg text-gray-700 dark:text-gray-300"> <p className="mb-6 text-lg text-gray-700 dark:text-gray-300">
Session Name: <strong>{sessionInfo.name}</strong> Session Name: <strong>{sessionInfo.name}</strong>
</p> </p>
<div className="flex justify-center gap-4"> <div className="flex justify-center gap-4">
<button <Button onClick={handleAccept}>Accept</Button>
onClick={handleAccept} <Button onClick={handleReturnHome} color={ButtonColor.Secondary}>
className="rounded-md bg-indigo-600 px-6 py-2 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-indigo-500" Return Home
> </Button>
Accept
</button>
<button
onClick={handleReturnHome}
className="rounded-md bg-gray-300 px-6 py-2 text-sm font-semibold text-gray-900 shadow-sm transition-colors hover:bg-gray-200 dark:bg-nero-700 dark:text-gray-100 dark:hover:bg-gray-600"
>
Return to Home
</button>
</div>
</div> </div>
</Card>
</div> </div>
); );
}; };

View File

@ -1,8 +1,9 @@
import { useForm } from '@tanstack/react-form'; import { useForm } from '@tanstack/react-form';
import { useUser } from '../lib/context/user'; import { useUser } from 'src/lib/context/user';
import { Button, ButtonColor, Input } from '../components'; import { Button, ButtonColor, Card, Input } from 'src/components';
import { yupValidator } from '@tanstack/yup-form-adapter'; import { yupValidator } from '@tanstack/yup-form-adapter';
import * as yup from 'yup'; import * as yup from 'yup';
import { Link } from '@tanstack/react-router';
const Login = () => { const Login = () => {
const user = useUser(); const user = useUser();
@ -24,14 +25,11 @@ const Login = () => {
}); });
return ( return (
<div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8"> <div className="flex h-screen flex-1 flex-col justify-center px-6 py-12 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-sm"> <Card
<h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight"> title="Sign in to your account"
Sign in to your account className="sm:mx-auto sm:w-full sm:max-w-sm"
</h2> >
</div>
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form className="space-y-6"> <form className="space-y-6">
<form.Field name="email"> <form.Field name="email">
{(field) => ( {(field) => (
@ -107,15 +105,14 @@ const Login = () => {
<p className="mt-10 text-center text-sm text-gray-500"> <p className="mt-10 text-center text-sm text-gray-500">
Don't want to create an account?{' '} Don't want to create an account?{' '}
<a <Link
href="#"
className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500" className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
onClick={() => user.loginAsGuest()} onClick={() => user.loginAsGuest()}
> >
Sign in as a guest Sign in as a guest
</a> </Link>
</p> </p>
</div> </Card>
</div> </div>
); );
}; };

View File

@ -1,6 +1,6 @@
import { useForm } from '@tanstack/react-form'; import { useForm } from '@tanstack/react-form';
import { Button, Input } from '../components'; import { Button, Card, Input } from 'src/components';
import { useUser } from '../lib/context/user'; import { useUser } from 'src/lib/context/user';
import { yupValidator } from '@tanstack/yup-form-adapter'; import { yupValidator } from '@tanstack/yup-form-adapter';
import * as yup from 'yup'; import * as yup from 'yup';
@ -15,7 +15,7 @@ const Profile = () => {
updateUsernameForm.reset(); updateUsernameForm.reset();
}, },
validators: { validators: {
onChange: yup.object({ onSubmit: yup.object({
name: yup.string().label('Name').max(128).required(), name: yup.string().label('Name').max(128).required(),
}), }),
}, },
@ -24,10 +24,11 @@ const Profile = () => {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 transition-colors dark:bg-nero-900"> <div className="flex min-h-screen items-center justify-center bg-gray-50 transition-colors dark:bg-nero-900">
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-lg dark:bg-nero-800"> <Card
<h1 className="mb-6 text-2xl font-semibold text-gray-900 dark:text-gray-100"> title="Update Name"
Update Name className="w-full max-w-md bg-white shadow-lg dark:bg-nero-800"
</h1> transparent
>
<form <form
className="space-y-6" className="space-y-6"
onSubmit={(e) => { onSubmit={(e) => {
@ -65,7 +66,7 @@ const Profile = () => {
)} )}
</updateUsernameForm.Subscribe> </updateUsernameForm.Subscribe>
</form> </form>
</div> </Card>
</div> </div>
); );
}; };

7
src/pages/index.ts Normal file
View File

@ -0,0 +1,7 @@
import Estimation from './Estimation/Estimation';
import Home from './Home/Home';
import Join from './Join';
import Login from './Login';
import Profile from './Profile';
export { Estimation, Home, Join, Login, Profile };

View File

@ -18,7 +18,13 @@
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true,
/* Paths */
"baseUrl": ".",
"paths": {
"src/*": ["./src/*"]
}
}, },
"include": ["src"] "include": ["src"]
} }

View File

@ -1,7 +1,21 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc' import react from '@vitejs/plugin-react-swc';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
}) build: {
rollupOptions: {
output: {
manualChunks: {
RichEditor: ['src/components/RichEditor'],
},
},
},
},
resolve: {
alias: {
src: '/src',
},
},
});