From 9710b6b3e85d0a10663254bc0dcc3738651b1e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Thu, 22 Aug 2024 09:17:06 +0300 Subject: [PATCH 1/5] Create first guilds --- arccode.dev/firestore.rules | 14 ++++ arccode.dev/public/sitemap.xml | 28 ++++--- .../app/administrator.../emails/layout.tsx | 3 + .../app/administrator.../guilds/layout.tsx | 3 + .../src/app/administrator.../guilds/page.tsx | 3 + .../administrator/AdministratorLayout.tsx | 25 +++++-- .../administrator/DevelopmentBouncer.tsx | 15 ++++ .../administrator/guilds/Guilds.tsx | 75 +++++++++++++++++++ .../src/components/guild/GuildsProvider.tsx | 52 +++++++++++++ .../src/contexts/guild/GuildsContext.ts | 13 ++++ .../subRouters/AdministratorSubRouter.tsx | 18 ++++- arccode.dev/src/types.ts | 16 ++-- arccode.dev/src/utils/db/createUser.ts | 2 + 13 files changed, 240 insertions(+), 27 deletions(-) create mode 100644 arccode.dev/src/app/administrator.../emails/layout.tsx create mode 100644 arccode.dev/src/app/administrator.../guilds/layout.tsx create mode 100644 arccode.dev/src/app/administrator.../guilds/page.tsx create mode 100644 arccode.dev/src/components/administrator/DevelopmentBouncer.tsx create mode 100644 arccode.dev/src/components/administrator/guilds/Guilds.tsx create mode 100644 arccode.dev/src/components/guild/GuildsProvider.tsx create mode 100644 arccode.dev/src/contexts/guild/GuildsContext.ts diff --git a/arccode.dev/firestore.rules b/arccode.dev/firestore.rules index ba78032..eb14a48 100644 --- a/arccode.dev/firestore.rules +++ b/arccode.dev/firestore.rules @@ -27,6 +27,14 @@ service cloud.firestore { || resource.data.character.unlockedItems[request.resource.data.character[type]] > 0 } + function isGuildPublicOrHasMember() { + return !resource.data.isPrivate || (request.auth != null && request.auth.uid in resource.data.members); + } + + function isGuildAdministrator() { + return request.auth != null && request.auth.uid in resource.data.administratorIds; + } + match /users/{userId} { allow read: if true; allow create: if request.auth != null && request.auth.uid == userId && !request.resource.data.isAdministrator; @@ -49,5 +57,11 @@ service cloud.firestore { && hasUnlockedItem('spell4ItemId'); } + match /guilds/{huildId} { + allow read: if request.auth != null && isGuildPublicOrHasMember(); + allow create: if request.auth != null && isAdministrator(); + allow update, delete: if request.auth != null && (isAdministrator() || isGuildAdministrator()); + } + } } diff --git a/arccode.dev/public/sitemap.xml b/arccode.dev/public/sitemap.xml index 78da7e3..1033a13 100644 --- a/arccode.dev/public/sitemap.xml +++ b/arccode.dev/public/sitemap.xml @@ -2,51 +2,55 @@ https://arccode.dev/ - 2024-08-19 + 2024-08-22 https://arccode.dev/authentication - 2024-08-19 + 2024-08-22 https://arccode.dev/authentication/password-reset - 2024-08-19 + 2024-08-22 https://arccode.dev/administrator - 2024-08-19 + 2024-08-22 https://arccode.dev/administrator/emails - 2024-08-19 + 2024-08-22 https://arccode.dev/administrator/users - 2024-08-19 + 2024-08-22 + + + https://arccode.dev/administrator/guilds + 2024-08-22 https://arccode.dev/~ - 2024-08-19 + 2024-08-22 https://arccode.dev/support - 2024-08-19 + 2024-08-22 https://arccode.dev/legal - 2024-08-19 + 2024-08-22 https://arccode.dev/onboarding - 2024-08-19 + 2024-08-22 https://arccode.dev/onboarding/install-extension - 2024-08-19 + 2024-08-22 https://arccode.dev/onboarding/start-adventure - 2024-08-19 + 2024-08-22 diff --git a/arccode.dev/src/app/administrator.../emails/layout.tsx b/arccode.dev/src/app/administrator.../emails/layout.tsx new file mode 100644 index 0000000..3880711 --- /dev/null +++ b/arccode.dev/src/app/administrator.../emails/layout.tsx @@ -0,0 +1,3 @@ +import DevelopmentBouncer from '~components/administrator/DevelopmentBouncer' + +export default DevelopmentBouncer diff --git a/arccode.dev/src/app/administrator.../guilds/layout.tsx b/arccode.dev/src/app/administrator.../guilds/layout.tsx new file mode 100644 index 0000000..3880711 --- /dev/null +++ b/arccode.dev/src/app/administrator.../guilds/layout.tsx @@ -0,0 +1,3 @@ +import DevelopmentBouncer from '~components/administrator/DevelopmentBouncer' + +export default DevelopmentBouncer diff --git a/arccode.dev/src/app/administrator.../guilds/page.tsx b/arccode.dev/src/app/administrator.../guilds/page.tsx new file mode 100644 index 0000000..84cfd91 --- /dev/null +++ b/arccode.dev/src/app/administrator.../guilds/page.tsx @@ -0,0 +1,3 @@ +import Guilds from '~components/administrator/guilds/Guilds' + +export default Guilds diff --git a/arccode.dev/src/components/administrator/AdministratorLayout.tsx b/arccode.dev/src/components/administrator/AdministratorLayout.tsx index 68afcf0..1394bf9 100644 --- a/arccode.dev/src/components/administrator/AdministratorLayout.tsx +++ b/arccode.dev/src/components/administrator/AdministratorLayout.tsx @@ -11,13 +11,24 @@ function AdministratorLayout({ children }: PropsWithChildren) { > Users - {' - '} - - Emails - + {import.meta.env.DEV && ( + <> + {' - '} + + Emails + + {' - '} + + Guilds + + + )} {children} diff --git a/arccode.dev/src/components/administrator/DevelopmentBouncer.tsx b/arccode.dev/src/components/administrator/DevelopmentBouncer.tsx new file mode 100644 index 0000000..b6633ed --- /dev/null +++ b/arccode.dev/src/components/administrator/DevelopmentBouncer.tsx @@ -0,0 +1,15 @@ +import type { PropsWithChildren } from 'react' + +function DevelopmentBouncer({ children }: PropsWithChildren) { + if (!import.meta.env.DEV) { + return ( +
+ Available in development mode only. +
+ ) + } + + return children as JSX.Element +} + +export default DevelopmentBouncer diff --git a/arccode.dev/src/components/administrator/guilds/Guilds.tsx b/arccode.dev/src/components/administrator/guilds/Guilds.tsx new file mode 100644 index 0000000..c2641eb --- /dev/null +++ b/arccode.dev/src/components/administrator/guilds/Guilds.tsx @@ -0,0 +1,75 @@ +import { doc, writeBatch } from 'firebase/firestore' +import { useCallback, useState } from 'react' +import { nanoid } from 'nanoid' + +import type { Guild } from '~types' + +import { db } from '~firebase' + +import useUser from '~hooks/user/useUser' + +import { Button } from '~components/ui/Button' +import Spinner from '~components/common/Spinner' + +function Guilds() { + const { user } = useUser() + + const [loading, setLoading] = useState(false) + const [createGuildSuccess, setCreateGuildSuccess] = useState(false) + + const handleCreateGuilds = useCallback(async (n: number) => { + if (!user) return + + setLoading(true) + setCreateGuildSuccess(false) + + const batch = writeBatch(db) + + for (let i = 0; i < n; i++) { + const now = new Date().toISOString() + const guild: Guild = { + id: nanoid(), + emoji: '๐ŸŽ‰', + name: `Guild ${i}`, + description: 'Lorem ipsum', + isPrivate: Math.random() < 0.1, + administratorIds: [user.id], + moderatorIds: [user.id], + memberIds: [user.id], + userId: user.id, + createdAt: now, + updatedAt: now, + deletedAt: '', + } + + batch.set(doc(db, 'guilds', guild.id), guild) + } + + await batch.commit() + + setLoading(false) + setCreateGuildSuccess(true) + }, [ + user, + ]) + + return ( +
+
+ + {loading && ( + + )} + {createGuildSuccess && ( +
+ Guilds created successfully +
+ )} +
+
+ ) +} + +export default Guilds diff --git a/arccode.dev/src/components/guild/GuildsProvider.tsx b/arccode.dev/src/components/guild/GuildsProvider.tsx new file mode 100644 index 0000000..9a735bd --- /dev/null +++ b/arccode.dev/src/components/guild/GuildsProvider.tsx @@ -0,0 +1,52 @@ +import { type PropsWithChildren, useCallback, useMemo } from 'react' +import { collection, query } from 'firebase/firestore' + +import type { Guild } from '~types' + +import { db } from '~firebase' + +import type { GuildsContextType } from '~contexts/guild/GuildsContext' +import GuildsContext from '~contexts/guild/GuildsContext' + +import useDocuments from '~hooks/db/useDocuments' + +import SpinnerCentered from '~components/common/CenteredSpinner' + +function GuildsProvider({ children }: PropsWithChildren) { + const q = useMemo(() => query(collection(db, 'guilds')), []) + const { data: guilds, loading, error } = useDocuments(q) + + const createGuild = useCallback(async (name: string) => { + console.log('name', name) + }, []) + + const guildsContextValue = useMemo(() => ({ + guilds: guilds ?? [], + createGuild, + }), [ + guilds, + createGuild, + ]) + + if (loading) { + return ( + + ) + } + + if (error) { + return ( +
+ An error occurred. +
+ ) + } + + return ( + + {children} + + ) +} + +export default GuildsProvider diff --git a/arccode.dev/src/contexts/guild/GuildsContext.ts b/arccode.dev/src/contexts/guild/GuildsContext.ts new file mode 100644 index 0000000..fcf49f2 --- /dev/null +++ b/arccode.dev/src/contexts/guild/GuildsContext.ts @@ -0,0 +1,13 @@ +import { createContext } from 'react' + +import type { Guild } from '~types' + +export type GuildsContextType = { + guilds: Guild[] + createGuild: (name: string) => Promise +} + +export default createContext({ + guilds: [], + createGuild: async () => {}, +}) diff --git a/arccode.dev/src/router/subRouters/AdministratorSubRouter.tsx b/arccode.dev/src/router/subRouters/AdministratorSubRouter.tsx index 931952d..e842b71 100644 --- a/arccode.dev/src/router/subRouters/AdministratorSubRouter.tsx +++ b/arccode.dev/src/router/subRouters/AdministratorSubRouter.tsx @@ -3,9 +3,12 @@ import { Outlet, Route, Routes } from 'react-router-dom' import AdministratorLayout from '~app/administrator.../layout' import Administrator from '~app/administrator.../page' +import EmailsLayout from '~app/administrator.../emails/layout' import Emails from '~app/administrator.../emails/page' import UsersLayout from '~app/administrator.../users/layout' import Users from '~app/administrator.../users/page' +import GuildsLayout from '~app/administrator.../guilds/layout' +import Guilds from '~app/administrator.../guilds/page' import NotFound from '~components/common/NotFound' @@ -22,7 +25,7 @@ function AdministratorSubRouter() { /> } + element={} > } /> + } + > + } + /> + } + /> + } diff --git a/arccode.dev/src/types.ts b/arccode.dev/src/types.ts index 2175c7b..2c22951 100644 --- a/arccode.dev/src/types.ts +++ b/arccode.dev/src/types.ts @@ -32,15 +32,17 @@ export type User = DatabaseResource<{ nUpdates: number sentDailyRecapEmailAt: string timezoneOffset: number + guildIds?: string[] }> -export type Email = DatabaseResource<{ - to: string - message: { - subject: string - text?: string - html?: string - } +export type Guild = DatabaseResource<{ + name: string + emoji: string + description: string + isPrivate: boolean + administratorIds: string[] + moderatorIds: string[] + memberIds: string[] }> /* --- diff --git a/arccode.dev/src/utils/db/createUser.ts b/arccode.dev/src/utils/db/createUser.ts index 37872f8..21655fb 100644 --- a/arccode.dev/src/utils/db/createUser.ts +++ b/arccode.dev/src/utils/db/createUser.ts @@ -11,6 +11,7 @@ type CreateUserArg = Omit< | 'nUpdates' | 'sentDailyRecapEmailAt' | 'timezoneOffset' + | 'guildIds' | 'createdAt' | 'updatedAt' | 'deletedAt' @@ -28,6 +29,7 @@ function createUser(user: CreateUserArg): User { nUpdates: 0, sentDailyRecapEmailAt: new Date(0).toISOString(), timezoneOffset: new Date().getTimezoneOffset(), + guildIds: [], createdAt: now, updatedAt: now, deletedAt: '', From 09e7025099a37d32d037238ca7f9a5d7890ebc9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Thu, 22 Aug 2024 10:45:18 +0300 Subject: [PATCH 2/5] Add first guild components --- arccode.dev/firestore.rules | 9 +++--- .../administrator/guilds/Guilds.tsx | 30 ++++++++++++------- .../components/character/CharacterProfile.tsx | 20 ++++++++----- arccode.dev/src/components/guild/Guilds.tsx | 22 ++++++++++++++ .../src/components/guild/GuildsList.tsx | 17 +++++++++++ .../src/components/guild/GuildsProvider.tsx | 20 +++++++++++-- arccode.dev/src/hooks/guild/useGuilds.ts | 9 ++++++ 7 files changed, 103 insertions(+), 24 deletions(-) create mode 100644 arccode.dev/src/components/guild/Guilds.tsx create mode 100644 arccode.dev/src/components/guild/GuildsList.tsx create mode 100644 arccode.dev/src/hooks/guild/useGuilds.ts diff --git a/arccode.dev/firestore.rules b/arccode.dev/firestore.rules index eb14a48..c11de04 100644 --- a/arccode.dev/firestore.rules +++ b/arccode.dev/firestore.rules @@ -28,7 +28,7 @@ service cloud.firestore { } function isGuildPublicOrHasMember() { - return !resource.data.isPrivate || (request.auth != null && request.auth.uid in resource.data.members); + return !resource.data.isPrivate || (request.auth != null && request.auth.uid in resource.data.memberIds); } function isGuildAdministrator() { @@ -57,10 +57,11 @@ service cloud.firestore { && hasUnlockedItem('spell4ItemId'); } - match /guilds/{huildId} { - allow read: if request.auth != null && isGuildPublicOrHasMember(); + match /guilds/{guildId} { + allow list: if request.auth != null && request.query.limit <= 100 && isGuildPublicOrHasMember(); + allow get: if request.auth != null && isGuildPublicOrHasMember(); allow create: if request.auth != null && isAdministrator(); - allow update, delete: if request.auth != null && (isAdministrator() || isGuildAdministrator()); + allow update, delete: if request.auth != null && (isGuildAdministrator() || isAdministrator()); } } diff --git a/arccode.dev/src/components/administrator/guilds/Guilds.tsx b/arccode.dev/src/components/administrator/guilds/Guilds.tsx index c2641eb..f784fe1 100644 --- a/arccode.dev/src/components/administrator/guilds/Guilds.tsx +++ b/arccode.dev/src/components/administrator/guilds/Guilds.tsx @@ -17,28 +17,32 @@ function Guilds() { const [loading, setLoading] = useState(false) const [createGuildSuccess, setCreateGuildSuccess] = useState(false) - const handleCreateGuilds = useCallback(async (n: number) => { + const handleCreateGuilds = useCallback(async (n: number, isPrivate = false, includeMemberIds = false) => { if (!user) return setLoading(true) setCreateGuildSuccess(false) const batch = writeBatch(db) + const now = Date.now() + + const repsonse = await fetch(`https://fakerapi.it/api/v1/texts?_quantity=${n}&_characters=128`) + const { data } = await repsonse.json() for (let i = 0; i < n; i++) { - const now = new Date().toISOString() + const createdAt = new Date(now + i).toISOString() const guild: Guild = { id: nanoid(), emoji: '๐ŸŽ‰', - name: `Guild ${i}`, - description: 'Lorem ipsum', - isPrivate: Math.random() < 0.1, + name: data[i].title, + description: data[i].content, + isPrivate, administratorIds: [user.id], moderatorIds: [user.id], - memberIds: [user.id], + memberIds: includeMemberIds ? [user.id] : [], userId: user.id, - createdAt: now, - updatedAt: now, + createdAt, + updatedAt: createdAt, deletedAt: '', } @@ -56,8 +60,14 @@ function Guilds() { return (
- + + {loading && ( diff --git a/arccode.dev/src/components/character/CharacterProfile.tsx b/arccode.dev/src/components/character/CharacterProfile.tsx index 370cf2c..7aa76a3 100644 --- a/arccode.dev/src/components/character/CharacterProfile.tsx +++ b/arccode.dev/src/components/character/CharacterProfile.tsx @@ -1,20 +1,24 @@ import CharacterGear from '~components/character/gear/CharacterGear' import CharacterKeywords from '~components/character/keywords/CharacterKeywords' import CharacterHeader from '~components/character/CharacterHeader' +import Guilds from '~components/guild/Guilds' function CharacterProfile() { return ( -
-
- -
- -
-
+
+
+
- + +
+
+ +
+ +
+
) } diff --git a/arccode.dev/src/components/guild/Guilds.tsx b/arccode.dev/src/components/guild/Guilds.tsx new file mode 100644 index 0000000..be0e5c0 --- /dev/null +++ b/arccode.dev/src/components/guild/Guilds.tsx @@ -0,0 +1,22 @@ +import GuildsProvider from '~components/guild/GuildsProvider' +import GuildsList from '~components/guild/GuildsList' + +function Guilds() { + return ( + +
+ Guilds +
+
+
+ +
+
+ Guild +
+
+
+ ) +} + +export default Guilds diff --git a/arccode.dev/src/components/guild/GuildsList.tsx b/arccode.dev/src/components/guild/GuildsList.tsx new file mode 100644 index 0000000..34afd88 --- /dev/null +++ b/arccode.dev/src/components/guild/GuildsList.tsx @@ -0,0 +1,17 @@ +import useGuilds from '~hooks/guild/useGuilds' + +function GuildsList() { + const { guilds } = useGuilds() + + return ( +
+ {guilds.map(guild => ( +
+ {guild.name} +
+ ))} +
+ ) +} + +export default GuildsList diff --git a/arccode.dev/src/components/guild/GuildsProvider.tsx b/arccode.dev/src/components/guild/GuildsProvider.tsx index 9a735bd..99eceea 100644 --- a/arccode.dev/src/components/guild/GuildsProvider.tsx +++ b/arccode.dev/src/components/guild/GuildsProvider.tsx @@ -1,19 +1,35 @@ import { type PropsWithChildren, useCallback, useMemo } from 'react' -import { collection, query } from 'firebase/firestore' +import { collection, limit, or, orderBy, query, where } from 'firebase/firestore' import type { Guild } from '~types' +import { NULL_DOCUMENT_ID } from '~constants' + import { db } from '~firebase' import type { GuildsContextType } from '~contexts/guild/GuildsContext' import GuildsContext from '~contexts/guild/GuildsContext' import useDocuments from '~hooks/db/useDocuments' +import useUser from '~hooks/user/useUser' import SpinnerCentered from '~components/common/CenteredSpinner' +const PAGINATION_LIMIT = 100 + function GuildsProvider({ children }: PropsWithChildren) { - const q = useMemo(() => query(collection(db, 'guilds')), []) + const { user } = useUser() + const q = useMemo(() => query( + collection(db, 'guilds'), + or( + where('isPrivate', '==', false), + where('memberIds', 'array-contains', user?.id ?? NULL_DOCUMENT_ID), + ), + orderBy('createdAt', 'desc'), + limit(PAGINATION_LIMIT), + ), [ + user?.id, + ]) const { data: guilds, loading, error } = useDocuments(q) const createGuild = useCallback(async (name: string) => { diff --git a/arccode.dev/src/hooks/guild/useGuilds.ts b/arccode.dev/src/hooks/guild/useGuilds.ts new file mode 100644 index 0000000..e387bc7 --- /dev/null +++ b/arccode.dev/src/hooks/guild/useGuilds.ts @@ -0,0 +1,9 @@ +import { useContext } from 'react' + +import GuildsContext from '~contexts/guild/GuildsContext' + +function useGuilds() { + return useContext(GuildsContext) +} + +export default useGuilds From 1ac3faa5b3fd69cf5a6cb3d34d81882f6cab1771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Thu, 22 Aug 2024 11:09:21 +0300 Subject: [PATCH 3/5] Create GuildProvider --- .../administrator/guilds/Guilds.tsx | 4 +- .../components/character/CharacterHeader.tsx | 2 +- .../components/character/CharacterProfile.tsx | 6 ++- arccode.dev/src/components/guild/Guild.tsx | 21 +++++++++ .../src/components/guild/GuildProvider.tsx | 43 +++++++++++++++++++ arccode.dev/src/components/guild/Guilds.tsx | 22 ++++++---- .../src/components/guild/GuildsList.tsx | 13 +++++- .../src/contexts/guild/GuildContext.ts | 13 ++++++ arccode.dev/src/hooks/guild/useGuild.ts | 9 ++++ arccode.dev/src/types.ts | 2 +- 10 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 arccode.dev/src/components/guild/Guild.tsx create mode 100644 arccode.dev/src/components/guild/GuildProvider.tsx create mode 100644 arccode.dev/src/contexts/guild/GuildContext.ts create mode 100644 arccode.dev/src/hooks/guild/useGuild.ts diff --git a/arccode.dev/src/components/administrator/guilds/Guilds.tsx b/arccode.dev/src/components/administrator/guilds/Guilds.tsx index f784fe1..9f7f241 100644 --- a/arccode.dev/src/components/administrator/guilds/Guilds.tsx +++ b/arccode.dev/src/components/administrator/guilds/Guilds.tsx @@ -11,6 +11,8 @@ import useUser from '~hooks/user/useUser' import { Button } from '~components/ui/Button' import Spinner from '~components/common/Spinner' +const emojies = ['๐ŸŽ‰', '๐ŸŽˆ', '๐ŸŽ', '๐ŸŽŠ', '๐ŸŽ‚', '๐ŸŽƒ', '๐ŸŽ„', '๐ŸŽ…', '๐ŸŽ†', '๐ŸŽ‡', '๐Ÿงจ', '๐ŸŽ—๏ธ', '๐Ÿต๏ธ', '๐ŸŽ–๏ธ', '๐Ÿ†', '๐Ÿฅ‡', '๐Ÿฅˆ', '๐Ÿฅ‰', '๐Ÿ…', '๐ŸŽฎ', '๐Ÿ•น๏ธ', '๐ŸŽฒ', '๐Ÿงฉ', '๐ŸŽจ', '๐ŸŽค', '๐ŸŽง', '๐ŸŽผ', '๐ŸŽน', '๐Ÿฅ', '๐ŸŽท', '๐ŸŽบ', '๐ŸŽธ', '๐Ÿช•', '๐ŸŽป', '๐ŸŽฌ', '๐ŸŽฅ', '๐Ÿ“ท', '๐Ÿ“ธ', '๐Ÿ“น', '๐ŸŽž๏ธ', '๐Ÿ“ฝ๏ธ', '๐ŸŽฆ', '๐ŸŸ๏ธ', '๐ŸŽช', '๐ŸŽญ', '๐Ÿฉฐ', '๐ŸŽจ', '๐ŸŽช', '๐ŸŽค', '๐ŸŽน', '๐ŸŽป', '๐ŸŽบ', '๐ŸŽท', '๐Ÿฅ', '๐ŸŽฌ', '๐ŸŽญ', '๐ŸŽจ', '๐ŸŽฏ', '๐ŸŽณ', '๐ŸŽฎ', '๐ŸŽฐ', '๐ŸŽฑ', '๐ŸŽฒ', '๐ŸŽด', '๐Ÿ€„', '๐Ÿƒ', '๐ŸŽธ', '๐Ÿช•', '๐ŸŽค', '๐ŸŽง', '๐ŸŽผ', '๐ŸŽถ', '๐ŸŽต', '๐ŸŽš๏ธ', '๐ŸŽ›๏ธ', '๐ŸŽ™๏ธ', '๐ŸŽค', '๐ŸŽง', '๐ŸŽผ', '๐ŸŽต', '๐ŸŽถ', '๐ŸŽน'] + function Guilds() { const { user } = useUser() @@ -33,9 +35,9 @@ function Guilds() { const createdAt = new Date(now + i).toISOString() const guild: Guild = { id: nanoid(), - emoji: '๐ŸŽ‰', name: data[i].title, description: data[i].content, + emoji: emojies[Math.floor(Math.random() * emojies.length)], isPrivate, administratorIds: [user.id], moderatorIds: [user.id], diff --git a/arccode.dev/src/components/character/CharacterHeader.tsx b/arccode.dev/src/components/character/CharacterHeader.tsx index 3c03d6b..8acb45b 100644 --- a/arccode.dev/src/components/character/CharacterHeader.tsx +++ b/arccode.dev/src/components/character/CharacterHeader.tsx @@ -5,7 +5,7 @@ function CharacterHeader() { const characterName = character.name || '(An unnamed character)' return ( -
+

{characterName}

diff --git a/arccode.dev/src/components/character/CharacterProfile.tsx b/arccode.dev/src/components/character/CharacterProfile.tsx index 7aa76a3..8582974 100644 --- a/arccode.dev/src/components/character/CharacterProfile.tsx +++ b/arccode.dev/src/components/character/CharacterProfile.tsx @@ -1,9 +1,13 @@ +import useCharacter from '~hooks/character/useCharacter' + import CharacterGear from '~components/character/gear/CharacterGear' import CharacterKeywords from '~components/character/keywords/CharacterKeywords' import CharacterHeader from '~components/character/CharacterHeader' import Guilds from '~components/guild/Guilds' function CharacterProfile() { + const { isEditable } = useCharacter() + return (
@@ -18,7 +22,7 @@ function CharacterProfile() {
- + {isEditable && }
) } diff --git a/arccode.dev/src/components/guild/Guild.tsx b/arccode.dev/src/components/guild/Guild.tsx new file mode 100644 index 0000000..1cf7bea --- /dev/null +++ b/arccode.dev/src/components/guild/Guild.tsx @@ -0,0 +1,21 @@ +import useGuild from '~hooks/guild/useGuild' + +function Guild() { + const { guild } = useGuild() + + if (!guild) { + return ( +
+ Start by selecting a guild +
+ ) + } + + return ( +
+ {guild.name} +
+ ) +} + +export default Guild diff --git a/arccode.dev/src/components/guild/GuildProvider.tsx b/arccode.dev/src/components/guild/GuildProvider.tsx new file mode 100644 index 0000000..c0b16ef --- /dev/null +++ b/arccode.dev/src/components/guild/GuildProvider.tsx @@ -0,0 +1,43 @@ +import { type PropsWithChildren, useCallback, useMemo } from 'react' +import { useSearchParams } from 'react-router-dom' + +import GuildContext, { GuildContextType } from '~contexts/guild/GuildContext' + +import useGuilds from '~hooks/guild/useGuilds' + +const GUILD_ID_SEARCH_PARAMETERS_KEY = 'guild' + +function GuildProvider({ children }: PropsWithChildren) { + const { guilds } = useGuilds() + const [searchParams, setSearchParams] = useSearchParams() + const guildId = searchParams.get(GUILD_ID_SEARCH_PARAMETERS_KEY) ?? '' + const guild = useMemo(() => guilds.find(x => x.id === guildId) ?? null, [guildId, guilds]) + + const setGuildId = useCallback((guildId: string) => { + setSearchParams(x => { + x.set(GUILD_ID_SEARCH_PARAMETERS_KEY, guildId) + + return x + }, { + replace: true, + }) + }, [ + setSearchParams, + ]) + + const guildContextValue = useMemo(() => ({ + guild, + setGuildId, + }), [ + guild, + setGuildId, + ]) + + return ( + + {children} + + ) +} + +export default GuildProvider diff --git a/arccode.dev/src/components/guild/Guilds.tsx b/arccode.dev/src/components/guild/Guilds.tsx index be0e5c0..01c926f 100644 --- a/arccode.dev/src/components/guild/Guilds.tsx +++ b/arccode.dev/src/components/guild/Guilds.tsx @@ -1,20 +1,24 @@ import GuildsProvider from '~components/guild/GuildsProvider' +import GuildProvider from '~components/guild/GuildProvider' import GuildsList from '~components/guild/GuildsList' +import Guild from '~components/guild/Guild' function Guilds() { return ( -
- Guilds -
-
-
- + +
+ Guilds
-
- Guild +
+
+ +
+
+ +
-
+
) } diff --git a/arccode.dev/src/components/guild/GuildsList.tsx b/arccode.dev/src/components/guild/GuildsList.tsx index 34afd88..6b169c7 100644 --- a/arccode.dev/src/components/guild/GuildsList.tsx +++ b/arccode.dev/src/components/guild/GuildsList.tsx @@ -1,12 +1,21 @@ +import useGuild from '~hooks/guild/useGuild' import useGuilds from '~hooks/guild/useGuilds' function GuildsList() { const { guilds } = useGuilds() + const { setGuildId } = useGuild() return ( -
+
{guilds.map(guild => ( -
+
setGuildId(guild.id)} + > + + {guild.emoji} + {guild.name}
))} diff --git a/arccode.dev/src/contexts/guild/GuildContext.ts b/arccode.dev/src/contexts/guild/GuildContext.ts new file mode 100644 index 0000000..3d7b8c4 --- /dev/null +++ b/arccode.dev/src/contexts/guild/GuildContext.ts @@ -0,0 +1,13 @@ +import { createContext } from 'react' + +import type { Guild } from '~types' + +export type GuildContextType = { + guild: Guild | null + setGuildId: (guildId: string) => void +} + +export default createContext({ + guild: null, + setGuildId: () => {}, +}) diff --git a/arccode.dev/src/hooks/guild/useGuild.ts b/arccode.dev/src/hooks/guild/useGuild.ts new file mode 100644 index 0000000..f9d8a5d --- /dev/null +++ b/arccode.dev/src/hooks/guild/useGuild.ts @@ -0,0 +1,9 @@ +import { useContext } from 'react' + +import GuildContext from '~contexts/guild/GuildContext' + +function useGuild() { + return useContext(GuildContext) +} + +export default useGuild diff --git a/arccode.dev/src/types.ts b/arccode.dev/src/types.ts index 2c22951..a8cae2e 100644 --- a/arccode.dev/src/types.ts +++ b/arccode.dev/src/types.ts @@ -37,8 +37,8 @@ export type User = DatabaseResource<{ export type Guild = DatabaseResource<{ name: string - emoji: string description: string + emoji: string isPrivate: boolean administratorIds: string[] moderatorIds: string[] From 1608c5dbd7bca7264c5d6835676f77861c1351fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Thu, 22 Aug 2024 14:19:22 +0300 Subject: [PATCH 4/5] Create usePaginatedDocuments --- .../administrator/guilds/Guilds.tsx | 1 + arccode.dev/src/components/guild/Guild.tsx | 6 ++ .../src/components/guild/GuildsList.tsx | 37 ++++++++- .../src/components/guild/GuildsProvider.tsx | 27 +++--- .../src/contexts/guild/GuildsContext.ts | 6 ++ arccode.dev/src/hooks/db/useDocuments.ts | 4 +- .../src/hooks/db/usePaginatedDocuments.ts | 83 +++++++++++++++++++ arccode.dev/src/types.ts | 1 + 8 files changed, 144 insertions(+), 21 deletions(-) create mode 100644 arccode.dev/src/hooks/db/usePaginatedDocuments.ts diff --git a/arccode.dev/src/components/administrator/guilds/Guilds.tsx b/arccode.dev/src/components/administrator/guilds/Guilds.tsx index 9f7f241..0f7c001 100644 --- a/arccode.dev/src/components/administrator/guilds/Guilds.tsx +++ b/arccode.dev/src/components/administrator/guilds/Guilds.tsx @@ -43,6 +43,7 @@ function Guilds() { moderatorIds: [user.id], memberIds: includeMemberIds ? [user.id] : [], userId: user.id, + lastMessageAt: createdAt, createdAt, updatedAt: createdAt, deletedAt: '', diff --git a/arccode.dev/src/components/guild/Guild.tsx b/arccode.dev/src/components/guild/Guild.tsx index 1cf7bea..6b89c22 100644 --- a/arccode.dev/src/components/guild/Guild.tsx +++ b/arccode.dev/src/components/guild/Guild.tsx @@ -1,8 +1,14 @@ import useGuild from '~hooks/guild/useGuild' +import useGuilds from '~hooks/guild/useGuilds' function Guild() { + const { guilds } = useGuilds() const { guild } = useGuild() + if (!guilds.length) { + return null + } + if (!guild) { return (
diff --git a/arccode.dev/src/components/guild/GuildsList.tsx b/arccode.dev/src/components/guild/GuildsList.tsx index 6b169c7..8f979cf 100644 --- a/arccode.dev/src/components/guild/GuildsList.tsx +++ b/arccode.dev/src/components/guild/GuildsList.tsx @@ -1,24 +1,53 @@ import useGuild from '~hooks/guild/useGuild' import useGuilds from '~hooks/guild/useGuilds' +import Spinner from '~components/common/Spinner' +import { Button } from '~components/ui/Button' + function GuildsList() { - const { guilds } = useGuilds() + const { guilds, loadingGuilds, hasMoreGuilds, fetchMoreGuilds } = useGuilds() const { setGuildId } = useGuild() return ( -
+
+
+ +
{guilds.map(guild => (
setGuildId(guild.id)} > - +
{guild.emoji} - +
{guild.name}
))} + {!loadingGuilds && hasMoreGuilds && ( +
+ +
+ )} + {loadingGuilds && ( +
+ +
+ )}
) } diff --git a/arccode.dev/src/components/guild/GuildsProvider.tsx b/arccode.dev/src/components/guild/GuildsProvider.tsx index 99eceea..e67ebfb 100644 --- a/arccode.dev/src/components/guild/GuildsProvider.tsx +++ b/arccode.dev/src/components/guild/GuildsProvider.tsx @@ -1,5 +1,5 @@ import { type PropsWithChildren, useCallback, useMemo } from 'react' -import { collection, limit, or, orderBy, query, where } from 'firebase/firestore' +import { collection, or, orderBy, query, where } from 'firebase/firestore' import type { Guild } from '~types' @@ -10,12 +10,8 @@ import { db } from '~firebase' import type { GuildsContextType } from '~contexts/guild/GuildsContext' import GuildsContext from '~contexts/guild/GuildsContext' -import useDocuments from '~hooks/db/useDocuments' import useUser from '~hooks/user/useUser' - -import SpinnerCentered from '~components/common/CenteredSpinner' - -const PAGINATION_LIMIT = 100 +import usePaginatedDocuments from '~hooks/db/usePaginatedDocuments' function GuildsProvider({ children }: PropsWithChildren) { const { user } = useUser() @@ -25,31 +21,30 @@ function GuildsProvider({ children }: PropsWithChildren) { where('isPrivate', '==', false), where('memberIds', 'array-contains', user?.id ?? NULL_DOCUMENT_ID), ), - orderBy('createdAt', 'desc'), - limit(PAGINATION_LIMIT), + orderBy('lastMessageAt', 'desc'), ), [ user?.id, ]) - const { data: guilds, loading, error } = useDocuments(q) + const { data: guilds, loading, error, hasMore, fetchMore } = usePaginatedDocuments(q, 25) const createGuild = useCallback(async (name: string) => { console.log('name', name) }, []) const guildsContextValue = useMemo(() => ({ - guilds: guilds ?? [], + guilds, + loadingGuilds: loading, + hasMoreGuilds: hasMore, createGuild, + fetchMoreGuilds: fetchMore, }), [ guilds, + loading, + hasMore, createGuild, + fetchMore, ]) - if (loading) { - return ( - - ) - } - if (error) { return (
diff --git a/arccode.dev/src/contexts/guild/GuildsContext.ts b/arccode.dev/src/contexts/guild/GuildsContext.ts index fcf49f2..14989ab 100644 --- a/arccode.dev/src/contexts/guild/GuildsContext.ts +++ b/arccode.dev/src/contexts/guild/GuildsContext.ts @@ -4,10 +4,16 @@ import type { Guild } from '~types' export type GuildsContextType = { guilds: Guild[] + loadingGuilds: boolean + hasMoreGuilds: boolean createGuild: (name: string) => Promise + fetchMoreGuilds: () => void } export default createContext({ guilds: [], + loadingGuilds: false, + hasMoreGuilds: false, createGuild: async () => {}, + fetchMoreGuilds: () => {}, }) diff --git a/arccode.dev/src/hooks/db/useDocuments.ts b/arccode.dev/src/hooks/db/useDocuments.ts index add784c..7c5964e 100644 --- a/arccode.dev/src/hooks/db/useDocuments.ts +++ b/arccode.dev/src/hooks/db/useDocuments.ts @@ -23,7 +23,9 @@ function useDocuments(query: Query, enabled = true) console.error(error) setError(error as Error) } - }, [query]) + }, [ + query, + ]) const fetch = useCallback(async () => { if (!enabled) { diff --git a/arccode.dev/src/hooks/db/usePaginatedDocuments.ts b/arccode.dev/src/hooks/db/usePaginatedDocuments.ts new file mode 100644 index 0000000..2e249fe --- /dev/null +++ b/arccode.dev/src/hooks/db/usePaginatedDocuments.ts @@ -0,0 +1,83 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { type DocumentSnapshot, type Query, getDocs, limit, query, startAfter } from 'firebase/firestore' + +import type { DatabaseResource } from '~types' + +function usePaginatedDocuments(q: Query, limitTo = 100, enabled = true) { + const [data, setData] = useState>({}) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [cursor, setCursor] = useState(null) + const [nextCursor, setNextCursor] = useState(null) + const q2 = useMemo(() => cursor ? query(q, startAfter(cursor), limit(limitTo)) : query(q, limit(limitTo)), [q, limitTo, cursor]) + + const refetch = useCallback(async () => { + setLoading(true) + + try { + const querySnapshot = await getDocs(q2) + const data: T[] = [] + let lastDoc: DocumentSnapshot | null = null + + querySnapshot.forEach(doc => { + data.push(doc.data() as T) + lastDoc = doc + }) + + setData(x => ({ + ...x, + ...data.reduce((acc, x) => ({ ...acc, [x.id]: x }), {}), + })) + setNextCursor(data.length === limitTo ? lastDoc : null) + } + catch (error) { + console.error(error) + setError(error as Error) + } + + setLoading(false) + }, [ + limitTo, + q2, + ]) + + const fetch = useCallback(async () => { + if (!enabled) { + setData({}) + setLoading(false) + setError(null) + + return + } + + await refetch() + }, [ + enabled, + refetch, + ]) + + const fetchMore = useCallback(() => { + if (!nextCursor) return + + setCursor(nextCursor) + }, [ + nextCursor, + ]) + + useEffect(() => { + fetch() + }, [ + fetch, + ]) + + return { + data: Object.values(data), + loading, + error, + hasMore: !!nextCursor, + refetch, + fetchMore, + } +} + +export default usePaginatedDocuments diff --git a/arccode.dev/src/types.ts b/arccode.dev/src/types.ts index a8cae2e..bae1d60 100644 --- a/arccode.dev/src/types.ts +++ b/arccode.dev/src/types.ts @@ -43,6 +43,7 @@ export type Guild = DatabaseResource<{ administratorIds: string[] moderatorIds: string[] memberIds: string[] + lastMessageAt: string }> /* --- From 2876afec4e2c7de2d6277affa23cfb082fef7b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A9rault?= Date: Fri, 23 Aug 2024 10:38:10 +0300 Subject: [PATCH 5/5] Make guilds dev only --- .../administrator/guilds/Guilds.tsx | 2 +- .../components/character/CharacterProfile.tsx | 2 +- arccode.dev/src/components/guild/Guilds.tsx | 2 +- .../src/components/guild/GuildsList.tsx | 32 +++++++++++-------- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/arccode.dev/src/components/administrator/guilds/Guilds.tsx b/arccode.dev/src/components/administrator/guilds/Guilds.tsx index 0f7c001..4ece199 100644 --- a/arccode.dev/src/components/administrator/guilds/Guilds.tsx +++ b/arccode.dev/src/components/administrator/guilds/Guilds.tsx @@ -35,7 +35,7 @@ function Guilds() { const createdAt = new Date(now + i).toISOString() const guild: Guild = { id: nanoid(), - name: data[i].title, + name: Math.random() < 0.8 ? data[i].title : data[i].title + data[i].title + data[i].title, description: data[i].content, emoji: emojies[Math.floor(Math.random() * emojies.length)], isPrivate, diff --git a/arccode.dev/src/components/character/CharacterProfile.tsx b/arccode.dev/src/components/character/CharacterProfile.tsx index 8582974..e16dcfd 100644 --- a/arccode.dev/src/components/character/CharacterProfile.tsx +++ b/arccode.dev/src/components/character/CharacterProfile.tsx @@ -22,7 +22,7 @@ function CharacterProfile() {
- {isEditable && } + {import.meta.env.DEV && isEditable && }
) } diff --git a/arccode.dev/src/components/guild/Guilds.tsx b/arccode.dev/src/components/guild/Guilds.tsx index 01c926f..6ec29ab 100644 --- a/arccode.dev/src/components/guild/Guilds.tsx +++ b/arccode.dev/src/components/guild/Guilds.tsx @@ -11,7 +11,7 @@ function Guilds() { Guilds
-
+
diff --git a/arccode.dev/src/components/guild/GuildsList.tsx b/arccode.dev/src/components/guild/GuildsList.tsx index 8f979cf..2c1a030 100644 --- a/arccode.dev/src/components/guild/GuildsList.tsx +++ b/arccode.dev/src/components/guild/GuildsList.tsx @@ -1,3 +1,4 @@ +import useUser from '~hooks/user/useUser' import useGuild from '~hooks/guild/useGuild' import useGuilds from '~hooks/guild/useGuilds' @@ -5,30 +6,35 @@ import Spinner from '~components/common/Spinner' import { Button } from '~components/ui/Button' function GuildsList() { + const { user } = useUser() const { guilds, loadingGuilds, hasMoreGuilds, fetchMoreGuilds } = useGuilds() const { setGuildId } = useGuild() return ( -
-
- -
+
+ {user?.isAdministrator && ( +
+ +
+ )} {guilds.map(guild => (
setGuildId(guild.id)} >
{guild.emoji}
- {guild.name} +
+ {guild.name} +
))} {!loadingGuilds && hasMoreGuilds && ( @@ -44,7 +50,7 @@ function GuildsList() {
)} {loadingGuilds && ( -
+
)}