Allow user to set a username; Show list of users on estimation screen
This commit is contained in:
parent
f4d3005acd
commit
40b1ef6f0c
|
@ -52,6 +52,21 @@
|
|||
"entrypoint": "src/main.ts",
|
||||
"commands": "bun install",
|
||||
"path": "functions/EstimationSessionInvite"
|
||||
},
|
||||
{
|
||||
"$id": "670a4b770001c5a71194",
|
||||
"execute": [],
|
||||
"name": "UsernameChangeHandler",
|
||||
"enabled": true,
|
||||
"logging": true,
|
||||
"runtime": "go-1.23",
|
||||
"scopes": ["users.read", "documents.read", "documents.write"],
|
||||
"events": ["users.*.update.name"],
|
||||
"schedule": "",
|
||||
"timeout": 15,
|
||||
"entrypoint": "main.go",
|
||||
"commands": "",
|
||||
"path": "functions/UsernameChangeHandler"
|
||||
}
|
||||
],
|
||||
"databases": [
|
||||
|
@ -101,6 +116,22 @@
|
|||
"array": false,
|
||||
"size": 1000,
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"key": "players",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"array": true,
|
||||
"size": 100,
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"key": "playerIds",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"array": true,
|
||||
"size": 100,
|
||||
"default": null
|
||||
}
|
||||
],
|
||||
"indexes": []
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Client, Databases } from 'node-appwrite';
|
||||
import { Client, Databases, Models, Users } from 'node-appwrite';
|
||||
import { AppwriteRuntimeContext, AppwriteSendReturn } from './definitions.mjs';
|
||||
|
||||
const joinSession = async ({
|
||||
|
@ -13,6 +13,7 @@ const joinSession = async ({
|
|||
ctx: AppwriteRuntimeContext;
|
||||
}): Promise<AppwriteSendReturn> => {
|
||||
const databases = new Databases(client);
|
||||
const users = new Users(client);
|
||||
|
||||
let estimation;
|
||||
try {
|
||||
|
@ -31,16 +32,43 @@ const joinSession = async ({
|
|||
);
|
||||
}
|
||||
|
||||
let user: Models.User<Models.Preferences>;
|
||||
try {
|
||||
user = await users.get(userId);
|
||||
} catch (e) {
|
||||
error({ e });
|
||||
return res.json(
|
||||
{
|
||||
message: 'Failed to get user',
|
||||
},
|
||||
500,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const permissions: string[] = estimation['$permissions'];
|
||||
permissions.push(`read("user:${userId}")`);
|
||||
permissions.push(`update("user:${userId}")`);
|
||||
|
||||
const playerIds: string[] = estimation['playerIds'];
|
||||
playerIds.push(userId);
|
||||
|
||||
const players: string[] = estimation['players'];
|
||||
players.push(
|
||||
JSON.stringify({
|
||||
userId,
|
||||
name: user.name.length > 0 ? user.name : `Guest - ${userId}`,
|
||||
}),
|
||||
);
|
||||
|
||||
await databases.updateDocument(
|
||||
Bun.env.APPWRITE_DATABASE_ID,
|
||||
Bun.env.APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
|
||||
estimationId,
|
||||
{},
|
||||
{
|
||||
playerIds,
|
||||
players,
|
||||
},
|
||||
permissions,
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
# Directory used by Appwrite CLI for local development
|
||||
.appwrite
|
|
@ -0,0 +1,10 @@
|
|||
# Username Change Handler
|
||||
|
||||
This function is called when a user changes his username.
|
||||
|
||||
Currently this funcion updates users name accross all of his estimation sessions.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- APPWRITE_DATABASE_ID - Database Id
|
||||
- APPWRITE_ESTIMATION_SESSION_COLLECTION_ID - Sessions collection Id
|
|
@ -0,0 +1,7 @@
|
|||
module openruntimes/handler
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require github.com/open-runtimes/types-for-go/v4 v4.0.6
|
||||
|
||||
require github.com/appwrite/sdk-for-go v0.0.1-rc.2
|
|
@ -0,0 +1,4 @@
|
|||
github.com/appwrite/sdk-for-go v0.0.1-rc.2 h1:kh8p6OmSgA4d7aT1KXE9Z3W99ioDKdhhY1OrKsTLu1I=
|
||||
github.com/appwrite/sdk-for-go v0.0.1-rc.2/go.mod h1:aFiOAbfOzGS3811eMCt3T9WDBvjvPVAfOjw10Vghi4E=
|
||||
github.com/open-runtimes/types-for-go/v4 v4.0.6 h1:0Xf58LMy/vwWkiRN6BvvpWt1mWzcWUWQ5wsWSezG2TU=
|
||||
github.com/open-runtimes/types-for-go/v4 v4.0.6/go.mod h1:ab4mDSfgeG4kN8wWpaBSv0Ao3m9P6oEfN5gsXtx+iaI=
|
|
@ -0,0 +1,134 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/appwrite/sdk-for-go/appwrite"
|
||||
"github.com/appwrite/sdk-for-go/models"
|
||||
"github.com/appwrite/sdk-for-go/query"
|
||||
"github.com/open-runtimes/types-for-go/v4/openruntimes"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type EstimationSession struct {
|
||||
Id string `json:"$id"`
|
||||
UserID string `json:"userId"`
|
||||
Name string `json:"name"`
|
||||
Tickets []string `json:"tickets"`
|
||||
SessionState string `json:"sessionState"`
|
||||
Players []string `json:"players"`
|
||||
PlayerIDs []string `json:"playerIds"`
|
||||
}
|
||||
|
||||
type EstimationSessionList struct {
|
||||
Total int `json:"total"`
|
||||
Documents []EstimationSession `json:"documents"`
|
||||
}
|
||||
|
||||
type Player struct {
|
||||
UserID string `json:"userId"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func Main(Context openruntimes.Context) openruntimes.Response {
|
||||
databaseId := os.Getenv("APPWRITE_DATABASE_ID")
|
||||
collectionId := os.Getenv("APPWRITE_ESTIMATION_SESSION_COLLECTION_ID")
|
||||
if databaseId == "" || collectionId == "" {
|
||||
Context.Error("Environment variables not provided")
|
||||
return Context.Res.Json(ErrorResponse{
|
||||
Error: "Environment variables not provided",
|
||||
}, Context.Res.WithStatusCode(500))
|
||||
}
|
||||
|
||||
var userData models.User
|
||||
Context.Req.BodyJson(&userData)
|
||||
if userData.Id == "" {
|
||||
Context.Log("Request body did not contain id")
|
||||
return Context.Res.Json(ErrorResponse{
|
||||
Error: "User id was not provided",
|
||||
}, Context.Res.WithStatusCode(400))
|
||||
}
|
||||
|
||||
client := appwrite.NewClient(
|
||||
appwrite.WithEndpoint(os.Getenv("APPWRITE_FUNCTION_API_ENDPOINT")),
|
||||
appwrite.WithProject(os.Getenv("APPWRITE_FUNCTION_PROJECT_ID")),
|
||||
appwrite.WithKey(Context.Req.Headers["x-appwrite-key"]),
|
||||
)
|
||||
databases := appwrite.NewDatabases(client)
|
||||
|
||||
queries := []string{query.Contains("playerIds", userData.Id)}
|
||||
userEstimationSessions, err := databases.ListDocuments(databaseId, collectionId, databases.WithListDocumentsQueries(queries))
|
||||
if err != nil {
|
||||
Context.Error(err.Error())
|
||||
return Context.Res.Json(ErrorResponse{
|
||||
Error: err.Error(),
|
||||
}, Context.Res.WithStatusCode(500))
|
||||
}
|
||||
|
||||
var documents EstimationSessionList
|
||||
err = userEstimationSessions.Decode(&documents)
|
||||
if err != nil {
|
||||
Context.Error(err.Error())
|
||||
return Context.Res.Json(ErrorResponse{
|
||||
Error: err.Error(),
|
||||
}, Context.Res.WithStatusCode(500))
|
||||
}
|
||||
|
||||
newUsername := userData.Name
|
||||
if newUsername == "" {
|
||||
newUsername = "Guest - " + userData.Id
|
||||
}
|
||||
|
||||
for _, estimationSession := range documents.Documents {
|
||||
for i, jsonString := range estimationSession.Players {
|
||||
var player Player
|
||||
|
||||
err := json.Unmarshal([]byte(jsonString), &player)
|
||||
if err != nil {
|
||||
Context.Error(err.Error())
|
||||
return Context.Res.Json(ErrorResponse{
|
||||
Error: err.Error(),
|
||||
}, Context.Res.WithStatusCode(500))
|
||||
}
|
||||
|
||||
if player.UserID == userData.Id {
|
||||
player.Name = newUsername
|
||||
|
||||
updatedPlayer, err := json.Marshal(player)
|
||||
if err != nil {
|
||||
Context.Error(err.Error())
|
||||
return Context.Res.Json(ErrorResponse{
|
||||
Error: err.Error(),
|
||||
}, Context.Res.WithStatusCode(500))
|
||||
|
||||
}
|
||||
|
||||
estimationSession.Players[i] = string(updatedPlayer)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
patch := map[string]any{"players": estimationSession.Players}
|
||||
_, err := databases.UpdateDocument(databaseId, collectionId, estimationSession.Id, databases.WithUpdateDocumentData(patch))
|
||||
if err != nil {
|
||||
Context.Error(err.Error())
|
||||
return Context.Res.Json(ErrorResponse{
|
||||
Error: err.Error(),
|
||||
}, Context.Res.WithStatusCode(500))
|
||||
}
|
||||
|
||||
Context.Log("Updated estimation: ", estimationSession.Id)
|
||||
}
|
||||
|
||||
return Context.Res.Json(Response{
|
||||
Message: "Updated player name",
|
||||
})
|
||||
}
|
|
@ -3,13 +3,20 @@ import { useState } from 'react';
|
|||
import { useUser } from '../lib/context/user';
|
||||
|
||||
const Header = () => {
|
||||
const { logout } = useUser();
|
||||
const { current, isLoading, logout } = useUser();
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsDropdownOpen(!isDropdownOpen);
|
||||
};
|
||||
|
||||
if (isLoading || !current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userName =
|
||||
current.name.length > 0 ? current.name : `Guest - ${current.$id}`;
|
||||
|
||||
return (
|
||||
<header className="bg-white shadow-md transition-colors dark:bg-nero-700">
|
||||
<nav
|
||||
|
@ -30,7 +37,7 @@ const Header = () => {
|
|||
className="text-sm font-semibold leading-6 text-gray-900 dark:text-gray-100"
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
Account <span aria-hidden="true">↓</span>
|
||||
{userName} <span aria-hidden="true">↓</span>
|
||||
</button>
|
||||
|
||||
{isDropdownOpen && (
|
||||
|
|
|
@ -52,7 +52,7 @@ export const EstimationContextProvider = (props: PropsWithChildren) => {
|
|||
sessionId,
|
||||
)
|
||||
.then((payload) => {
|
||||
const userId = userData?.$id ?? ''; // TODO: Not sure if this is the user id or session
|
||||
const userId = userData?.$id ?? '';
|
||||
setCurrentSessionData(mapDatabaseToEntity(payload, { userId }));
|
||||
});
|
||||
|
||||
|
@ -61,7 +61,7 @@ export const EstimationContextProvider = (props: PropsWithChildren) => {
|
|||
`databases.${DATABASE_ID}.collections.${ESTIMATION_SESSION_COLLECTION_ID}.documents.${sessionId}`,
|
||||
],
|
||||
({ payload }) => {
|
||||
const userId = userData?.$id ?? ''; // TODO: Not sure if this is the user id or session
|
||||
const userId = userData?.$id ?? '';
|
||||
setCurrentSessionData(mapDatabaseToEntity(payload, { userId }));
|
||||
},
|
||||
);
|
||||
|
@ -98,7 +98,7 @@ export const EstimationContextProvider = (props: PropsWithChildren) => {
|
|||
};
|
||||
|
||||
const setVote = async (estimate: string) => {
|
||||
const userId = userData?.$id ?? ''; // TODO: Not sure if this is the user id or session
|
||||
const userId = userData?.$id ?? '';
|
||||
await updateSessionState({
|
||||
votes: currentSessionData?.sessionState.votes
|
||||
.filter((x) => x.userId !== userId)
|
||||
|
@ -106,6 +106,7 @@ export const EstimationContextProvider = (props: PropsWithChildren) => {
|
|||
{
|
||||
estimate: estimate,
|
||||
userId: userId,
|
||||
username: userData?.name ?? '',
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
|
|
@ -11,22 +11,17 @@ import {
|
|||
databases,
|
||||
ESTIMATION_SESSION_COLLECTION_ID,
|
||||
} from '../appwrite';
|
||||
import { ID, Models, Query } from 'appwrite';
|
||||
import { EntityModels } from '../types';
|
||||
import { mapDatabaseToEntity } from '../mappers/estimationSession';
|
||||
|
||||
interface EstimationSessionType extends Models.Document {
|
||||
userId: string;
|
||||
name: string;
|
||||
tickets: string[];
|
||||
sessionState: string;
|
||||
}
|
||||
import { ID, Query } from 'appwrite';
|
||||
import { DatabaseModels, EntityModels } from '../types';
|
||||
import {
|
||||
mapDatabaseToEntity,
|
||||
mapEntityToDatabase,
|
||||
} from '../mappers/estimationSession';
|
||||
import { useUser } from './user';
|
||||
|
||||
interface EstimationsListContextType {
|
||||
current: EntityModels.EstimationSession[];
|
||||
add: (
|
||||
estimationSession: Omit<EstimationSessionType, keyof Models.Document>,
|
||||
) => Promise<void>;
|
||||
add: (estimationSession: { name: string; userId?: string }) => Promise<void>;
|
||||
remove: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
|
@ -39,19 +34,38 @@ export function useEstimationsList() {
|
|||
}
|
||||
|
||||
export function EstimationsListContextProvider(props: PropsWithChildren) {
|
||||
const { current: userData } = useUser();
|
||||
const [estimationSessions, setEstimationSessions] = useState<
|
||||
EntityModels.EstimationSession[]
|
||||
>([]);
|
||||
|
||||
const add = async (
|
||||
estimationSession: Omit<EstimationSessionType, keyof Models.Document>,
|
||||
) => {
|
||||
const response = await databases.createDocument<EstimationSessionType>(
|
||||
DATABASE_ID,
|
||||
ESTIMATION_SESSION_COLLECTION_ID,
|
||||
ID.unique(),
|
||||
estimationSession,
|
||||
);
|
||||
const add = async (estimationSession: { name: string; userId?: string }) => {
|
||||
if (!userData) {
|
||||
throw Error('Tried to create new estimation with no user context');
|
||||
}
|
||||
|
||||
const username =
|
||||
userData.name.length > 0 ? userData.name : `Guest - ${userData.$id}`;
|
||||
|
||||
const newEstimationSession: Partial<EntityModels.EstimationSession> = {
|
||||
name: estimationSession.name,
|
||||
userId: userData.$id,
|
||||
playerIds: [userData.$id],
|
||||
players: [
|
||||
{
|
||||
userId: userData.$id,
|
||||
name: username,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response =
|
||||
await databases.createDocument<DatabaseModels.EstimationSession>(
|
||||
DATABASE_ID,
|
||||
ESTIMATION_SESSION_COLLECTION_ID,
|
||||
ID.unique(),
|
||||
mapEntityToDatabase(newEstimationSession),
|
||||
);
|
||||
setEstimationSessions((estimationSessions) =>
|
||||
[mapDatabaseToEntity(response, {}), ...estimationSessions].slice(0, 10),
|
||||
);
|
||||
|
@ -72,11 +86,12 @@ export function EstimationsListContextProvider(props: PropsWithChildren) {
|
|||
};
|
||||
|
||||
const init = async () => {
|
||||
const response = await databases.listDocuments<EstimationSessionType>(
|
||||
DATABASE_ID,
|
||||
ESTIMATION_SESSION_COLLECTION_ID,
|
||||
[Query.orderDesc('$createdAt'), Query.limit(10)],
|
||||
);
|
||||
const response =
|
||||
await databases.listDocuments<DatabaseModels.EstimationSession>(
|
||||
DATABASE_ID,
|
||||
ESTIMATION_SESSION_COLLECTION_ID,
|
||||
[Query.orderDesc('$createdAt'), Query.limit(10)],
|
||||
);
|
||||
setEstimationSessions(
|
||||
response.documents.map((document) => mapDatabaseToEntity(document, {})),
|
||||
);
|
||||
|
@ -85,7 +100,7 @@ export function EstimationsListContextProvider(props: PropsWithChildren) {
|
|||
useEffect(() => {
|
||||
init();
|
||||
|
||||
return client.subscribe<EstimationSessionType>(
|
||||
return client.subscribe<DatabaseModels.EstimationSession>(
|
||||
[
|
||||
`databases.${DATABASE_ID}.collections.${ESTIMATION_SESSION_COLLECTION_ID}.documents`,
|
||||
],
|
||||
|
|
|
@ -9,12 +9,13 @@ import {
|
|||
import { account } from '../appwrite';
|
||||
|
||||
export interface UserContextType {
|
||||
current: Models.Session | Models.User<Models.Preferences> | null;
|
||||
current: Models.User<Models.Preferences> | null;
|
||||
isLoading: boolean;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
register: (email: string, password: string) => Promise<void>;
|
||||
loginAsGuest: () => Promise<void>;
|
||||
updateUsername: (username: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const UserContext = createContext<UserContextType | undefined>(undefined);
|
||||
|
@ -28,14 +29,15 @@ export const useUser = () => {
|
|||
};
|
||||
|
||||
export const UserProvider = (props: PropsWithChildren) => {
|
||||
const [user, setUser] = useState<
|
||||
Models.Session | Models.User<Models.Preferences> | null
|
||||
>(null);
|
||||
const [user, setUser] = useState<Models.User<Models.Preferences> | null>(
|
||||
null,
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
const loggedIn = await account.createEmailPasswordSession(email, password);
|
||||
setUser(loggedIn);
|
||||
await account.createEmailPasswordSession(email, password);
|
||||
const userData = await account.get();
|
||||
setUser(userData);
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const redirectPath = params.get('redirect');
|
||||
|
@ -54,14 +56,20 @@ export const UserProvider = (props: PropsWithChildren) => {
|
|||
};
|
||||
|
||||
const loginAsGuest = async () => {
|
||||
const session = await account.createAnonymousSession();
|
||||
setUser(session);
|
||||
await account.createAnonymousSession();
|
||||
const userData = await account.get();
|
||||
setUser(userData);
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const redirectPath = params.get('redirect');
|
||||
window.location.replace(redirectPath || '/');
|
||||
};
|
||||
|
||||
const updateUsername = async (username: string) => {
|
||||
const user = await account.updateName(username);
|
||||
setUser(user);
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
const loggedIn = await account.get();
|
||||
|
@ -86,6 +94,7 @@ export const UserProvider = (props: PropsWithChildren) => {
|
|||
logout,
|
||||
register,
|
||||
loginAsGuest,
|
||||
updateUsername,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
|
|
@ -16,13 +16,26 @@ export const mapDatabaseToEntity = (
|
|||
)
|
||||
: [];
|
||||
|
||||
const players = data.players
|
||||
? data.players.map<EntityModels.Player>((user) => JSON.parse(user))
|
||||
: [];
|
||||
|
||||
const votes = sessionState.votes.map<EntityModels.PlayerVote>((vote) => ({
|
||||
...vote,
|
||||
username:
|
||||
players.find((x) => x.userId === vote.userId)?.name ?? vote.userId,
|
||||
}));
|
||||
|
||||
const result: EntityModels.EstimationSession = {
|
||||
id: data.$id,
|
||||
name: data.name,
|
||||
userId: data.userId,
|
||||
tickets,
|
||||
players,
|
||||
playerIds: data.playerIds,
|
||||
sessionState: {
|
||||
...sessionState,
|
||||
votes,
|
||||
currentPlayerVote: sessionState.votes.find((x) => x.userId === userId)
|
||||
?.estimate,
|
||||
currentTicket: tickets.find((x) => x.id === sessionState.currentTicketId),
|
||||
|
@ -31,3 +44,19 @@ export const mapDatabaseToEntity = (
|
|||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const mapEntityToDatabase = (
|
||||
data: Partial<EntityModels.EstimationSession>,
|
||||
) => {
|
||||
const result: Partial<DatabaseModels.EstimationSession> = {
|
||||
$id: data.id,
|
||||
name: data.name,
|
||||
userId: data.userId,
|
||||
tickets: data.tickets?.map((ticket) => JSON.stringify(ticket)),
|
||||
playerIds: data.playerIds,
|
||||
players: data.players?.map((player) => JSON.stringify(player)),
|
||||
sessionState: JSON.stringify(data.sessionState),
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
|
|
|
@ -5,6 +5,8 @@ interface EstimationSession extends Models.Document {
|
|||
name: string;
|
||||
tickets: string[];
|
||||
sessionState: string;
|
||||
players: string[];
|
||||
playerIds: string[];
|
||||
}
|
||||
|
||||
export type { EstimationSession };
|
||||
|
|
|
@ -4,6 +4,8 @@ interface EstimationSession {
|
|||
name: string;
|
||||
tickets: EstimationSessionTicket[];
|
||||
sessionState: SessionState;
|
||||
players: Player[];
|
||||
playerIds: string[];
|
||||
}
|
||||
|
||||
interface EstimationSessionTicket {
|
||||
|
@ -21,12 +23,19 @@ interface SessionState {
|
|||
|
||||
interface PlayerVote {
|
||||
userId: string;
|
||||
username: string;
|
||||
estimate: string;
|
||||
}
|
||||
|
||||
interface Player {
|
||||
userId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type {
|
||||
EstimationSession,
|
||||
EstimationSessionTicket,
|
||||
Player,
|
||||
SessionState,
|
||||
PlayerVote,
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ import VoteList from './components/VoteList';
|
|||
import { Button, ButtonColor, Drawer } from '../../components';
|
||||
import CreateTicketForm from './components/CreateTicketForm';
|
||||
import CopyInput from '../../components/CopyInput';
|
||||
import PlayerList from './components/PlayerList';
|
||||
|
||||
const fibonacciSequence = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 100];
|
||||
|
||||
|
@ -35,6 +36,7 @@ const Estimation: React.FC = () => {
|
|||
currentPlayerVote,
|
||||
currentTicket,
|
||||
},
|
||||
players,
|
||||
},
|
||||
} = estimationState;
|
||||
|
||||
|
@ -84,6 +86,8 @@ const Estimation: React.FC = () => {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<PlayerList players={players ?? []} />
|
||||
|
||||
<Drawer isOpen={isDrawerOpen} onClose={() => setDrawerOpen(false)}>
|
||||
<CreateTicketForm
|
||||
onCreate={async (ticket) => {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import { EntityModels } from '../../../lib/types';
|
||||
|
||||
interface PlayerListProps {
|
||||
players: EntityModels.Player[];
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const PlayerList: React.FC<PlayerListProps> = ({
|
||||
players,
|
||||
title = 'Players',
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-full max-w-sm rounded-lg 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">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<ul className="max-h-48 divide-y divide-gray-300 overflow-y-auto dark:divide-gray-600">
|
||||
{players.length > 0 ? (
|
||||
players.map((player, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="py-2 text-sm text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{player.name}
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<li className="text-sm text-gray-500 dark:text-gray-400">
|
||||
No players available
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayerList;
|
|
@ -16,7 +16,7 @@ const VoteList: React.FC<VoteListProps> = ({ className, votes, revealed }) => {
|
|||
itemComponent={({ item }, idx) => (
|
||||
<Card
|
||||
key={idx}
|
||||
title={item.userId}
|
||||
title={item.username}
|
||||
description={revealed ? item.estimate : 'Hidden'}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -1,5 +1,52 @@
|
|||
import { useForm } from '@tanstack/react-form';
|
||||
import { Button, Input } from '../components';
|
||||
import { useUser } from '../lib/context/user';
|
||||
|
||||
const Profile = () => {
|
||||
return <p>TODO</p>;
|
||||
const user = useUser();
|
||||
const updateUsernameForm = useForm({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
await user.updateUsername(value.name);
|
||||
updateUsernameForm.reset();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<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">
|
||||
<h1 className="mb-6 text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
Update Name
|
||||
</h1>
|
||||
<form
|
||||
className="space-y-6"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
updateUsernameForm.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<updateUsernameForm.Field
|
||||
name="name"
|
||||
children={(field) => (
|
||||
<Input
|
||||
label="Name"
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" fullWidth>
|
||||
Update
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
|
|
Loading…
Reference in New Issue