Code cleanup
This commit is contained in:
parent
048f8e53cc
commit
f0803ca6ec
|
@ -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
|
||||||
|
|
|
@ -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 && (
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 = ({
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 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"
|
||||||
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"
|
{...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}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
16
src/main.tsx
16
src/main.tsx
|
@ -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>
|
||||||
|
|
|
@ -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];
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
const voteListItem = ({ item }: { item: PlayerVote }, idx: string) => (
|
||||||
|
<Card
|
||||||
|
key={idx}
|
||||||
|
title={item.username}
|
||||||
|
description={revealed ? item.estimate : 'Hidden'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{votes.length > 0 && (
|
{votes.length > 0 && (
|
||||||
<h2 className="mb-4 text-xl font-bold">Player Votes</h2>
|
<h2 className="mb-4 text-xl font-bold">Player Votes</h2>
|
||||||
)}
|
)}
|
||||||
<GridList
|
<GridList colNum={5} itemComponent={voteListItem} items={votes} />
|
||||||
colNum={5}
|
|
||||||
itemComponent={({ item }, idx) => (
|
|
||||||
<Card
|
|
||||||
key={idx}
|
|
||||||
title={item.username}
|
|
||||||
description={revealed ? item.estimate : 'Hidden'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
items={votes}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 };
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue