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';
|
'use server';
|
||||||
|
|
||||||
import { db } from '@/db';
|
import { GeneratedDB, db } from '@/db';
|
||||||
import { getUser, requireUser } from './auth';
|
import { getUser, requireUser } from './auth';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { CompiledQuery, sql } from 'kysely';
|
import { CompiledQuery, Transaction, sql } from 'kysely';
|
||||||
import { syncUserFriends, withChuniRivalCount } from '@/data/friend';
|
import { syncUserFriends, withChuniRivalCount } from '@/data/friend';
|
||||||
import { SqlBool } from 'kysely';
|
import { SqlBool } from 'kysely';
|
||||||
import { Exact } from 'type-fest';
|
import { Exact } from 'type-fest';
|
||||||
@ -131,3 +131,58 @@ export const rejectFriendRequest = async (id: string) => {
|
|||||||
.where('uuid', '=', id)
|
.where('uuid', '=', id)
|
||||||
.executeTakeFirst();
|
.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 { Bars3Icon, ChevronLeftIcon, XMarkIcon } from '@heroicons/react/20/solid';
|
||||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||||
import Link from 'next/link';
|
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 { AdjustmentsHorizontalIcon } from '@heroicons/react/24/solid';
|
||||||
import { login, logout } from '@/actions/auth';
|
import { login, logout } from '@/actions/auth';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
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 { useUser } from '@/helpers/use-user';
|
||||||
import { useBreakpoint } from '@/helpers/use-breakpoint';
|
import { useBreakpoint } from '@/helpers/use-breakpoint';
|
||||||
import { useCookies } from 'next-client-cookies';
|
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 { useTheme } from 'next-themes';
|
||||||
import { BellAlertIcon } from '@heroicons/react/24/solid';
|
import { BellAlertIcon } from '@heroicons/react/24/solid';
|
||||||
import { FriendRequest, acceptFriendRequest, getFriendRequests, rejectFriendRequest } from '@/actions/friend';
|
import { FriendRequest, acceptFriendRequest, getFriendRequests, rejectFriendRequest } from '@/actions/friend';
|
||||||
@ -248,7 +248,23 @@ export const HeaderSidebar = ({ children }: HeaderSidebarProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</header>
|
</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">
|
<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>
|
<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">
|
<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>
|
<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>
|
||||||
<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">
|
<Link href="/settings" className="w-full h-full block px-2 py-1.5">
|
||||||
Settings
|
Settings
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownItem>
|
</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')}>
|
<div className="w-full flex pl-2 h-8 items-center" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
|
||||||
Dark Theme
|
Dark Theme
|
||||||
|
|
||||||
@ -340,10 +361,12 @@ export const HeaderSidebar = ({ children }: HeaderSidebarProps) => {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</> :
|
</> :
|
||||||
|
<>
|
||||||
|
<ThemeSwitcherDropdown />
|
||||||
<Button size="sm" className="ml-2" color="primary" onClick={() => login()}>
|
<Button size="sm" className="ml-2" color="primary" onClick={() => login()}>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
@ -81,3 +81,14 @@ export const syncUserFriends = async (user: number, builder: Kysely<GeneratedDB>
|
|||||||
})
|
})
|
||||||
.executeTakeFirst();
|
.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