mirror of
https://github.com/pikami/scrummie-poker.git
synced 2026-04-17 03:49:23 +01:00
Style estimation screen
This commit is contained in:
92
src/pages/Estimation/Estimation.tsx
Normal file
92
src/pages/Estimation/Estimation.tsx
Normal 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;
|
||||
51
src/pages/Estimation/components/CreateTicketForm.tsx
Normal file
51
src/pages/Estimation/components/CreateTicketForm.tsx
Normal 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;
|
||||
36
src/pages/Estimation/components/TaskSidebar.tsx
Normal file
36
src/pages/Estimation/components/TaskSidebar.tsx
Normal 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;
|
||||
29
src/pages/Estimation/components/VoteList.tsx
Normal file
29
src/pages/Estimation/components/VoteList.tsx
Normal 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;
|
||||
37
src/pages/Estimation/components/VoteSelection.tsx
Normal file
37
src/pages/Estimation/components/VoteSelection.tsx
Normal 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;
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user