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",
|
"entrypoint": "src/main.ts",
|
||||||
"commands": "bun install",
|
"commands": "bun install",
|
||||||
"path": "functions/EstimationSessionInvite"
|
"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": [
|
"databases": [
|
||||||
|
@ -101,6 +116,22 @@
|
||||||
"array": false,
|
"array": false,
|
||||||
"size": 1000,
|
"size": 1000,
|
||||||
"default": null
|
"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": []
|
"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';
|
import { AppwriteRuntimeContext, AppwriteSendReturn } from './definitions.mjs';
|
||||||
|
|
||||||
const joinSession = async ({
|
const joinSession = async ({
|
||||||
|
@ -13,6 +13,7 @@ const joinSession = async ({
|
||||||
ctx: AppwriteRuntimeContext;
|
ctx: AppwriteRuntimeContext;
|
||||||
}): Promise<AppwriteSendReturn> => {
|
}): Promise<AppwriteSendReturn> => {
|
||||||
const databases = new Databases(client);
|
const databases = new Databases(client);
|
||||||
|
const users = new Users(client);
|
||||||
|
|
||||||
let estimation;
|
let estimation;
|
||||||
try {
|
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 {
|
try {
|
||||||
const permissions: string[] = estimation['$permissions'];
|
const permissions: string[] = estimation['$permissions'];
|
||||||
permissions.push(`read("user:${userId}")`);
|
permissions.push(`read("user:${userId}")`);
|
||||||
permissions.push(`update("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(
|
await databases.updateDocument(
|
||||||
Bun.env.APPWRITE_DATABASE_ID,
|
Bun.env.APPWRITE_DATABASE_ID,
|
||||||
Bun.env.APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
|
Bun.env.APPWRITE_ESTIMATION_SESSION_COLLECTION_ID,
|
||||||
estimationId,
|
estimationId,
|
||||||
{},
|
{
|
||||||
|
playerIds,
|
||||||
|
players,
|
||||||
|
},
|
||||||
permissions,
|
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';
|
import { useUser } from '../lib/context/user';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { logout } = useUser();
|
const { current, isLoading, logout } = useUser();
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
setIsDropdownOpen(!isDropdownOpen);
|
setIsDropdownOpen(!isDropdownOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isLoading || !current) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userName =
|
||||||
|
current.name.length > 0 ? current.name : `Guest - ${current.$id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-white shadow-md transition-colors dark:bg-nero-700">
|
<header className="bg-white shadow-md transition-colors dark:bg-nero-700">
|
||||||
<nav
|
<nav
|
||||||
|
@ -30,7 +37,7 @@ const Header = () => {
|
||||||
className="text-sm font-semibold leading-6 text-gray-900 dark:text-gray-100"
|
className="text-sm font-semibold leading-6 text-gray-900 dark:text-gray-100"
|
||||||
onClick={toggleDropdown}
|
onClick={toggleDropdown}
|
||||||
>
|
>
|
||||||
Account <span aria-hidden="true">↓</span>
|
{userName} <span aria-hidden="true">↓</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isDropdownOpen && (
|
{isDropdownOpen && (
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const EstimationContextProvider = (props: PropsWithChildren) => {
|
||||||
sessionId,
|
sessionId,
|
||||||
)
|
)
|
||||||
.then((payload) => {
|
.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 }));
|
setCurrentSessionData(mapDatabaseToEntity(payload, { userId }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ export const EstimationContextProvider = (props: PropsWithChildren) => {
|
||||||
`databases.${DATABASE_ID}.collections.${ESTIMATION_SESSION_COLLECTION_ID}.documents.${sessionId}`,
|
`databases.${DATABASE_ID}.collections.${ESTIMATION_SESSION_COLLECTION_ID}.documents.${sessionId}`,
|
||||||
],
|
],
|
||||||
({ payload }) => {
|
({ payload }) => {
|
||||||
const userId = userData?.$id ?? ''; // TODO: Not sure if this is the user id or session
|
const userId = userData?.$id ?? '';
|
||||||
setCurrentSessionData(mapDatabaseToEntity(payload, { userId }));
|
setCurrentSessionData(mapDatabaseToEntity(payload, { userId }));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -98,7 +98,7 @@ export const EstimationContextProvider = (props: PropsWithChildren) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const setVote = async (estimate: string) => {
|
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({
|
await updateSessionState({
|
||||||
votes: currentSessionData?.sessionState.votes
|
votes: currentSessionData?.sessionState.votes
|
||||||
.filter((x) => x.userId !== userId)
|
.filter((x) => x.userId !== userId)
|
||||||
|
@ -106,6 +106,7 @@ export const EstimationContextProvider = (props: PropsWithChildren) => {
|
||||||
{
|
{
|
||||||
estimate: estimate,
|
estimate: estimate,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
username: userData?.name ?? '',
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,22 +11,17 @@ import {
|
||||||
databases,
|
databases,
|
||||||
ESTIMATION_SESSION_COLLECTION_ID,
|
ESTIMATION_SESSION_COLLECTION_ID,
|
||||||
} from '../appwrite';
|
} from '../appwrite';
|
||||||
import { ID, Models, Query } from 'appwrite';
|
import { ID, Query } from 'appwrite';
|
||||||
import { EntityModels } from '../types';
|
import { DatabaseModels, EntityModels } from '../types';
|
||||||
import { mapDatabaseToEntity } from '../mappers/estimationSession';
|
import {
|
||||||
|
mapDatabaseToEntity,
|
||||||
interface EstimationSessionType extends Models.Document {
|
mapEntityToDatabase,
|
||||||
userId: string;
|
} from '../mappers/estimationSession';
|
||||||
name: string;
|
import { useUser } from './user';
|
||||||
tickets: string[];
|
|
||||||
sessionState: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EstimationsListContextType {
|
interface EstimationsListContextType {
|
||||||
current: EntityModels.EstimationSession[];
|
current: EntityModels.EstimationSession[];
|
||||||
add: (
|
add: (estimationSession: { name: string; userId?: string }) => Promise<void>;
|
||||||
estimationSession: Omit<EstimationSessionType, keyof Models.Document>,
|
|
||||||
) => Promise<void>;
|
|
||||||
remove: (id: string) => Promise<void>;
|
remove: (id: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,19 +34,38 @@ export function useEstimationsList() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EstimationsListContextProvider(props: PropsWithChildren) {
|
export function EstimationsListContextProvider(props: PropsWithChildren) {
|
||||||
|
const { current: userData } = useUser();
|
||||||
const [estimationSessions, setEstimationSessions] = useState<
|
const [estimationSessions, setEstimationSessions] = useState<
|
||||||
EntityModels.EstimationSession[]
|
EntityModels.EstimationSession[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const add = async (
|
const add = async (estimationSession: { name: string; userId?: string }) => {
|
||||||
estimationSession: Omit<EstimationSessionType, keyof Models.Document>,
|
if (!userData) {
|
||||||
) => {
|
throw Error('Tried to create new estimation with no user context');
|
||||||
const response = await databases.createDocument<EstimationSessionType>(
|
}
|
||||||
DATABASE_ID,
|
|
||||||
ESTIMATION_SESSION_COLLECTION_ID,
|
const username =
|
||||||
ID.unique(),
|
userData.name.length > 0 ? userData.name : `Guest - ${userData.$id}`;
|
||||||
estimationSession,
|
|
||||||
);
|
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) =>
|
setEstimationSessions((estimationSessions) =>
|
||||||
[mapDatabaseToEntity(response, {}), ...estimationSessions].slice(0, 10),
|
[mapDatabaseToEntity(response, {}), ...estimationSessions].slice(0, 10),
|
||||||
);
|
);
|
||||||
|
@ -72,11 +86,12 @@ export function EstimationsListContextProvider(props: PropsWithChildren) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
const response = await databases.listDocuments<EstimationSessionType>(
|
const response =
|
||||||
DATABASE_ID,
|
await databases.listDocuments<DatabaseModels.EstimationSession>(
|
||||||
ESTIMATION_SESSION_COLLECTION_ID,
|
DATABASE_ID,
|
||||||
[Query.orderDesc('$createdAt'), Query.limit(10)],
|
ESTIMATION_SESSION_COLLECTION_ID,
|
||||||
);
|
[Query.orderDesc('$createdAt'), Query.limit(10)],
|
||||||
|
);
|
||||||
setEstimationSessions(
|
setEstimationSessions(
|
||||||
response.documents.map((document) => mapDatabaseToEntity(document, {})),
|
response.documents.map((document) => mapDatabaseToEntity(document, {})),
|
||||||
);
|
);
|
||||||
|
@ -85,7 +100,7 @@ export function EstimationsListContextProvider(props: PropsWithChildren) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
init();
|
init();
|
||||||
|
|
||||||
return client.subscribe<EstimationSessionType>(
|
return client.subscribe<DatabaseModels.EstimationSession>(
|
||||||
[
|
[
|
||||||
`databases.${DATABASE_ID}.collections.${ESTIMATION_SESSION_COLLECTION_ID}.documents`,
|
`databases.${DATABASE_ID}.collections.${ESTIMATION_SESSION_COLLECTION_ID}.documents`,
|
||||||
],
|
],
|
||||||
|
|
|
@ -9,12 +9,13 @@ import {
|
||||||
import { account } from '../appwrite';
|
import { account } from '../appwrite';
|
||||||
|
|
||||||
export interface UserContextType {
|
export interface UserContextType {
|
||||||
current: Models.Session | Models.User<Models.Preferences> | null;
|
current: Models.User<Models.Preferences> | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
login: (email: string, password: string) => Promise<void>;
|
login: (email: string, password: string) => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
register: (email: string, password: string) => Promise<void>;
|
register: (email: string, password: string) => Promise<void>;
|
||||||
loginAsGuest: () => Promise<void>;
|
loginAsGuest: () => Promise<void>;
|
||||||
|
updateUsername: (username: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserContext = createContext<UserContextType | undefined>(undefined);
|
const UserContext = createContext<UserContextType | undefined>(undefined);
|
||||||
|
@ -28,14 +29,15 @@ export const useUser = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserProvider = (props: PropsWithChildren) => {
|
export const UserProvider = (props: PropsWithChildren) => {
|
||||||
const [user, setUser] = useState<
|
const [user, setUser] = useState<Models.User<Models.Preferences> | null>(
|
||||||
Models.Session | Models.User<Models.Preferences> | null
|
null,
|
||||||
>(null);
|
);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const login = async (email: string, password: string) => {
|
const login = async (email: string, password: string) => {
|
||||||
const loggedIn = await account.createEmailPasswordSession(email, password);
|
await account.createEmailPasswordSession(email, password);
|
||||||
setUser(loggedIn);
|
const userData = await account.get();
|
||||||
|
setUser(userData);
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const redirectPath = params.get('redirect');
|
const redirectPath = params.get('redirect');
|
||||||
|
@ -54,14 +56,20 @@ export const UserProvider = (props: PropsWithChildren) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const loginAsGuest = async () => {
|
const loginAsGuest = async () => {
|
||||||
const session = await account.createAnonymousSession();
|
await account.createAnonymousSession();
|
||||||
setUser(session);
|
const userData = await account.get();
|
||||||
|
setUser(userData);
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const redirectPath = params.get('redirect');
|
const redirectPath = params.get('redirect');
|
||||||
window.location.replace(redirectPath || '/');
|
window.location.replace(redirectPath || '/');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateUsername = async (username: string) => {
|
||||||
|
const user = await account.updateName(username);
|
||||||
|
setUser(user);
|
||||||
|
};
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
try {
|
try {
|
||||||
const loggedIn = await account.get();
|
const loggedIn = await account.get();
|
||||||
|
@ -86,6 +94,7 @@ export const UserProvider = (props: PropsWithChildren) => {
|
||||||
logout,
|
logout,
|
||||||
register,
|
register,
|
||||||
loginAsGuest,
|
loginAsGuest,
|
||||||
|
updateUsername,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{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 = {
|
const result: EntityModels.EstimationSession = {
|
||||||
id: data.$id,
|
id: data.$id,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
userId: data.userId,
|
userId: data.userId,
|
||||||
tickets,
|
tickets,
|
||||||
|
players,
|
||||||
|
playerIds: data.playerIds,
|
||||||
sessionState: {
|
sessionState: {
|
||||||
...sessionState,
|
...sessionState,
|
||||||
|
votes,
|
||||||
currentPlayerVote: sessionState.votes.find((x) => x.userId === userId)
|
currentPlayerVote: sessionState.votes.find((x) => x.userId === userId)
|
||||||
?.estimate,
|
?.estimate,
|
||||||
currentTicket: tickets.find((x) => x.id === sessionState.currentTicketId),
|
currentTicket: tickets.find((x) => x.id === sessionState.currentTicketId),
|
||||||
|
@ -31,3 +44,19 @@ export const mapDatabaseToEntity = (
|
||||||
|
|
||||||
return result;
|
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;
|
name: string;
|
||||||
tickets: string[];
|
tickets: string[];
|
||||||
sessionState: string;
|
sessionState: string;
|
||||||
|
players: string[];
|
||||||
|
playerIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { EstimationSession };
|
export type { EstimationSession };
|
||||||
|
|
|
@ -4,6 +4,8 @@ interface EstimationSession {
|
||||||
name: string;
|
name: string;
|
||||||
tickets: EstimationSessionTicket[];
|
tickets: EstimationSessionTicket[];
|
||||||
sessionState: SessionState;
|
sessionState: SessionState;
|
||||||
|
players: Player[];
|
||||||
|
playerIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EstimationSessionTicket {
|
interface EstimationSessionTicket {
|
||||||
|
@ -21,12 +23,19 @@ interface SessionState {
|
||||||
|
|
||||||
interface PlayerVote {
|
interface PlayerVote {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
username: string;
|
||||||
estimate: string;
|
estimate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Player {
|
||||||
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
EstimationSession,
|
EstimationSession,
|
||||||
EstimationSessionTicket,
|
EstimationSessionTicket,
|
||||||
|
Player,
|
||||||
SessionState,
|
SessionState,
|
||||||
PlayerVote,
|
PlayerVote,
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,6 +7,7 @@ import VoteList from './components/VoteList';
|
||||||
import { Button, ButtonColor, Drawer } from '../../components';
|
import { Button, ButtonColor, Drawer } from '../../components';
|
||||||
import CreateTicketForm from './components/CreateTicketForm';
|
import CreateTicketForm from './components/CreateTicketForm';
|
||||||
import CopyInput from '../../components/CopyInput';
|
import CopyInput from '../../components/CopyInput';
|
||||||
|
import PlayerList from './components/PlayerList';
|
||||||
|
|
||||||
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];
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@ const Estimation: React.FC = () => {
|
||||||
currentPlayerVote,
|
currentPlayerVote,
|
||||||
currentTicket,
|
currentTicket,
|
||||||
},
|
},
|
||||||
|
players,
|
||||||
},
|
},
|
||||||
} = estimationState;
|
} = estimationState;
|
||||||
|
|
||||||
|
@ -84,6 +86,8 @@ const Estimation: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<PlayerList players={players ?? []} />
|
||||||
|
|
||||||
<Drawer isOpen={isDrawerOpen} onClose={() => setDrawerOpen(false)}>
|
<Drawer isOpen={isDrawerOpen} onClose={() => setDrawerOpen(false)}>
|
||||||
<CreateTicketForm
|
<CreateTicketForm
|
||||||
onCreate={async (ticket) => {
|
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) => (
|
itemComponent={({ item }, idx) => (
|
||||||
<Card
|
<Card
|
||||||
key={idx}
|
key={idx}
|
||||||
title={item.userId}
|
title={item.username}
|
||||||
description={revealed ? item.estimate : 'Hidden'}
|
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 = () => {
|
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;
|
export default Profile;
|
||||||
|
|
Loading…
Reference in New Issue