Style estimation screen
This commit is contained in:
parent
cbde13314b
commit
253d13abd4
|
@ -9,15 +9,17 @@ enum ButtonColor {
|
|||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
color?: ButtonColor;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const Button: React.FC<ButtonProps> = ({
|
||||
children,
|
||||
color = ButtonColor.Primary,
|
||||
fullWidth = false,
|
||||
...props
|
||||
}) => {
|
||||
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':
|
||||
color === ButtonColor.Primary,
|
||||
|
@ -27,6 +29,7 @@ const Button: React.FC<ButtonProps> = ({
|
|||
color === ButtonColor.Error,
|
||||
'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600':
|
||||
color === ButtonColor.Success,
|
||||
'w-full': fullWidth,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ const Card: React.FC<CardProps> = ({ title, description, onClick }) => {
|
|||
'p-4 border rounded-lg shadow-sm transition',
|
||||
{
|
||||
'hover:bg-gray-100 dark:hover:bg-nero-800 cursor-pointer': onClick,
|
||||
'cursor-default': !onClick,
|
||||
},
|
||||
'border-gray-300 dark:border-nero-700',
|
||||
'bg-white dark:bg-nero-900',
|
||||
|
|
|
@ -32,7 +32,7 @@ const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, children }) => {
|
|||
|
||||
<div
|
||||
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-full': !isOpen,
|
||||
|
@ -41,7 +41,7 @@ const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, children }) => {
|
|||
>
|
||||
{children}
|
||||
|
||||
<Button onClick={onClose} color={ButtonColor.Secondary}>
|
||||
<Button onClick={onClose} color={ButtonColor.Secondary} fullWidth>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
interface GridListProps<T> {
|
||||
items: T[];
|
||||
colNum: number;
|
||||
onAddItem: () => void;
|
||||
onAddItem?: () => void;
|
||||
itemComponent: React.ComponentType<{ item: T }>;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ const CreateEstimationSessionForm: React.FC<
|
|||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
await estimationSessions?.add({
|
||||
Name: value.name,
|
||||
UserId: user.current?.$id,
|
||||
name: value.name,
|
||||
userId: user.current?.$id,
|
||||
});
|
||||
onCreated();
|
||||
},
|
||||
|
@ -51,7 +51,9 @@ const CreateEstimationSessionForm: React.FC<
|
|||
/>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Create</Button>
|
||||
<Button type="submit" fullWidth>
|
||||
Create
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// TODO: Think about moving to .env
|
||||
export const APPWRITE_ENDPOINT = 'https://cloud.appwrite.io/v1';
|
||||
export const APPWRITE_PROJECT_ID = 'scrummie-poker';
|
||||
export const APPWRITE_DATABASE_ID = 'scrummie-poker-db';
|
||||
export const APPWRITE_ESTIMATION_SESSION_COLLECTION_ID = 'estimation-session';
|
||||
export const APPWRITE_DATABASE_ID = '670402eb000f5aff721f';
|
||||
export const APPWRITE_ESTIMATION_SESSION_COLLECTION_ID = '670402f60023cb78d441';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -13,23 +13,23 @@ import {
|
|||
} from '../../constants';
|
||||
|
||||
interface EstimationSessionType extends Models.Document {
|
||||
UserId: string;
|
||||
Name: string;
|
||||
Tickets: string[];
|
||||
SessionState: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
tickets: string[];
|
||||
sessionState: string;
|
||||
}
|
||||
|
||||
interface EstimationSessionTicket {
|
||||
Id: string;
|
||||
Name: string;
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface SessionStateType {
|
||||
CurrentTicketId: string;
|
||||
VotesRevealed: boolean;
|
||||
Votes: {
|
||||
UserId: string;
|
||||
Estimate: number;
|
||||
currentTicketId: string;
|
||||
votesRevealed: boolean;
|
||||
votes: {
|
||||
userId: string;
|
||||
estimate: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,7 @@ interface EstimationSessionsContextType {
|
|||
remove: (id: string) => Promise<void>;
|
||||
addTicket: (
|
||||
sessionId: string,
|
||||
ticket: Omit<EstimationSessionTicket, 'Id'>,
|
||||
ticket: Omit<EstimationSessionTicket, 'id'>,
|
||||
) => Promise<void>;
|
||||
getTickets: (sessionId: string) => EstimationSessionTicket[];
|
||||
selectTicket: (sessionId: string, ticketId: string) => Promise<void>;
|
||||
|
@ -98,7 +98,7 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
|
|||
|
||||
const addTicket = async (
|
||||
sessionId: string,
|
||||
ticket: Omit<EstimationSessionTicket, 'Id'>,
|
||||
ticket: Omit<EstimationSessionTicket, 'id'>,
|
||||
) => {
|
||||
const currentSession = estimationSessions.find((x) => x.$id === sessionId);
|
||||
const response = await databases.updateDocument<EstimationSessionType>(
|
||||
|
@ -106,10 +106,10 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
|
|||
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
|
||||
sessionId,
|
||||
{
|
||||
Tickets: currentSession?.Tickets.concat([
|
||||
tickets: currentSession?.tickets.concat([
|
||||
JSON.stringify({
|
||||
...ticket,
|
||||
Id: crypto.randomUUID(),
|
||||
id: crypto.randomUUID(),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
|
@ -126,7 +126,7 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
|
|||
return (
|
||||
estimationSessions
|
||||
.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,
|
||||
sessionId,
|
||||
{
|
||||
SessionState: JSON.stringify({
|
||||
CurrentTicketId: ticketId,
|
||||
sessionState: JSON.stringify({
|
||||
currentTicketId: ticketId,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
@ -151,7 +151,7 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
|
|||
|
||||
const getState = (sessionId: string): SessionStateType => {
|
||||
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,
|
||||
) => {
|
||||
const currentState = getState(sessionId);
|
||||
const newVotes = (currentState.Votes ?? [])
|
||||
.filter((x) => x.UserId !== userId)
|
||||
const newVotes = (currentState.votes ?? [])
|
||||
.filter((x) => x.userId !== userId)
|
||||
.concat([
|
||||
{
|
||||
Estimate: estimate,
|
||||
UserId: userId,
|
||||
estimate: estimate,
|
||||
userId: userId,
|
||||
},
|
||||
]);
|
||||
const response = await databases.updateDocument<EstimationSessionType>(
|
||||
|
@ -175,9 +175,9 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
|
|||
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
|
||||
sessionId,
|
||||
{
|
||||
SessionState: JSON.stringify({
|
||||
CurrentTicketId: ticketId,
|
||||
Votes: newVotes,
|
||||
sessionState: JSON.stringify({
|
||||
currentTicketId: ticketId,
|
||||
votes: newVotes,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
@ -196,7 +196,7 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
|
|||
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
|
||||
sessionId,
|
||||
{
|
||||
SessionState: JSON.stringify({
|
||||
sessionState: JSON.stringify({
|
||||
...currentState,
|
||||
VotesRevealed: true,
|
||||
}),
|
||||
|
@ -217,8 +217,12 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
|
|||
[Query.orderDesc('$createdAt'), Query.limit(10)],
|
||||
);
|
||||
setEstimationSessions(response.documents);
|
||||
};
|
||||
|
||||
client.subscribe<EstimationSessionType>(
|
||||
useEffect(() => {
|
||||
init();
|
||||
|
||||
return client.subscribe<EstimationSessionType>(
|
||||
[
|
||||
`databases.${APPWRITE_DATABASE_ID}.collections.${APPWRITE_ESTIMATION_SESSION_COLLECTION_ID}.documents`,
|
||||
],
|
||||
|
@ -231,10 +235,6 @@ export function EstimationSessionProvider(props: PropsWithChildren) {
|
|||
);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
init();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { Models } from 'appwrite';
|
||||
|
||||
interface EstimationSession extends Models.Document {
|
||||
userId: string;
|
||||
name: string;
|
||||
tickets: string[];
|
||||
sessionState: string;
|
||||
}
|
||||
|
||||
export type { EstimationSession };
|
|
@ -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,
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export * as DatabaseModels from './databaseModels';
|
||||
export * as EntityModels from './entityModels';
|
13
src/main.tsx
13
src/main.tsx
|
@ -10,8 +10,9 @@ import {
|
|||
import Home from './pages/Home';
|
||||
import { UserProvider } from './lib/context/user';
|
||||
import Login from './pages/Login';
|
||||
import EstimationSession from './pages/EstimationSession';
|
||||
import { EstimationSessionProvider } from './lib/context/estimationSession';
|
||||
import { EstimationContextProvider } from './lib/context/estimation';
|
||||
import Estimation from './pages/Estimation/Estimation';
|
||||
|
||||
const rootRoute = createRootRoute();
|
||||
|
||||
|
@ -29,7 +30,7 @@ const loginRoute = createRoute({
|
|||
|
||||
const estimationSessionRoute = createRoute({
|
||||
path: 'estimate/session/$sessionId',
|
||||
component: EstimationSession,
|
||||
component: Estimation,
|
||||
getParentRoute: () => rootRoute,
|
||||
});
|
||||
|
||||
|
@ -49,11 +50,13 @@ declare module '@tanstack/react-router' {
|
|||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<EstimationSessionProvider>
|
||||
{/* TODO: Move ctx providers to layout */}
|
||||
<UserProvider>
|
||||
<EstimationSessionProvider>
|
||||
<EstimationContextProvider>
|
||||
{/* TODO: Move ctx providers to layout */}
|
||||
<RouterProvider router={router} />
|
||||
</UserProvider>
|
||||
</EstimationContextProvider>
|
||||
</EstimationSessionProvider>
|
||||
</UserProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,10 +1,3 @@
|
|||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
|
|
|
@ -47,7 +47,7 @@ function Home() {
|
|||
itemComponent={({ item }) => (
|
||||
<Card
|
||||
key={item.$id}
|
||||
title={item.Name}
|
||||
title={item.name}
|
||||
description={item.$id}
|
||||
onClick={() => {
|
||||
navigate({
|
||||
|
|
Loading…
Reference in New Issue