hook up config for users to add cards

This commit is contained in:
sk1982 2024-04-02 01:31:25 -04:00
parent 28ae102c02
commit 619c8bf2cd
6 changed files with 128 additions and 40 deletions

View File

@ -3,15 +3,16 @@
import { requireUser } from '@/actions/auth';
import { db } from '@/db';
import { UserPermissions } from '@/types/permissions';
import { requirePermission } from '@/helpers/permissions';
import { hasPermission, requirePermission } from '@/helpers/permissions';
import { addCardToUser } from '@/data/card';
import { AdminUser, getUsers } from '@/data/user';
import { ActionResult } from '@/types/action-result';
import { revalidatePath } from 'next/cache';
import { getGlobalConfig } from '@/config';
import { UserPayload } from '@/types/user';
import { DB } from '@/types/db';
export const getCards = async () => {
const user = await requireUser();
export const getCards = async (user: UserPayload) => {
return db.selectFrom('aime_card')
.where('user', '=', user.id)
.selectAll()
@ -63,3 +64,36 @@ export const adminAddCardToUser = async (user: number, code: string): Promise<Ac
return { data: await getUsers() };
};
export const userAddCard = async (code: string): Promise<ActionResult<{ card: DB['aime_card'] }>> => {
const user = await requireUser();
if (!hasPermission(user.permissions, UserPermissions.USERMOD)) {
const cards = await getCards(user);
if (!getGlobalConfig('allow_user_add_card'))
return { error: true, message: 'You do not have permissions to add a card' };
if (cards.length >= (getGlobalConfig('user_max_card') ?? Infinity))
return { error: true, message: 'You cannot add a card because you have reached your max card count' };
}
const res = await addCardToUser(user.id, code);
if (res.error)
return res;
revalidatePath('/settings', 'page');
revalidatePath('/admin/users', 'page');
return {
card: {
id: res.id,
access_code: code,
user: user.id,
created_date: new Date(),
is_locked: 0,
is_banned: 0,
last_login_date: null
}};
};

View File

@ -7,8 +7,6 @@ import { USER_PERMISSION_NAMES, UserPermissions } from '@/types/permissions';
import { Button, Divider, Tooltip, Input, Accordion, AccordionItem, Spacer } from '@nextui-org/react';
import { ChevronDownIcon, CreditCardIcon, PencilSquareIcon, PlusIcon } from '@heroicons/react/24/outline';
import { usePromptModal } from '@/components/prompt-modal';
import { ArrowPathIcon } from '@heroicons/react/24/outline';
import { generateAccessCode } from '@/helpers/access-code';
import { useUser } from '@/helpers/use-user';
import { hasPermission } from '@/helpers/permissions';
import { AimeCard } from '@/components/aime-card';
@ -19,6 +17,7 @@ import { TrashIcon } from '@heroicons/react/24/outline';
import { useConfirmModal } from '@/components/confirm-modal';
import Link from 'next/link';
import { PermissionIcon } from '@/components/permission-icon';
import { promptAccessCode } from '@/components/prompt-access-code';
const FORMAT = {
month: 'numeric',
@ -42,33 +41,13 @@ export const AdminUserList = ({ users: initialUsers }: { users: AdminUser[]; })
setOpenUsers(new Set([window.location.hash.slice(1)]));
}, []);
const promptAccessCode = (message: string, onConfirm: (val: string) => void) => {
prompt({
size: '2xl',
title: 'Enter Access Code', content: (val, setVal) => <>
{ message }
<div className="flex overflow-hidden rounded-lg">
<Input label="Access Code" inputMode="numeric" size="sm" type="text" maxLength={24} radius="none"
classNames={{ input: `[font-feature-settings:"fwid"] text-xs sm:text-sm` }}
value={val.match(/.{1,4}/g)?.join('-') ?? ''}
onValueChange={v => setVal(v.replace(/\D/g, ''))} />
<Tooltip content="Generate Random Code">
<Button isIconOnly color="primary" size="lg" radius="none" onPress={() => setVal(generateAccessCode())}>
<ArrowPathIcon className="h-7" />
</Button>
</Tooltip>
</div>
</>
}, v => onConfirm(v.replace(/\D/g, '')));
}
return (<main className="max-w-5xl mx-auto w-full">
<header className="p-4 font-semibold text-2xl flex items-center">
Users
<Tooltip content="Create new user">
<Button isIconOnly className="ml-auto"
onPress={() => promptAccessCode('Enter an access code to create this user', code => {
onPress={() => promptAccessCode(prompt, 'Enter an access code to create this user', code => {
createUserWithAccessCode(code)
.then(res => {
if (res.error)
@ -172,7 +151,7 @@ export const AdminUserList = ({ users: initialUsers }: { users: AdminUser[]; })
}} />)}
</div>
<Tooltip content="Add new card to this user">
<Button isIconOnly onPress={() => promptAccessCode('Enter an access code to add',
<Button isIconOnly onPress={() => promptAccessCode(prompt, 'Enter an access code to add',
code => adminAddCardToUser(userEntry.id, code).then(res => {
if (res.error)
return setError(res.message);

View File

@ -0,0 +1,53 @@
'use client';
import { DB } from '@/types/db';
import { AimeCard } from '@/components/aime-card';
import { Button, Divider, Tooltip } from '@nextui-org/react';
import { PlusIcon } from '@heroicons/react/24/outline';
import { usePromptModal } from '@/components/prompt-modal';
import { promptAccessCode } from '@/components/prompt-access-code';
import { useUser } from '@/helpers/use-user';
import { UserPermissions } from '@/types/permissions';
import { hasPermission } from '@/helpers/permissions';
import { useErrorModal } from '@/components/error-modal';
import { useState } from 'react';
import { userAddCard } from '@/actions/card';
export type CardsProps = {
cards: DB['aime_card'][],
canAddCard: boolean,
maxCard: number | null
};
export const Cards = ({ cards: initialCards, canAddCard, maxCard }: CardsProps) => {
const prompt = usePromptModal();
const user = useUser();
const setError = useErrorModal();
const [cards, setCards] = useState(initialCards);
return (<section className="w-full rounded-lg sm:bg-content1 sm:shadow-lg">
<header className="text-2xl font-semibold flex px-4 h-16 items-center">
Cards
{(hasPermission(user?.permissions, UserPermissions.USERMOD) ||
(canAddCard && cards.length < (maxCard ?? Infinity))) && <Tooltip content="Add card">
<Button isIconOnly className="ml-auto" onPress={() => {
promptAccessCode(prompt, 'Enter an access code for this card', code => {
userAddCard(code)
.then(res => {
if (res.error)
return setError(res.message);
setCards(c => [...c, res.card]);
})
});
}}>
<PlusIcon className="h-3/4" />
</Button>
</Tooltip>}
</header>
<Divider className="mb-4 hidden sm:block" />
<div className="px-1 sm:px-4 sm:pb-4 flex flex-wrap items-center justify-center gap-4">
{cards.map(c => <AimeCard key={c.id} card={c} className="w-full" />)}
</div>
</section>);
};

View File

@ -1,26 +1,24 @@
import { getCards } from '@/actions/card';
import { Divider } from '@nextui-org/react';
import { AimeCard } from '@/components/aime-card';
import { UserSettings } from './user-settings';
import { Cards } from './cards';
import { requireUser } from '@/actions/auth';
import { getGlobalConfig } from '@/config';
export default async function SettingsPage() {
const card = await getCards();
const user = await requireUser();
const cards = await getCards(user);
return (<div className="w-full flex items-center justify-center">
<div className="w-full max-w-full sm:max-w-5xl flex flex-col gap-2 2xl:max-w-screen-4xl 2xl:grid grid-cols-2">
<div>
<div className="w-full max-w-full sm:max-w-5xl flex flex-col gap-2 2xl:max-w-screen-4xl 2xl:grid grid-cols-12">
<div className="col-span-3">
<UserSettings />
</div>
<Divider className="block sm:hidden mt-2" />
<div className="w-full rounded-lg sm:bg-content1 sm:shadow-lg">
<div className="text-2xl font-semibold p-4">Cards</div>
<Divider className="mb-4 hidden sm:block" />
<div className="px-1 sm:px-4 sm:pb-4 flex flex-wrap items-center justify-center gap-4">
{card.map(c => <AimeCard key={c.id} card={c} className="w-full" />)}
</div>
<div className="col-span-9">
<Cards cards={cards} canAddCard={getGlobalConfig('allow_user_add_card')} maxCard={getGlobalConfig('user_max_card') ?? Infinity} />
</div>
</div>
</div>);

View File

@ -0,0 +1,24 @@
import { generateAccessCode } from '@/helpers/access-code';
import { PromptCallback } from './prompt-modal';
import { Button, Input, Tooltip } from '@nextui-org/react';
import { ArrowPathIcon } from '@heroicons/react/24/outline';
export const promptAccessCode = (prompt: PromptCallback, message: string, onConfirm: (val: string) => void) => {
prompt({
size: '2xl',
title: 'Enter Access Code', content: (val, setVal) => <>
{message}
<div className="flex overflow-hidden rounded-lg">
<Input label="Access Code" inputMode="numeric" size="sm" type="text" maxLength={24} radius="none"
classNames={{ input: `[font-feature-settings:"fwid"] text-xs sm:text-sm` }}
value={val.match(/.{1,4}/g)?.join('-') ?? ''}
onValueChange={v => setVal(v.replace(/\D/g, ''))} />
<Tooltip content="Generate Random Code">
<Button isIconOnly color="primary" size="lg" radius="none" onPress={() => setVal(generateAccessCode())}>
<ArrowPathIcon className="h-7" />
</Button>
</Tooltip>
</div>
</>
}, v => onConfirm(v.replace(/\D/g, '')));
};

View File

@ -6,7 +6,7 @@ import { useHashNavigation } from '@/helpers/use-hash-navigation';
type PromptOptions = { title: string, size?: ModalProps['size'] } &
(({ message: string, content?: never } & Partial<Pick<InputProps, 'type' | 'name' | 'label' | 'placeholder'>>) |
{ content: (value: string, setValue: (v: string) => void) => ReactNode, message?: never });
type PromptCallback = (options: PromptOptions, onConfirm: (val: string) => void, onCancel?: () => void) => void;
export type PromptCallback = (options: PromptOptions, onConfirm: (val: string) => void, onCancel?: () => void) => void;
const PromptContext = createContext<PromptCallback>(() => {});
export const PromptProvider = ({ children }: { children: ReactNode }) => {