Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Guilds #2

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions arccode.dev/firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -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.memberIds);
}

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;
Expand All @@ -49,5 +57,12 @@ service cloud.firestore {
&& hasUnlockedItem('spell4ItemId');
}

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 && (isGuildAdministrator() || isAdministrator());
}

}
}
3 changes: 3 additions & 0 deletions arccode.dev/src/app/administrator.../emails/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import DevelopmentBouncer from '~components/administrator/DevelopmentBouncer'

export default DevelopmentBouncer
3 changes: 3 additions & 0 deletions arccode.dev/src/app/administrator.../guilds/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import DevelopmentBouncer from '~components/administrator/DevelopmentBouncer'

export default DevelopmentBouncer
3 changes: 3 additions & 0 deletions arccode.dev/src/app/administrator.../guilds/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Guilds from '~components/administrator/guilds/Guilds'

export default Guilds
25 changes: 18 additions & 7 deletions arccode.dev/src/components/administrator/AdministratorLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,24 @@ function AdministratorLayout({ children }: PropsWithChildren) {
>
Users
</Link>
{' - '}
<Link
to="/administrator/emails"
className="text-blue hover:underline"
>
Emails
</Link>
{import.meta.env.DEV && (
<>
{' - '}
<Link
to="/administrator/emails"
className="text-blue hover:underline"
>
Emails
</Link>
{' - '}
<Link
to="/administrator/guilds"
className="text-blue hover:underline"
>
Guilds
</Link>
</>
)}
</div>
{children}
</>
Expand Down
15 changes: 15 additions & 0 deletions arccode.dev/src/components/administrator/DevelopmentBouncer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { PropsWithChildren } from 'react'

function DevelopmentBouncer({ children }: PropsWithChildren) {
if (!import.meta.env.DEV) {
return (
<div>
Available in development mode only.
</div>
)
}

return children as JSX.Element
}

export default DevelopmentBouncer
88 changes: 88 additions & 0 deletions arccode.dev/src/components/administrator/guilds/Guilds.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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'

const emojies = ['🎉', '🎈', '🎁', '🎊', '🎂', '🎃', '🎄', '🎅', '🎆', '🎇', '🧨', '🎗️', '🏵️', '🎖️', '🏆', '🥇', '🥈', '🥉', '🏅', '🎮', '🕹️', '🎲', '🧩', '🎨', '🎤', '🎧', '🎼', '🎹', '🥁', '🎷', '🎺', '🎸', '🪕', '🎻', '🎬', '🎥', '📷', '📸', '📹', '🎞️', '📽️', '🎦', '🏟️', '🎪', '🎭', '🩰', '🎨', '🎪', '🎤', '🎹', '🎻', '🎺', '🎷', '🥁', '🎬', '🎭', '🎨', '🎯', '🎳', '🎮', '🎰', '🎱', '🎲', '🎴', '🀄', '🃏', '🎸', '🪕', '🎤', '🎧', '🎼', '🎶', '🎵', '🎚️', '🎛️', '🎙️', '🎤', '🎧', '🎼', '🎵', '🎶', '🎹']

function Guilds() {
const { user } = useUser()

const [loading, setLoading] = useState(false)
const [createGuildSuccess, setCreateGuildSuccess] = useState(false)

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 createdAt = new Date(now + i).toISOString()
const guild: Guild = {
id: nanoid(),
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,
administratorIds: [user.id],
moderatorIds: [user.id],
memberIds: includeMemberIds ? [user.id] : [],
userId: user.id,
lastMessageAt: createdAt,
createdAt,
updatedAt: createdAt,
deletedAt: '',
}

batch.set(doc(db, 'guilds', guild.id), guild)
}

await batch.commit()

setLoading(false)
setCreateGuildSuccess(true)
}, [
user,
])

return (
<div className="container">
<div className="flex items-center gap-2">
<Button onClick={() => handleCreateGuilds(10)}>
Create 10 public guilds
</Button>
<Button onClick={() => handleCreateGuilds(10, true)}>
Create 10 private guilds without memberIds
</Button>
<Button onClick={() => handleCreateGuilds(10, true, true)}>
Create 10 private guilds with memberIds
</Button>
{loading && (
<Spinner className="w-4" />
)}
{createGuildSuccess && (
<div className="text-green-500 text-sm">
Guilds created successfully
</div>
)}
</div>
</div>
)
}

export default Guilds
2 changes: 1 addition & 1 deletion arccode.dev/src/components/character/CharacterHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ function CharacterHeader() {
const characterName = character.name || '(An unnamed character)'

return (
<div className="mb-3 flex flex-wrap items-baseline gap-x-4">
<div className="mb-4 flex flex-wrap items-baseline gap-x-4">
<h1 className="font-display font-bold text-3xl lg:text-4xl text-nowrap">
{characterName}
</h1>
Expand Down
24 changes: 16 additions & 8 deletions arccode.dev/src/components/character/CharacterProfile.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
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 (
<div className="px-4 lg:px-8 container flex flex-col lg:flex-row lg:items-start gap-x-8 xl:gap-x-16">
<div className="block lg:hidden">
<CharacterHeader />
</div>
<CharacterGear />
<div className="mt-16 lg:mt-0 grow">
<div className="hidden lg:block">
<div className="px-4 lg:px-8 pb-8 container">
<div className="mb-2 md:mb-12 flex flex-col lg:flex-row lg:items-start gap-x-8 xl:gap-x-16">
<div className="block lg:hidden">
<CharacterHeader />
</div>
<CharacterKeywords />
<CharacterGear />
<div className="mt-16 lg:mt-0 grow">
<div className="hidden lg:block">
<CharacterHeader />
</div>
<CharacterKeywords />
</div>
</div>
{import.meta.env.DEV && isEditable && <Guilds />}
</div>
)
}
Expand Down
27 changes: 27 additions & 0 deletions arccode.dev/src/components/guild/Guild.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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 (
<div className="text-sm text-neutral-500">
Start by selecting a guild
</div>
)
}

return (
<div className="p-4 bg-white border rounded">
{guild.name}
</div>
)
}

export default Guild
43 changes: 43 additions & 0 deletions arccode.dev/src/components/guild/GuildProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<GuildContextType>(() => ({
guild,
setGuildId,
}), [
guild,
setGuildId,
])

return (
<GuildContext.Provider value={guildContextValue}>
{children}
</GuildContext.Provider>
)
}

export default GuildProvider
26 changes: 26 additions & 0 deletions arccode.dev/src/components/guild/Guilds.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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 (
<GuildsProvider>
<GuildProvider>
<div className="text-3xl font-bold font-display">
Guilds
</div>
<div className="mt-4 flex flex-col lg:flex-row lg:items-start gap-4">
<div className="lg:w-[320px]">
<GuildsList />
</div>
<div className="grow">
<Guild />
</div>
</div>
</GuildProvider>
</GuildsProvider>
)
}

export default Guilds
61 changes: 61 additions & 0 deletions arccode.dev/src/components/guild/GuildsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import useUser from '~hooks/user/useUser'
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 { user } = useUser()
const { guilds, loadingGuilds, hasMoreGuilds, fetchMoreGuilds } = useGuilds()
const { setGuildId } = useGuild()

return (
<div className="py-3 bg-white border rounded max-h-[512px] overflow-y-auto">
{user?.isAdministrator && (
<div className="px-4">
<Button
size="xs"
variant="ghost"
className="w-full"
>
Create your own guild!
</Button>
</div>
)}
{guilds.map(guild => (
<div
key={guild.id}
className="py-1 px-4 flex items-center gap-2 hover:bg-neutral-50 cursor-pointer"
onClick={() => setGuildId(guild.id)}
>
<div>
{guild.emoji}
</div>
<div className="truncate">
{guild.name}
</div>
</div>
))}
{!loadingGuilds && hasMoreGuilds && (
<div className="px-4">
<Button
size="xs"
variant="ghost"
className="w-full"
onClick={fetchMoreGuilds}
>
Load more
</Button>
</div>
)}
{loadingGuilds && (
<div className="py-1 flex items-center justify-center">
<Spinner className="w-4" />
</div>
)}
</div>
)
}

export default GuildsList
Loading