Write a POC of estimation session
This commit is contained in:
parent
6fc1b81fd9
commit
45039d356f
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "all",
|
||||||
|
"singleQuote": true,
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true
|
||||||
|
}
|
|
@ -1,23 +1,27 @@
|
||||||
import js from '@eslint/js'
|
import js from '@eslint/js';
|
||||||
import globals from 'globals'
|
import globals from 'globals';
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{ ignores: ['dist'] },
|
{ ignores: ['dist'] },
|
||||||
{
|
{
|
||||||
extends: [js.configs.recommended, ...tseslint.configs.recommendedTypeChecked],
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
],
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ['**/*.{ts,tsx}'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
tsconfigRootDir: import.meta.dirname,
|
tsconfigRootDir: import.meta.dirname,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
|
react: react,
|
||||||
'react-hooks': reactHooks,
|
'react-hooks': reactHooks,
|
||||||
'react-refresh': reactRefresh,
|
'react-refresh': reactRefresh,
|
||||||
},
|
},
|
||||||
|
@ -27,6 +31,9 @@ export default tseslint.config(
|
||||||
'warn',
|
'warn',
|
||||||
{ allowConstantExport: true },
|
{ allowConstantExport: true },
|
||||||
],
|
],
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'no-console': 'warn',
|
||||||
|
curly: ['error', 'all'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
|
|
|
@ -10,6 +10,9 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tanstack/react-form": "^0.33.0",
|
||||||
|
"@tanstack/react-router": "^1.62.0",
|
||||||
|
"appwrite": "^16.0.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
},
|
},
|
||||||
|
@ -19,6 +22,7 @@
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
"eslint": "^9.11.1",
|
"eslint": "^9.11.1",
|
||||||
|
"eslint-plugin-react": "^7.37.1",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.12",
|
"eslint-plugin-react-refresh": "^0.4.12",
|
||||||
"globals": "^15.9.0",
|
"globals": "^15.9.0",
|
||||||
|
|
35
src/App.tsx
35
src/App.tsx
|
@ -1,35 +0,0 @@
|
||||||
import { useState } from 'react'
|
|
||||||
import reactLogo from './assets/react.svg'
|
|
||||||
import viteLogo from '/vite.svg'
|
|
||||||
import './App.css'
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [count, setCount] = useState(0)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<a href="https://vitejs.dev" target="_blank">
|
|
||||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
|
||||||
</a>
|
|
||||||
<a href="https://react.dev" target="_blank">
|
|
||||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<h1>Vite + React</h1>
|
|
||||||
<div className="card">
|
|
||||||
<button onClick={() => setCount((count) => count + 1)}>
|
|
||||||
count is {count}
|
|
||||||
</button>
|
|
||||||
<p>
|
|
||||||
Edit <code>src/App.tsx</code> and save to test HMR
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p className="read-the-docs">
|
|
||||||
Click on the Vite and React logos to learn more
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
|
@ -0,0 +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';
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Client, Account, Databases } from 'appwrite';
|
||||||
|
import { APPWRITE_ENDPOINT, APPWRITE_PROJECT_ID } from '../constants';
|
||||||
|
|
||||||
|
export const client = new Client();
|
||||||
|
|
||||||
|
client.setEndpoint(APPWRITE_ENDPOINT).setProject(APPWRITE_PROJECT_ID);
|
||||||
|
|
||||||
|
export { ID } from 'appwrite';
|
||||||
|
|
||||||
|
export const account = new Account(client);
|
||||||
|
export const databases = new Databases(client);
|
|
@ -0,0 +1,257 @@
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
PropsWithChildren,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { client, databases } from '../appwrite';
|
||||||
|
import { ID, Models, Query } from 'appwrite';
|
||||||
|
import {
|
||||||
|
APPWRITE_DATABASE_ID,
|
||||||
|
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
|
||||||
|
} from '../../constants';
|
||||||
|
|
||||||
|
interface EstimationSessionType extends Models.Document {
|
||||||
|
UserId: string;
|
||||||
|
Name: string;
|
||||||
|
Tickets: string[];
|
||||||
|
SessionState: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EstimationSessionTicket {
|
||||||
|
Id: string;
|
||||||
|
Name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SessionStateType {
|
||||||
|
CurrentTicketId: string;
|
||||||
|
VotesRevealed: boolean;
|
||||||
|
Votes: {
|
||||||
|
UserId: string;
|
||||||
|
Estimate: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EstimationSessionsContextType {
|
||||||
|
current: EstimationSessionType[];
|
||||||
|
add: (
|
||||||
|
estimationSession: Omit<EstimationSessionType, keyof Models.Document>,
|
||||||
|
) => Promise<void>;
|
||||||
|
remove: (id: string) => Promise<void>;
|
||||||
|
addTicket: (
|
||||||
|
sessionId: string,
|
||||||
|
ticket: Omit<EstimationSessionTicket, 'Id'>,
|
||||||
|
) => Promise<void>;
|
||||||
|
getTickets: (sessionId: string) => EstimationSessionTicket[];
|
||||||
|
selectTicket: (sessionId: string, ticketId: string) => Promise<void>;
|
||||||
|
getState: (sessionId: string) => SessionStateType;
|
||||||
|
voteEstimate: (
|
||||||
|
sessionId: string,
|
||||||
|
ticketId: string,
|
||||||
|
estimate: number,
|
||||||
|
userId: string,
|
||||||
|
) => Promise<void>;
|
||||||
|
revealVotes: (sessionId: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EstimationSessionsContext = createContext<
|
||||||
|
EstimationSessionsContextType | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
export function useEstimationSessions() {
|
||||||
|
return useContext(EstimationSessionsContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EstimationSessionProvider(props: PropsWithChildren) {
|
||||||
|
const [estimationSessions, setEstimationSessions] = useState<
|
||||||
|
EstimationSessionType[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const add = async (
|
||||||
|
estimationSession: Omit<EstimationSessionType, keyof Models.Document>,
|
||||||
|
) => {
|
||||||
|
const response = await databases.createDocument<EstimationSessionType>(
|
||||||
|
APPWRITE_DATABASE_ID,
|
||||||
|
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
|
||||||
|
ID.unique(),
|
||||||
|
estimationSession,
|
||||||
|
);
|
||||||
|
setEstimationSessions((estimationSessions) =>
|
||||||
|
[response, ...estimationSessions].slice(0, 10),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = async (id: string) => {
|
||||||
|
await databases.deleteDocument(
|
||||||
|
APPWRITE_DATABASE_ID,
|
||||||
|
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
setEstimationSessions((estimationSessions) =>
|
||||||
|
estimationSessions.filter(
|
||||||
|
(estimationSession) => estimationSession.$id !== id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await init();
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTicket = async (
|
||||||
|
sessionId: string,
|
||||||
|
ticket: Omit<EstimationSessionTicket, 'Id'>,
|
||||||
|
) => {
|
||||||
|
const currentSession = estimationSessions.find((x) => x.$id === sessionId);
|
||||||
|
const response = await databases.updateDocument<EstimationSessionType>(
|
||||||
|
APPWRITE_DATABASE_ID,
|
||||||
|
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
|
||||||
|
sessionId,
|
||||||
|
{
|
||||||
|
Tickets: currentSession?.Tickets.concat([
|
||||||
|
JSON.stringify({
|
||||||
|
...ticket,
|
||||||
|
Id: crypto.randomUUID(),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setEstimationSessions((estimationSessions) =>
|
||||||
|
estimationSessions
|
||||||
|
.filter((x) => x.$id != sessionId)
|
||||||
|
.concat([response])
|
||||||
|
.slice(0, 10),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTickets = (sessionId: string) => {
|
||||||
|
return (
|
||||||
|
estimationSessions
|
||||||
|
.find((x) => x.$id === sessionId)
|
||||||
|
?.Tickets.map<EstimationSessionTicket>((x) => JSON.parse(x)) ?? []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectTicket = async (sessionId: string, ticketId: string) => {
|
||||||
|
const response = await databases.updateDocument<EstimationSessionType>(
|
||||||
|
APPWRITE_DATABASE_ID,
|
||||||
|
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
|
||||||
|
sessionId,
|
||||||
|
{
|
||||||
|
SessionState: JSON.stringify({
|
||||||
|
CurrentTicketId: ticketId,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setEstimationSessions((estimationSessions) =>
|
||||||
|
estimationSessions
|
||||||
|
.filter((x) => x.$id != sessionId)
|
||||||
|
.concat([response])
|
||||||
|
.slice(0, 10),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getState = (sessionId: string): SessionStateType => {
|
||||||
|
return JSON.parse(
|
||||||
|
estimationSessions.find((x) => x.$id === sessionId)?.SessionState ?? '{}',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const voteEstimate = async (
|
||||||
|
sessionId: string,
|
||||||
|
ticketId: string,
|
||||||
|
estimate: number,
|
||||||
|
userId: string,
|
||||||
|
) => {
|
||||||
|
const currentState = getState(sessionId);
|
||||||
|
const newVotes = (currentState.Votes ?? [])
|
||||||
|
.filter((x) => x.UserId !== userId)
|
||||||
|
.concat([
|
||||||
|
{
|
||||||
|
Estimate: estimate,
|
||||||
|
UserId: userId,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const response = await databases.updateDocument<EstimationSessionType>(
|
||||||
|
APPWRITE_DATABASE_ID,
|
||||||
|
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
|
||||||
|
sessionId,
|
||||||
|
{
|
||||||
|
SessionState: JSON.stringify({
|
||||||
|
CurrentTicketId: ticketId,
|
||||||
|
Votes: newVotes,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setEstimationSessions((estimationSessions) =>
|
||||||
|
estimationSessions
|
||||||
|
.filter((x) => x.$id != sessionId)
|
||||||
|
.concat([response])
|
||||||
|
.slice(0, 10),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const revealVotes = async (sessionId: string) => {
|
||||||
|
const currentState = getState(sessionId);
|
||||||
|
const response = await databases.updateDocument<EstimationSessionType>(
|
||||||
|
APPWRITE_DATABASE_ID,
|
||||||
|
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
|
||||||
|
sessionId,
|
||||||
|
{
|
||||||
|
SessionState: JSON.stringify({
|
||||||
|
...currentState,
|
||||||
|
VotesRevealed: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setEstimationSessions((estimationSessions) =>
|
||||||
|
estimationSessions
|
||||||
|
.filter((x) => x.$id != sessionId)
|
||||||
|
.concat([response])
|
||||||
|
.slice(0, 10),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
const response = await databases.listDocuments<EstimationSessionType>(
|
||||||
|
APPWRITE_DATABASE_ID,
|
||||||
|
APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
|
||||||
|
[Query.orderDesc('$createdAt'), Query.limit(10)],
|
||||||
|
);
|
||||||
|
setEstimationSessions(response.documents);
|
||||||
|
|
||||||
|
client.subscribe<EstimationSessionType>(
|
||||||
|
[
|
||||||
|
`databases.${APPWRITE_DATABASE_ID}.collections.${APPWRITE_ESTIMATION_SESSION_COLLECTION_ID}.documents`,
|
||||||
|
],
|
||||||
|
(payload) => {
|
||||||
|
setEstimationSessions((estimationSessions) =>
|
||||||
|
estimationSessions
|
||||||
|
.filter((x) => x.$id != payload.payload.$id)
|
||||||
|
.concat([payload.payload])
|
||||||
|
.slice(0, 10),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
init();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EstimationSessionsContext.Provider
|
||||||
|
value={{
|
||||||
|
current: estimationSessions,
|
||||||
|
add,
|
||||||
|
remove,
|
||||||
|
addTicket,
|
||||||
|
getTickets,
|
||||||
|
selectTicket,
|
||||||
|
getState,
|
||||||
|
voteEstimate,
|
||||||
|
revealVotes,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</EstimationSessionsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { ID, Models } from 'appwrite';
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
PropsWithChildren,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { account } from '../appwrite';
|
||||||
|
|
||||||
|
interface UserContextType {
|
||||||
|
current: Models.Session | Models.User<Models.Preferences> | null;
|
||||||
|
login: (email: string, password: string) => Promise<void>;
|
||||||
|
logout: () => Promise<void>;
|
||||||
|
register: (email: string, password: string) => Promise<void>;
|
||||||
|
loginAsGuest: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserContext = createContext<UserContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const useUser = () => {
|
||||||
|
const context = useContext(UserContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useUser must be used within a UserProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserProvider = (props: PropsWithChildren) => {
|
||||||
|
const [user, setUser] = useState<
|
||||||
|
Models.Session | Models.User<Models.Preferences> | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
const login = async (email: string, password: string) => {
|
||||||
|
const loggedIn = await account.createEmailPasswordSession(email, password);
|
||||||
|
setUser(loggedIn);
|
||||||
|
window.location.replace('/'); // you can use different redirect method for your application
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
await account.deleteSession('current');
|
||||||
|
setUser(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const register = async (email: string, password: string) => {
|
||||||
|
await account.create(ID.unique(), email, password);
|
||||||
|
await login(email, password);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginAsGuest = async () => {
|
||||||
|
const session = await account.createAnonymousSession();
|
||||||
|
setUser(session);
|
||||||
|
window.location.replace('/'); // you can use different redirect method for your application
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
try {
|
||||||
|
const loggedIn = await account.get();
|
||||||
|
setUser(loggedIn);
|
||||||
|
} catch (err) {
|
||||||
|
setUser(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
init();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserContext.Provider
|
||||||
|
value={{ current: user, login, logout, register, loginAsGuest }}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</UserContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
69
src/main.tsx
69
src/main.tsx
|
@ -1,10 +1,67 @@
|
||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client';
|
||||||
import App from './App.tsx'
|
import './index.css';
|
||||||
import './index.css'
|
import {
|
||||||
|
createRootRoute,
|
||||||
|
createRoute,
|
||||||
|
createRouter,
|
||||||
|
RouterProvider,
|
||||||
|
} from '@tanstack/react-router';
|
||||||
|
import Home from './pages/Home';
|
||||||
|
import { UserProvider } from './lib/context/user';
|
||||||
|
import Login from './pages/Login';
|
||||||
|
import EstimationSession from './pages/EstimationSession';
|
||||||
|
import CreateEstimationSession from './pages/CreateEstimationSession';
|
||||||
|
import { EstimationSessionProvider } from './lib/context/estimationSession';
|
||||||
|
|
||||||
|
const rootRoute = createRootRoute();
|
||||||
|
|
||||||
|
const indexRoute = createRoute({
|
||||||
|
path: '/',
|
||||||
|
component: Home,
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginRoute = createRoute({
|
||||||
|
path: 'login',
|
||||||
|
component: Login,
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createEstimationSessionRoute = createRoute({
|
||||||
|
path: 'estimate/new',
|
||||||
|
component: CreateEstimationSession,
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
});
|
||||||
|
|
||||||
|
const estimationSessionRoute = createRoute({
|
||||||
|
path: 'estimate/session/$sessionId',
|
||||||
|
component: EstimationSession,
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
routeTree: rootRoute.addChildren([
|
||||||
|
indexRoute,
|
||||||
|
loginRoute,
|
||||||
|
createEstimationSessionRoute,
|
||||||
|
estimationSessionRoute,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface Register {
|
||||||
|
router: typeof router;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<EstimationSessionProvider>
|
||||||
|
{/* TODO: Move ctx providers to layout */}
|
||||||
|
<UserProvider>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</UserProvider>
|
||||||
|
</EstimationSessionProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
);
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { useForm } from '@tanstack/react-form';
|
||||||
|
import { useEstimationSessions } from '../lib/context/estimationSession';
|
||||||
|
import { useUser } from '../lib/context/user';
|
||||||
|
|
||||||
|
const CreateEstimationSession = () => {
|
||||||
|
const user = useUser();
|
||||||
|
const estimationSessions = useEstimationSessions();
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
await estimationSessions?.add({
|
||||||
|
Name: value.name,
|
||||||
|
UserId: user.current?.$id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Create Estimation Session</h1>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form.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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateEstimationSession;
|
|
@ -0,0 +1,110 @@
|
||||||
|
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;
|
|
@ -32,11 +32,3 @@
|
||||||
animation: logo-spin infinite 20s linear;
|
animation: logo-spin infinite 20s linear;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import reactLogo from '../assets/react.svg';
|
||||||
|
import viteLogo from '/vite.svg';
|
||||||
|
import './Home.css';
|
||||||
|
import { Link } from '@tanstack/react-router';
|
||||||
|
import { useUser } from '../lib/context/user';
|
||||||
|
import { useEstimationSessions } from '../lib/context/estimationSession';
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
const user = useUser();
|
||||||
|
const estimationSessions = useEstimationSessions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<a href="https://vitejs.dev" target="_blank">
|
||||||
|
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||||
|
</a>
|
||||||
|
<a href="https://react.dev" target="_blank">
|
||||||
|
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>Scrummie-Poker</h1>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<Link to="/login">Login</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link to="/estimate/new">Create Estimation Session</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<pre>User Id: {user.current?.$id}</pre>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Estimation sessions</p>
|
||||||
|
{estimationSessions?.current.map((session) => (
|
||||||
|
<Link key={session.$id} to={`/estimate/session/${session.$id}`}>
|
||||||
|
{session.Name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { useForm } from '@tanstack/react-form';
|
||||||
|
import { useUser } from '../lib/context/user';
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
const user = useUser();
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
console.log({ value });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Login or register</h1>
|
||||||
|
<form>
|
||||||
|
<form.Field
|
||||||
|
name="email"
|
||||||
|
children={(field) => (
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<form.Field
|
||||||
|
name="password"
|
||||||
|
children={(field) => (
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="button"
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
user.login(form.state.values.email, form.state.values.password)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="button"
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
user.register(form.state.values.email, form.state.values.password)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<button onClick={() => user.loginAsGuest()}>Login as guest</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
Loading…
Reference in New Issue