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

@@ -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 {
height: 6em;
padding: 1.5em;

View File

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