Style estimation screen

This commit is contained in:
Pijus Kamandulis 2024-10-09 00:08:12 +03:00
parent cbde13314b
commit 253d13abd4
20 changed files with 532 additions and 168 deletions

View File

@ -9,15 +9,17 @@ enum ButtonColor {
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
color?: ButtonColor; color?: ButtonColor;
fullWidth?: boolean;
} }
const Button: React.FC<ButtonProps> = ({ const Button: React.FC<ButtonProps> = ({
children, children,
color = ButtonColor.Primary, color = ButtonColor.Primary,
fullWidth = false,
...props ...props
}) => { }) => {
const buttonClass = classNames( const buttonClass = classNames(
'flex w-full 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 text-white 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': 'bg-indigo-600 hover:bg-indigo-500 focus-visible:outline-indigo-600':
color === ButtonColor.Primary, color === ButtonColor.Primary,
@ -27,6 +29,7 @@ const Button: React.FC<ButtonProps> = ({
color === ButtonColor.Error, color === ButtonColor.Error,
'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600': 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600':
color === ButtonColor.Success, color === ButtonColor.Success,
'w-full': fullWidth,
}, },
); );

View File

@ -11,7 +11,6 @@ const Card: React.FC<CardProps> = ({ title, description, onClick }) => {
'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,
'cursor-default': !onClick,
}, },
'border-gray-300 dark:border-nero-700', 'border-gray-300 dark:border-nero-700',
'bg-white dark:bg-nero-900', 'bg-white dark:bg-nero-900',

View File

@ -32,7 +32,7 @@ const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, children }) => {
<div <div
className={classNames( className={classNames(
'dark:bg-nero-900 relative h-full w-80 transform space-y-6 bg-white p-6 shadow-lg transition-transform', 'relative h-full w-80 transform space-y-6 bg-white p-6 shadow-lg transition-transform dark:bg-nero-900',
{ {
'translate-x-0': isOpen, 'translate-x-0': isOpen,
'translate-x-full': !isOpen, 'translate-x-full': !isOpen,
@ -41,7 +41,7 @@ const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, children }) => {
> >
{children} {children}
<Button onClick={onClose} color={ButtonColor.Secondary}> <Button onClick={onClose} color={ButtonColor.Secondary} fullWidth>
Cancel Cancel
</Button> </Button>
</div> </div>

View File

@ -1,7 +1,7 @@
interface GridListProps<T> { interface GridListProps<T> {
items: T[]; items: T[];
colNum: number; colNum: number;
onAddItem: () => void; onAddItem?: () => void;
itemComponent: React.ComponentType<{ item: T }>; itemComponent: React.ComponentType<{ item: T }>;
} }

View File

@ -19,8 +19,8 @@ const CreateEstimationSessionForm: React.FC<
}, },
onSubmit: async ({ value }) => { onSubmit: async ({ value }) => {
await estimationSessions?.add({ await estimationSessions?.add({
Name: value.name, name: value.name,
UserId: user.current?.$id, userId: user.current?.$id,
}); });
onCreated(); onCreated();
}, },
@ -51,7 +51,9 @@ const CreateEstimationSessionForm: React.FC<
/> />
)} )}
/> />
<Button type="submit">Create</Button> <Button type="submit" fullWidth>
Create
</Button>
</form> </form>
</> </>
); );

View File

@ -1,5 +1,5 @@
// TODO: Think about moving to .env // TODO: Think about moving to .env
export const APPWRITE_ENDPOINT = 'https://cloud.appwrite.io/v1'; export const APPWRITE_ENDPOINT = 'https://cloud.appwrite.io/v1';
export const APPWRITE_PROJECT_ID = 'scrummie-poker'; export const APPWRITE_PROJECT_ID = 'scrummie-poker';
export const APPWRITE_DATABASE_ID = 'scrummie-poker-db'; export const APPWRITE_DATABASE_ID = '670402eb000f5aff721f';
export const APPWRITE_ESTIMATION_SESSION_COLLECTION_ID = 'estimation-session'; export const APPWRITE_ESTIMATION_SESSION_COLLECTION_ID = '670402f60023cb78d441';

View File

@ -0,0 +1,185 @@
import {
createContext,
PropsWithChildren,
useContext,
useEffect,
useState,
} from 'react';
import { client, databases } from '../appwrite';
import { DatabaseModels, EntityModels } from '../types';
import {
APPWRITE_DATABASE_ID,
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
} from '../../constants';
import { useUser } from './user';
import { EstimationSessionTicket } from '../types/entityModels';
interface EstimationContextType {
setSessionId: (sessionId: string) => void;
setActiveTicket: (ticketId: string) => Promise<void>;
setRevealed: (revealed: boolean) => Promise<void>;
setVote: (estimate: string) => Promise<void>;
createTicket: (ticket: Omit<EstimationSessionTicket, 'id'>) => Promise<void>;
currentSessionData?: EntityModels.EstimationSession;
}
const EstimationContext = createContext<EstimationContextType | undefined>(
undefined,
);
export const useEstimationContext = () => {
return useContext(EstimationContext);
};
const mapEstimationSession = (
data: DatabaseModels.EstimationSession,
{ userId }: { userId?: string },
) => {
const sessionState = JSON.parse(
data.sessionState,
) as EntityModels.SessionState;
const tickets = data.tickets.map<EntityModels.EstimationSessionTicket>(
(ticket) => JSON.parse(ticket),
);
const result: EntityModels.EstimationSession = {
id: data.$id,
name: data.name,
userId: data.userId,
tickets,
sessionState: {
...sessionState,
currentPlayerVote: sessionState.votes.find((x) => x.userId === userId)
?.estimate,
currentTicket: tickets.find((x) => x.id === sessionState.currentTicketId),
},
};
console.log({
result,
userId,
});
return result;
};
export const EstimationContextProvider = (props: PropsWithChildren) => {
const [sessionId, setSessionId] = useState('');
const [currentSessionData, setCurrentSessionData] =
useState<EntityModels.EstimationSession>();
const { current: userData } = useUser();
useEffect(() => {
if (!sessionId || !userData) {
return () => {};
}
databases
.getDocument<DatabaseModels.EstimationSession>(
APPWRITE_DATABASE_ID,
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
sessionId,
)
.then((payload) => {
const userId = userData?.$id ?? ''; // TODO: Not sure if this is the user id or session
setCurrentSessionData(mapEstimationSession(payload, { userId }));
});
return client.subscribe<DatabaseModels.EstimationSession>(
[
`databases.${APPWRITE_DATABASE_ID}.collections.${APPWRITE_ESTIMATION_SESSION_COLLECTION_ID}.documents.${sessionId}`,
],
({ payload }) => {
const userId = userData?.$id ?? ''; // TODO: Not sure if this is the user id or session
setCurrentSessionData(mapEstimationSession(payload, { userId }));
},
);
}, [sessionId, userData]);
const setSessionIdFunc = (newSessionId: string) => {
if (sessionId !== newSessionId) {
setSessionId(newSessionId);
}
};
const updateSessionState = async (
data: Partial<EntityModels.SessionState>,
) => {
await databases.updateDocument<DatabaseModels.EstimationSession>(
APPWRITE_DATABASE_ID,
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
sessionId,
{
sessionState: JSON.stringify({
...currentSessionData?.sessionState,
...data,
}),
},
);
};
const setActiveTicket = async (ticketId: string) => {
await updateSessionState({
currentTicketId: ticketId,
votes: [],
votesRevealed: false,
});
};
const setVote = async (estimate: string) => {
const userId = userData?.$id ?? ''; // TODO: Not sure if this is the user id or session
await updateSessionState({
votes: currentSessionData?.sessionState.votes
.filter((x) => x.userId !== userId)
.concat([
{
estimate: estimate,
userId: userId,
},
]),
});
};
const setRevealed = async (revealed: boolean) => {
await updateSessionState({
votesRevealed: revealed,
});
};
const createTicket = async (ticket: Omit<EstimationSessionTicket, 'id'>) => {
const newTicketsValue = currentSessionData?.tickets
.concat([
{
...ticket,
id: crypto.randomUUID(),
},
])
.map((x) => JSON.stringify(x));
await databases.updateDocument<DatabaseModels.EstimationSession>(
APPWRITE_DATABASE_ID,
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
sessionId,
{
tickets: newTicketsValue,
},
);
};
return (
<EstimationContext.Provider
value={{
setSessionId: setSessionIdFunc,
setActiveTicket,
setRevealed,
setVote,
createTicket,
currentSessionData,
}}
>
{props.children}
</EstimationContext.Provider>
);
};

View File

@ -13,23 +13,23 @@ import {
} from '../../constants'; } from '../../constants';
interface EstimationSessionType extends Models.Document { interface EstimationSessionType extends Models.Document {
UserId: string; userId: string;
Name: string; name: string;
Tickets: string[]; tickets: string[];
SessionState: string; sessionState: string;
} }
interface EstimationSessionTicket { interface EstimationSessionTicket {
Id: string; id: string;
Name: string; name: string;
} }
interface SessionStateType { interface SessionStateType {
CurrentTicketId: string; currentTicketId: string;
VotesRevealed: boolean; votesRevealed: boolean;
Votes: { votes: {
UserId: string; userId: string;
Estimate: number; estimate: number;
}[]; }[];
} }
@ -41,7 +41,7 @@ interface EstimationSessionsContextType {
remove: (id: string) => Promise<void>; remove: (id: string) => Promise<void>;
addTicket: ( addTicket: (
sessionId: string, sessionId: string,
ticket: Omit<EstimationSessionTicket, 'Id'>, ticket: Omit<EstimationSessionTicket, 'id'>,
) => Promise<void>; ) => Promise<void>;
getTickets: (sessionId: string) => EstimationSessionTicket[]; getTickets: (sessionId: string) => EstimationSessionTicket[];
selectTicket: (sessionId: string, ticketId: string) => Promise<void>; selectTicket: (sessionId: string, ticketId: string) => Promise<void>;
@ -98,7 +98,7 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
const addTicket = async ( const addTicket = async (
sessionId: string, sessionId: string,
ticket: Omit<EstimationSessionTicket, 'Id'>, ticket: Omit<EstimationSessionTicket, 'id'>,
) => { ) => {
const currentSession = estimationSessions.find((x) => x.$id === sessionId); const currentSession = estimationSessions.find((x) => x.$id === sessionId);
const response = await databases.updateDocument<EstimationSessionType>( const response = await databases.updateDocument<EstimationSessionType>(
@ -106,10 +106,10 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID, APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
sessionId, sessionId,
{ {
Tickets: currentSession?.Tickets.concat([ tickets: currentSession?.tickets.concat([
JSON.stringify({ JSON.stringify({
...ticket, ...ticket,
Id: crypto.randomUUID(), id: crypto.randomUUID(),
}), }),
]), ]),
}, },
@ -126,7 +126,7 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
return ( return (
estimationSessions estimationSessions
.find((x) => x.$id === sessionId) .find((x) => x.$id === sessionId)
?.Tickets.map<EstimationSessionTicket>((x) => JSON.parse(x)) ?? [] ?.tickets.map<EstimationSessionTicket>((x) => JSON.parse(x)) ?? []
); );
}; };
@ -136,8 +136,8 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID, APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
sessionId, sessionId,
{ {
SessionState: JSON.stringify({ sessionState: JSON.stringify({
CurrentTicketId: ticketId, currentTicketId: ticketId,
}), }),
}, },
); );
@ -151,7 +151,7 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
const getState = (sessionId: string): SessionStateType => { const getState = (sessionId: string): SessionStateType => {
return JSON.parse( return JSON.parse(
estimationSessions.find((x) => x.$id === sessionId)?.SessionState ?? '{}', estimationSessions.find((x) => x.$id === sessionId)?.sessionState ?? '{}',
); );
}; };
@ -162,12 +162,12 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
userId: string, userId: string,
) => { ) => {
const currentState = getState(sessionId); const currentState = getState(sessionId);
const newVotes = (currentState.Votes ?? []) const newVotes = (currentState.votes ?? [])
.filter((x) => x.UserId !== userId) .filter((x) => x.userId !== userId)
.concat([ .concat([
{ {
Estimate: estimate, estimate: estimate,
UserId: userId, userId: userId,
}, },
]); ]);
const response = await databases.updateDocument<EstimationSessionType>( const response = await databases.updateDocument<EstimationSessionType>(
@ -175,9 +175,9 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID, APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
sessionId, sessionId,
{ {
SessionState: JSON.stringify({ sessionState: JSON.stringify({
CurrentTicketId: ticketId, currentTicketId: ticketId,
Votes: newVotes, votes: newVotes,
}), }),
}, },
); );
@ -196,7 +196,7 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID, APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
sessionId, sessionId,
{ {
SessionState: JSON.stringify({ sessionState: JSON.stringify({
...currentState, ...currentState,
VotesRevealed: true, VotesRevealed: true,
}), }),
@ -217,8 +217,12 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
[Query.orderDesc('$createdAt'), Query.limit(10)], [Query.orderDesc('$createdAt'), Query.limit(10)],
); );
setEstimationSessions(response.documents); setEstimationSessions(response.documents);
};
client.subscribe<EstimationSessionType>( useEffect(() => {
init();
return client.subscribe<EstimationSessionType>(
[ [
`databases.${APPWRITE_DATABASE_ID}.collections.${APPWRITE_ESTIMATION_SESSION_COLLECTION_ID}.documents`, `databases.${APPWRITE_DATABASE_ID}.collections.${APPWRITE_ESTIMATION_SESSION_COLLECTION_ID}.documents`,
], ],
@ -231,10 +235,6 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
); );
}, },
); );
};
useEffect(() => {
init();
}, []); }, []);
return ( return (

View File

@ -0,0 +1,10 @@
import { Models } from 'appwrite';
interface EstimationSession extends Models.Document {
userId: string;
name: string;
tickets: string[];
sessionState: string;
}
export type { EstimationSession };

View File

@ -0,0 +1,32 @@
interface EstimationSession {
id: string;
userId: string;
name: string;
tickets: EstimationSessionTicket[];
sessionState: SessionState;
}
interface EstimationSessionTicket {
id: string;
name: string;
}
interface SessionState {
currentTicketId?: string;
votesRevealed: boolean;
votes: PlayerVote[];
currentPlayerVote?: string;
currentTicket?: EstimationSessionTicket;
}
interface PlayerVote {
userId: string;
estimate: string;
}
export type {
EstimationSession,
EstimationSessionTicket,
SessionState,
PlayerVote,
};

2
src/lib/types/index.ts Normal file
View File

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

View File

@ -10,8 +10,9 @@ import {
import Home from './pages/Home'; import Home from './pages/Home';
import { UserProvider } from './lib/context/user'; import { UserProvider } from './lib/context/user';
import Login from './pages/Login'; import Login from './pages/Login';
import EstimationSession from './pages/EstimationSession';
import { EstimationSessionProvider } from './lib/context/estimationSession'; import { EstimationSessionProvider } from './lib/context/estimationSession';
import { EstimationContextProvider } from './lib/context/estimation';
import Estimation from './pages/Estimation/Estimation';
const rootRoute = createRootRoute(); const rootRoute = createRootRoute();
@ -29,7 +30,7 @@ const loginRoute = createRoute({
const estimationSessionRoute = createRoute({ const estimationSessionRoute = createRoute({
path: 'estimate/session/$sessionId', path: 'estimate/session/$sessionId',
component: EstimationSession, component: Estimation,
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
}); });
@ -49,11 +50,13 @@ declare module '@tanstack/react-router' {
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<EstimationSessionProvider> <UserProvider>
{/* TODO: Move ctx providers to layout */} <EstimationSessionProvider>
<UserProvider> <EstimationContextProvider>
<RouterProvider router={router} /> {/* TODO: Move ctx providers to layout */}
</UserProvider> <RouterProvider router={router} />
</EstimationSessionProvider> </EstimationContextProvider>
</EstimationSessionProvider>
</UserProvider>
</StrictMode>, </StrictMode>,
); );

View File

@ -0,0 +1,92 @@
import React, { useEffect, useState } from 'react';
import { useEstimationContext } from '../../lib/context/estimation';
import { getRouteApi } from '@tanstack/react-router';
import TaskSidebar from './components/TaskSidebar';
import VoteSelection from './components/VoteSelection';
import VoteList from './components/VoteList';
import { Button, ButtonColor, Drawer } from '../../components';
import CreateTicketForm from './components/CreateTicketForm';
const fibonacciSequence = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 100];
const route = getRouteApi('/estimate/session/$sessionId');
const Estimation: React.FC = () => {
const { sessionId } = route.useParams();
const estimationState = useEstimationContext();
useEffect(() => estimationState?.setSessionId(sessionId), [sessionId]);
const [isDrawerOpen, setDrawerOpen] = useState(false);
if (!estimationState?.currentSessionData) {
return null; // TODO: Add a loader
}
const {
setActiveTicket,
setVote,
setRevealed,
createTicket,
currentSessionData: {
tickets: tickets,
sessionState: {
votesRevealed: revealed,
votes: votes,
currentPlayerVote,
currentTicket,
},
},
} = estimationState;
return (
<div className="flex h-screen">
<TaskSidebar
className="dark:bg-customGray-800 w-64 overflow-y-scroll bg-gray-50 p-4 dark:bg-nero-700"
tickets={tickets}
onSelectTicket={(ticket) => setActiveTicket(ticket.id)}
onAddTicket={() => setDrawerOpen(true)}
/>
<div className="flex w-full flex-grow flex-col p-6">
{currentTicket ? (
<>
<h1 className="mb-4 text-2xl font-bold">{currentTicket.name}</h1>
<p className="mb-8 text-gray-700 dark:text-gray-200">
{currentTicket.id}
</p>
<VoteSelection
className="mb-4 mt-auto flex flex-wrap gap-1 space-x-4"
onSelect={(vote) => setVote(vote)}
options={fibonacciSequence.map((x) => `${x}`)}
value={currentPlayerVote}
/>
<VoteList className="mt-6" revealed={revealed} votes={votes} />
<div className="mt-4">
<Button
color={ButtonColor.Error}
onClick={() => setRevealed(true)}
>
Reveal Votes
</Button>
</div>
</>
) : (
<p>Select a task to see the details and estimate.</p>
)}
</div>
<Drawer isOpen={isDrawerOpen} onClose={() => setDrawerOpen(false)}>
<CreateTicketForm
onCreate={async (ticket) => {
await createTicket(ticket);
setDrawerOpen(false);
}}
/>
</Drawer>
</div>
);
};
export default Estimation;

View File

@ -0,0 +1,51 @@
import { useForm } from '@tanstack/react-form';
import { Button, Input } from '../../../components';
import { EstimationSessionTicket } from '../../../lib/types/entityModels';
interface CreateTicketFormProps {
onCreate: (ticket: Omit<EstimationSessionTicket, 'id'>) => Promise<void>;
}
const CreateTicketForm: React.FC<CreateTicketFormProps> = ({ onCreate }) => {
const form = useForm({
defaultValues: {
name: '',
},
onSubmit: async ({ value }) => {
await onCreate(value);
},
});
return (
<div>
<h2 className="mb-4 text-xl font-bold">Create a New Ticket</h2>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<form.Field
name="name"
children={(field) => (
<Input
label="Name"
required
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
<Button type="submit" fullWidth>
Create
</Button>
</form>
</div>
);
};
export default CreateTicketForm;

View File

@ -0,0 +1,36 @@
import { Card, GridList } from '../../../components';
import { EstimationSessionTicket } from '../../../lib/types/entityModels';
interface TaskSidebarProps {
className?: string;
tickets: EstimationSessionTicket[];
onSelectTicket: (ticket: EstimationSessionTicket) => void;
onAddTicket: () => void;
}
const TaskSidebar: React.FC<TaskSidebarProps> = ({
className,
tickets,
onSelectTicket,
onAddTicket,
}) => {
return (
<div className={className}>
<GridList
items={tickets}
colNum={1}
itemComponent={({ item }) => (
<Card
key={item.id}
title={item.name}
description={item.id}
onClick={() => onSelectTicket(item)}
/>
)}
onAddItem={onAddTicket}
/>
</div>
);
};
export default TaskSidebar;

View File

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

View File

@ -0,0 +1,37 @@
import classNames from 'classnames';
interface VoteSelectionProps {
className: string;
value?: string;
options: string[];
onSelect: (vote: string) => void;
}
const VoteSelection: React.FC<VoteSelectionProps> = ({
className,
value,
options,
onSelect,
}) => {
const getItemClassName = (option: string) =>
classNames('rounded-md px-4 py-2 text-white transition-colors', {
'bg-indigo-800': value !== option,
'bg-indigo-600 hover:bg-indigo-500': value === option,
});
return (
<div className={className}>
{options.map((option) => (
<button
key={option}
className={getItemClassName(option)}
onClick={() => onSelect(option)}
>
{option}
</button>
))}
</div>
);
};
export default VoteSelection;

View File

@ -1,110 +0,0 @@
import { getRouteApi } from '@tanstack/react-router';
import { useEstimationSessions } from '../lib/context/estimationSession';
import { useForm } from '@tanstack/react-form';
import { useUser } from '../lib/context/user';
const route = getRouteApi('/estimate/session/$sessionId');
const EstimationSession = () => {
const { sessionId } = route.useParams();
const user = useUser();
const estimationSessions = useEstimationSessions();
const estimationSession = estimationSessions?.current.find(
(x) => x.$id == sessionId,
);
const tickets = estimationSessions?.getTickets(sessionId);
const currentState = estimationSessions?.getState(sessionId);
const createTicketForm = useForm({
defaultValues: {
name: '',
},
onSubmit: async ({ value }) => {
await estimationSessions?.addTicket(sessionId, {
Name: value.name,
});
},
});
return (
<>
<h1>Estimation Session - {estimationSession?.Name}</h1>
<div>
<h2>Tasks</h2>
{tickets?.map((x) => (
<div key={x.Id}>
{x.Id} - {x.Name}
<button
onClick={() => estimationSessions?.selectTicket(sessionId, x.Id)}
>
Select
</button>
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
createTicketForm.handleSubmit();
}}
>
<createTicketForm.Field
name="name"
children={(field) => (
<input
placeholder="Name"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
<button type="submit">Submit</button>
</form>
</div>
{currentState?.CurrentTicketId && (
<div>
<h2>
{currentState.CurrentTicketId} -{' '}
{tickets?.find((x) => x.Id === currentState.CurrentTicketId)?.Name}
</h2>
{[0.5, 1, 2, 3, 5, 8, 13, 21].map((estimate) => (
<button
key={estimate}
onClick={() =>
estimationSessions?.voteEstimate(
sessionId,
currentState.CurrentTicketId,
estimate,
user.current?.$id ?? '',
)
}
>
{estimate}
</button>
))}
{currentState.VotesRevealed ? (
<>
<h3>Votes</h3>
<ul>
{currentState.Votes.map((vote) => (
<li key={vote.UserId}>
{vote.UserId} - {vote.Estimate}
</li>
))}
</ul>
</>
) : (
<button onClick={() => estimationSessions?.revealVotes(sessionId)}>
Reveal Votes
</button>
)}
</div>
)}
<pre>Session Id: {sessionId}</pre>
</>
);
};
export default EstimationSession;

View File

@ -1,10 +1,3 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo { .logo {
height: 6em; height: 6em;
padding: 1.5em; padding: 1.5em;

View File

@ -47,7 +47,7 @@ function Home() {
itemComponent={({ item }) => ( itemComponent={({ item }) => (
<Card <Card
key={item.$id} key={item.$id}
title={item.Name} title={item.name}
description={item.$id} description={item.$id}
onClick={() => { onClick={() => {
navigate({ navigate({