Move to monorepo for React Native!
This commit is contained in:
301
apps/next/src/components/layout/status/table/index.tsx
Normal file
301
apps/next/src/components/layout/status/table/index.tsx
Normal file
@@ -0,0 +1,301 @@
|
||||
'use client';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { type Preloaded, usePreloadedQuery, useMutation } from 'convex/react';
|
||||
import { api } from '~/convex/_generated/api';
|
||||
import { type Id } from '~/convex/_generated/dataModel';
|
||||
import { useTVMode } from '@/components/providers';
|
||||
import {
|
||||
BasedAvatar,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Drawer,
|
||||
DrawerTrigger,
|
||||
Input,
|
||||
SubmitButton,
|
||||
} from '@/components/ui';
|
||||
import { toast } from 'sonner';
|
||||
import { ccn, formatTime, formatDate } from '@/lib/utils';
|
||||
import { Clock, Calendar, CheckCircle2 } from 'lucide-react';
|
||||
import { StatusHistory } from '@/components/layout/status';
|
||||
|
||||
type StatusTableProps = {
|
||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
||||
preloadedStatuses: Preloaded<typeof api.statuses.getCurrentForAll>;
|
||||
};
|
||||
|
||||
export const StatusTable = ({
|
||||
preloadedUser,
|
||||
preloadedStatuses,
|
||||
}: StatusTableProps) => {
|
||||
const user = usePreloadedQuery(preloadedUser);
|
||||
const statuses = usePreloadedQuery(preloadedStatuses);
|
||||
|
||||
const { tvMode } = useTVMode();
|
||||
const [selectedUserIds, setSelectedUserIds] = useState<Id<'users'>[]>([]);
|
||||
const [selectAll, setSelectAll] = useState(false);
|
||||
const [statusInput, setStatusInput] = useState('');
|
||||
const [updatingStatus, setUpdatingStatus] = useState(false);
|
||||
|
||||
const bulkCreate = useMutation(api.statuses.bulkCreate);
|
||||
|
||||
const handleSelectUser = (id: Id<'users'>) => {
|
||||
setSelectedUserIds((prev) =>
|
||||
prev.some((i) => i === id)
|
||||
? prev.filter((prevId) => prevId !== id)
|
||||
: [...prev, id],
|
||||
);
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectAll) setSelectedUserIds([]);
|
||||
else setSelectedUserIds(statuses.map((s) => s.user.id));
|
||||
setSelectAll(!selectAll);
|
||||
};
|
||||
|
||||
const handleUpdateStatus = async () => {
|
||||
const message = statusInput.trim();
|
||||
setUpdatingStatus(true);
|
||||
try {
|
||||
if (message.length < 3 || message.length > 80)
|
||||
throw new Error('Status must be between 3 & 80 characters');
|
||||
if (selectedUserIds.length === 0 && user?.id)
|
||||
await bulkCreate({ message, userIds: [user.id] });
|
||||
await bulkCreate({ message, userIds: selectedUserIds });
|
||||
toast.success('Status updated.');
|
||||
setSelectedUserIds([]);
|
||||
setSelectAll(false);
|
||||
setStatusInput('');
|
||||
} catch (error) {
|
||||
toast.error(`Update failed. ${error as Error}`);
|
||||
} finally {
|
||||
setUpdatingStatus(false);
|
||||
}
|
||||
};
|
||||
|
||||
const containerCn = ccn({
|
||||
context: tvMode,
|
||||
className: 'mx-auto',
|
||||
on: 'lg:w-11/12 w-full',
|
||||
off: 'w-5/6',
|
||||
});
|
||||
const headerCn = ccn({
|
||||
context: tvMode,
|
||||
className: 'w-full mb-2 flex justify-between',
|
||||
on: '',
|
||||
off: 'mb-2',
|
||||
});
|
||||
const thCn = ccn({
|
||||
context: tvMode,
|
||||
className: 'py-4 px-4 border font-semibold ',
|
||||
on: 'lg:text-6xl xl:min-w-[420px]',
|
||||
off: 'lg:text-5xl xl:min-w-[320px]',
|
||||
});
|
||||
const tdCn = ccn({
|
||||
context: tvMode,
|
||||
className: 'py-2 px-2 border',
|
||||
on: 'lg:text-5xl',
|
||||
off: 'lg:text-4xl',
|
||||
});
|
||||
const tCheckboxCn = `py-3 px-4 border`;
|
||||
const checkBoxCn = `lg:scale-200 cursor-pointer`;
|
||||
|
||||
return (
|
||||
<div className={containerCn}>
|
||||
<div className={headerCn}>
|
||||
<div className='flex items-center gap-2'>
|
||||
{!tvMode && (
|
||||
<div className='flex flex-row gap-2 text-xs'>
|
||||
<p className='text-muted-foreground'>Tired of the old table? </p>
|
||||
<Link
|
||||
href='/'
|
||||
className='italic font-semibold hover:text-primary/80'
|
||||
>
|
||||
Try the new status list!
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<table className='w-full text-center rounded-md'>
|
||||
<thead>
|
||||
<tr className='bg-muted'>
|
||||
{!tvMode && (
|
||||
<th className={tCheckboxCn}>
|
||||
<input
|
||||
type='checkbox'
|
||||
className={checkBoxCn}
|
||||
checked={selectAll}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
</th>
|
||||
)}
|
||||
<th className={thCn}>Technician</th>
|
||||
<th className={thCn}>
|
||||
<Drawer>
|
||||
<DrawerTrigger className='hover:text-foreground/60 cursor-pointer'>
|
||||
Status
|
||||
</DrawerTrigger>
|
||||
<StatusHistory />
|
||||
</Drawer>
|
||||
</th>
|
||||
<th className={thCn}>Updated At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{statuses.map((status, i) => {
|
||||
const { user: u, status: s } = status;
|
||||
const isSelected = selectedUserIds.includes(u.id);
|
||||
return (
|
||||
<tr
|
||||
key={u.id}
|
||||
className={`
|
||||
${i % 2 === 0 ? 'bg-muted/50' : 'bg-background'}
|
||||
${isSelected ? 'ring-2 ring-primary' : ''}
|
||||
hover:bg-muted/75 transition-all duration-300
|
||||
`}
|
||||
>
|
||||
{!tvMode && (
|
||||
<td className={tCheckboxCn}>
|
||||
<input
|
||||
type='checkbox'
|
||||
className={checkBoxCn}
|
||||
checked={isSelected}
|
||||
onChange={() => handleSelectUser(u.id)}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
<td className={tdCn}>
|
||||
<div className='flex items-center gap-3'>
|
||||
<BasedAvatar
|
||||
src={u.imageUrl}
|
||||
fullName={u.name}
|
||||
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
|
||||
/>
|
||||
<div>
|
||||
<p> {u.name ?? 'Technician #' + (i + 1)} </p>
|
||||
{s?.updatedBy && s.updatedBy.id !== u.id && (
|
||||
<div className='flex items-center gap-1 text-muted-foreground'>
|
||||
<BasedAvatar
|
||||
src={s.updatedBy.imageUrl}
|
||||
fullName={s.updatedBy.name}
|
||||
className='w-5 h-5'
|
||||
/>
|
||||
<span className={tvMode ? 'text-xl' : 'text-base'}>
|
||||
Updated by {s.updatedBy.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className={tdCn}>
|
||||
<Drawer>
|
||||
<DrawerTrigger>{s?.message}</DrawerTrigger>
|
||||
<StatusHistory user={u} />
|
||||
</Drawer>
|
||||
</td>
|
||||
<td className={tdCn}>
|
||||
<Drawer>
|
||||
<DrawerTrigger>
|
||||
<div className='flex w-full'>
|
||||
<div className='flex flex-col my-auto items-start'>
|
||||
<div className='flex gap-4 my-1'>
|
||||
<Clock
|
||||
className={`${tvMode ? 'lg:w-11 lg:h-11' : 'lg:w-9 lg:h-9'}`}
|
||||
/>
|
||||
<p
|
||||
className={`${tvMode ? 'text-4xl' : 'text-3xl'}`}
|
||||
>
|
||||
{s ? formatTime(s.updatedAt) : '--:--'}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex gap-4 my-1'>
|
||||
<Calendar
|
||||
className={`${tvMode ? 'lg:w-11 lg:h-11' : 'lg:w-9 lg:h-9'}`}
|
||||
/>
|
||||
<p
|
||||
className={`${tvMode ? 'text-4xl' : 'text-3xl'}`}
|
||||
>
|
||||
{s ? formatDate(s.updatedAt) : '--:--'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerTrigger>
|
||||
<StatusHistory user={u} />
|
||||
</Drawer>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{statuses.length === 0 && (
|
||||
<div className='p-8 text-center'>
|
||||
<p
|
||||
className={`text-muted-foreground ${tvMode ? 'text-4xl' : 'text-lg'}`}
|
||||
>
|
||||
No status updates yet
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!tvMode && (
|
||||
<div className='mx-auto flex flex-row items-center justify-center py-5 gap-4'>
|
||||
<Input
|
||||
autoFocus
|
||||
type='text'
|
||||
placeholder='New Status'
|
||||
className={
|
||||
'min-w-[120px] lg:max-w-[400px] py-6 px-3 rounded-xl \
|
||||
border bg-background lg:text-2xl focus:outline-none \
|
||||
focus:ring-2 focus:ring-primary'
|
||||
}
|
||||
value={statusInput}
|
||||
disabled={updatingStatus}
|
||||
onChange={(e) => setStatusInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && !updatingStatus) {
|
||||
e.preventDefault();
|
||||
void handleUpdateStatus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<SubmitButton
|
||||
className={
|
||||
'px-8 rounded-xl font-semibold lg:text-2xl \
|
||||
disabled:opacity-50 disabled:cursor-not-allowed \
|
||||
cursor-pointer'
|
||||
}
|
||||
onClick={handleUpdateStatus}
|
||||
disabled={updatingStatus}
|
||||
pendingText='Updating...'
|
||||
>
|
||||
{selectedUserIds.length > 0
|
||||
? `Update status for ${selectedUserIds.length}
|
||||
${selectedUserIds.length > 1 ? 'users' : 'user'}`
|
||||
: 'Update status'}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Global Status History Drawer */}
|
||||
{!tvMode && (
|
||||
<div className='flex justify-center mt-6'>
|
||||
<Drawer>
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
className={tvMode ? 'text-3xl p-6' : ''}
|
||||
>
|
||||
View All Status History
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<StatusHistory />
|
||||
</Drawer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user