forked from sk1982/actaeon
add friends list and chuni rival selection
This commit is contained in:
parent
769a484fc1
commit
baf1898d23
@ -1,9 +1,9 @@
|
||||
'use server';
|
||||
|
||||
import { db } from '@/db';
|
||||
import { GeneratedDB, db } from '@/db';
|
||||
import { getUser, requireUser } from './auth';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { CompiledQuery, sql } from 'kysely';
|
||||
import { CompiledQuery, Transaction, sql } from 'kysely';
|
||||
import { syncUserFriends, withChuniRivalCount } from '@/data/friend';
|
||||
import { SqlBool } from 'kysely';
|
||||
import { Exact } from 'type-fest';
|
||||
@ -131,3 +131,58 @@ export const rejectFriendRequest = async (id: string) => {
|
||||
.where('uuid', '=', id)
|
||||
.executeTakeFirst();
|
||||
};
|
||||
|
||||
const setRivalStatus = (user1: number, user2: number, chuniRival: number, trx: Transaction<GeneratedDB>) => {
|
||||
return trx.updateTable('actaeon_user_friends')
|
||||
.where(({ and, eb, or }) => or([
|
||||
and([
|
||||
eb('user1', '=', user1),
|
||||
eb('user2', '=', user2)
|
||||
]),
|
||||
and([
|
||||
eb('user2', '=', user1),
|
||||
eb('user1', '=', user2)
|
||||
]),
|
||||
]))
|
||||
.set({ chuniRival })
|
||||
.execute();
|
||||
}
|
||||
|
||||
export const addFriendAsRival = async (friend: number) => {
|
||||
const user = await requireUser();
|
||||
|
||||
const rivalCount = await withChuniRivalCount()
|
||||
.selectFrom('chuni_rival_count')
|
||||
.where(({ or, eb }) => or([
|
||||
eb('user', '=', user.id),
|
||||
eb('user', '=', friend)
|
||||
]))
|
||||
.select(['user', 'rivalCount'])
|
||||
.execute();
|
||||
|
||||
const userRivalCount = Number(rivalCount.find(r => r.user === user.id)?.rivalCount ?? 0);
|
||||
const friendRivalCount = Number(rivalCount.find(r => r.user === friend)?.rivalCount ?? 0);
|
||||
|
||||
if (userRivalCount >= 4)
|
||||
return { error: true, message: 'You already have 4 rivals. You must remove a rival before adding a new one.' };
|
||||
if (friendRivalCount >= 4)
|
||||
return { error: true, message: 'This user already has 4 rivals. They must remove a rival before you can add them as a rival.' };
|
||||
|
||||
await db.transaction().execute(async trx => {
|
||||
await setRivalStatus(user.id, friend, 1, trx);
|
||||
|
||||
await syncUserFriends(user.id, trx);
|
||||
await syncUserFriends(friend, trx);
|
||||
});
|
||||
};
|
||||
|
||||
export const removeFriendAsRival = async (friend: number) => {
|
||||
const user = await requireUser();
|
||||
|
||||
await db.transaction().execute(async trx => {
|
||||
await setRivalStatus(user.id, friend, 0, trx);
|
||||
|
||||
await syncUserFriends(user.id, trx);
|
||||
await syncUserFriends(friend, trx);
|
||||
});
|
||||
};
|
||||
|
66
src/app/(with-header)/friends/friend-row.tsx
Normal file
66
src/app/(with-header)/friends/friend-row.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { Avatar, Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Divider, Tooltip } from '@nextui-org/react';
|
||||
import { Friend } from '@/data/friend';
|
||||
import Link from 'next/link';
|
||||
import { ChuniPenguinIcon } from '@/components/chuni/chuni-penguin-icon';
|
||||
import { EllipsisVerticalIcon } from '@heroicons/react/24/outline';
|
||||
import { useConfirmModal } from '@/components/confirm-modal';
|
||||
import { useState } from 'react';
|
||||
import { addFriendAsRival, removeFriendAsRival, unfriend } from '@/actions/friend';
|
||||
import { useErrorModal } from '@/components/error-modal';
|
||||
|
||||
type FriendRowProps = {
|
||||
friend: Friend,
|
||||
onUnfriend: () => void
|
||||
};
|
||||
|
||||
export const FriendRow = ({ friend: initialFriend, onUnfriend }: FriendRowProps) => {
|
||||
const confirm = useConfirmModal();
|
||||
const [friend, setFriend] = useState(initialFriend);
|
||||
const setError = useErrorModal();
|
||||
|
||||
return (<section>
|
||||
<section className="px-3 sm:bg-content1 sm:rounded-lg py-2.5 flex items-center">
|
||||
<Link href={`/user/${friend.uuid}`} className="flex items-center">
|
||||
<Avatar
|
||||
name={friend.username?.[0]?.toUpperCase() ?? undefined}
|
||||
className={`w-10 h-10 mr-2 text-2xl [font-feature-settings:"fwid"]`} />
|
||||
<span className="font-semibold transition hover:text-secondary">{friend.username}</span>
|
||||
</Link>
|
||||
{!!friend.chuniRival && <Tooltip content="Chunithm Rival">
|
||||
<div>
|
||||
<ChuniPenguinIcon className="ml-2 h-9" />
|
||||
</div>
|
||||
</Tooltip>}
|
||||
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly variant="light" className="ml-auto" radius="full">
|
||||
<EllipsisVerticalIcon className="h-3/4" />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownItem onPress={() => {
|
||||
if (friend.chuniRival)
|
||||
removeFriendAsRival(friend.id).then(() => setFriend({ ...friend, chuniRival: 0 }));
|
||||
else
|
||||
addFriendAsRival(friend.id).then(res => {
|
||||
if (res?.error)
|
||||
return setError(res.message);
|
||||
setFriend({ ...friend, chuniRival: 1 })
|
||||
});
|
||||
}}>
|
||||
{friend.chuniRival ? 'Remove as rival' : 'Add as rival'}
|
||||
</DropdownItem>
|
||||
<DropdownItem color="danger" variant="flat" className="text-danger" onPress={() =>
|
||||
confirm('Are you sure you want to unfriend this user? This will also remove them as a rival.', () => {
|
||||
unfriend(friend.id)
|
||||
.then(onUnfriend)
|
||||
})}>
|
||||
Unfriend
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</section>
|
||||
<Divider className="sm:hidden mt-1.5" />
|
||||
</section>);
|
||||
};
|
30
src/app/(with-header)/friends/friends.tsx
Normal file
30
src/app/(with-header)/friends/friends.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import { Friend } from '@/data/friend';
|
||||
import { Divider } from '@nextui-org/react';
|
||||
import { useState } from 'react';
|
||||
import { FriendRow } from './friend-row';
|
||||
|
||||
export type FriendsProps = {
|
||||
friends: Friend[],
|
||||
};
|
||||
|
||||
export const Friends = ({ friends: initialFriends }: FriendsProps) => {
|
||||
const [friends, setFriends] = useState(initialFriends);
|
||||
|
||||
return (<main className="w-full mx-auto max-w-7xl flex flex-col">
|
||||
<header className="font-semibold flex items-center text-2xl p-4">
|
||||
Friends
|
||||
</header>
|
||||
|
||||
<Divider className="mb-1.5 sm:mb-2" />
|
||||
|
||||
{!friends.length && <span className="text-gray-500 italic ml-4 mt-4">You don't have any friends</span>}
|
||||
|
||||
<section className="flex flex-col sm:grid gap-1.5 grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{friends.map(f => (<FriendRow friend={f} key={f.id} onUnfriend={() => {
|
||||
setFriends(friends => friends.filter(friend => friend.uuid !== f.uuid))
|
||||
}} />))}
|
||||
</section>
|
||||
</main>);
|
||||
};
|
10
src/app/(with-header)/friends/page.tsx
Normal file
10
src/app/(with-header)/friends/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { requireUser } from '@/actions/auth';
|
||||
import { getFriends } from '@/data/friend';
|
||||
import { Friends } from './friends';
|
||||
|
||||
export default async function FriendsPage() {
|
||||
const user = await requireUser();
|
||||
const friends = await getFriends(user.id);
|
||||
|
||||
return (<Friends friends={friends} />);
|
||||
};
|
@ -4,7 +4,7 @@ import { Avatar, Badge, Button, Divider, Dropdown, DropdownItem, DropdownMenu, D
|
||||
import { Bars3Icon, ChevronLeftIcon, XMarkIcon } from '@heroicons/react/20/solid';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ThemeSwitcherSwitch } from '@/components/theme-switcher';
|
||||
import { ThemeSwitcherDropdown, ThemeSwitcherSwitch } from '@/components/theme-switcher';
|
||||
import { AdjustmentsHorizontalIcon } from '@heroicons/react/24/solid';
|
||||
import { login, logout } from '@/actions/auth';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
@ -12,7 +12,7 @@ import { MAIN_ROUTES, ROUTES, Subroute, UserOnly, filterRoute } from '@/routes';
|
||||
import { useUser } from '@/helpers/use-user';
|
||||
import { useBreakpoint } from '@/helpers/use-breakpoint';
|
||||
import { useCookies } from 'next-client-cookies';
|
||||
import { BellIcon, ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import { ChevronRightIcon, BellIcon, ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { BellAlertIcon } from '@heroicons/react/24/solid';
|
||||
import { FriendRequest, acceptFriendRequest, getFriendRequests, rejectFriendRequest } from '@/actions/friend';
|
||||
@ -248,7 +248,23 @@ export const HeaderSidebar = ({ children }: HeaderSidebarProps) => {
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<Divider className="my-4" />
|
||||
<Divider className="mt-4" />
|
||||
|
||||
<Link href="/settings" className="font-semibold text-2xl p-3 pl-4 flex items-center" onClick={() => setNotificationsOpen(false)}>
|
||||
Settings
|
||||
|
||||
<ChevronRightIcon className="ml-auto h-6" />
|
||||
</Link>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Link href="/friends" className="font-semibold text-2xl p-3 pl-4 flex items-center" onClick={() => setNotificationsOpen(false)}>
|
||||
Friends
|
||||
|
||||
<ChevronRightIcon className="ml-auto h-6" />
|
||||
</Link>
|
||||
|
||||
<Divider className="mb-4" />
|
||||
|
||||
<section className="px-2 flex flex-col flex-auto overflow-y-auto mb-3">
|
||||
<header className="font-semibold text-2xl px-2 mb-3">Notifications</header>
|
||||
@ -320,12 +336,17 @@ export const HeaderSidebar = ({ children }: HeaderSidebarProps) => {
|
||||
<DropdownItem showDivider className="p-0">
|
||||
<Link className="text-lg font-semibold block w-full h-full px-2 py-1.5" href={`/user/${user.uuid}`}>{user.username}</Link>
|
||||
</DropdownItem>
|
||||
<DropdownItem className="p-0 mt-1">
|
||||
<DropdownItem className="p-0 mt-0.5">
|
||||
<Link href="/settings" className="w-full h-full block px-2 py-1.5">
|
||||
Settings
|
||||
</Link>
|
||||
</DropdownItem>
|
||||
<DropdownItem className="p-0 mt-1" showDivider closeOnSelect={false}>
|
||||
<DropdownItem className="p-0 mt-0.5">
|
||||
<Link href="/friends" className="w-full h-full block px-2 py-1.5">
|
||||
Friends
|
||||
</Link>
|
||||
</DropdownItem>
|
||||
<DropdownItem className="p-0 mt-0.5" showDivider closeOnSelect={false}>
|
||||
<div className="w-full flex pl-2 h-8 items-center" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
|
||||
Dark Theme
|
||||
|
||||
@ -340,10 +361,12 @@ export const HeaderSidebar = ({ children }: HeaderSidebarProps) => {
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</> :
|
||||
|
||||
<Button size="sm" className="ml-2" color="primary" onClick={() => login()}>
|
||||
Login
|
||||
</Button>
|
||||
<>
|
||||
<ThemeSwitcherDropdown />
|
||||
<Button size="sm" className="ml-2" color="primary" onClick={() => login()}>
|
||||
Login
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</Navbar>
|
||||
|
@ -81,3 +81,14 @@ export const syncUserFriends = async (user: number, builder: Kysely<GeneratedDB>
|
||||
})
|
||||
.executeTakeFirst();
|
||||
};
|
||||
|
||||
export const getFriends = async (user: number) => {
|
||||
return db.selectFrom('actaeon_user_friends as friend')
|
||||
.where('friend.user1', '=', user)
|
||||
.innerJoin('aime_user as u', 'u.id', 'friend.user2')
|
||||
.innerJoin('actaeon_user_ext as ext', 'ext.userId', 'u.id')
|
||||
.select(['friend.user2 as id', 'friend.chuniRival', 'u.username', 'ext.uuid'])
|
||||
.execute();
|
||||
};
|
||||
|
||||
export type Friend = Awaited<ReturnType<typeof getFriends>>[number];
|
||||
|
Loading…
Reference in New Issue
Block a user