diff --git a/convex/statuses.ts b/convex/statuses.ts
index 9d9c505..189e9a6 100644
--- a/convex/statuses.ts
+++ b/convex/statuses.ts
@@ -9,8 +9,7 @@ import {
import type { Doc, Id } from './_generated/dataModel';
import { paginationOptsValidator } from 'convex/server';
-// NEW: shared ctx type for helpers
-type RWCtx = MutationCtx | QueryCtx;
+type RWCtx = MutationCtx | QueryCtx
type StatusRow = {
user: {
diff --git a/src/app/(auth)/signin/page.tsx b/src/app/(auth)/signin/page.tsx
index 33e3668..4611f98 100644
--- a/src/app/(auth)/signin/page.tsx
+++ b/src/app/(auth)/signin/page.tsx
@@ -24,7 +24,7 @@ import {
TabsTrigger,
} from '@/components/ui';
import { toast } from 'sonner';
-import { PASSWORD_MIN, PASSWORD_MAX, PASSWORD_REGEX } from '@/lib/utils';
+import { PASSWORD_MIN, PASSWORD_MAX, PASSWORD_REGEX } from '@/lib/types';
const signInFormSchema = z.object({
email: z.email({
diff --git a/src/app/(status)/table/layout.tsx b/src/app/(status)/table/layout.tsx
new file mode 100644
index 0000000..b3c3eaa
--- /dev/null
+++ b/src/app/(status)/table/layout.tsx
@@ -0,0 +1,14 @@
+import type { Metadata } from 'next';
+
+export const generateMetadata = (): Metadata => {
+ return {
+ title: 'Status Table',
+ };
+};
+
+const SignInLayout = ({
+ children,
+}: Readonly<{ children: React.ReactNode }>) => {
+ return
{children}
;
+};
+export default SignInLayout;
diff --git a/src/app/(status)/table/page.tsx b/src/app/(status)/table/page.tsx
new file mode 100644
index 0000000..7265ef3
--- /dev/null
+++ b/src/app/(status)/table/page.tsx
@@ -0,0 +1,18 @@
+'use server';
+import { preloadQuery } from 'convex/nextjs';
+import { api } from '~/convex/_generated/api';
+import { StatusTable } from '@/components/layout/status';
+
+const StatusTablePage = async () => {
+ const preloadedUser = await preloadQuery(api.auth.getUser);
+ const preloadedStatuses = await preloadQuery(api.statuses.getCurrentForAll);
+ return (
+
+
+
+ );
+};
+export default StatusTablePage;
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 0cd5ba1..30b5864 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -2,9 +2,6 @@
import { preloadQuery } from 'convex/nextjs';
import { api } from '~/convex/_generated/api';
import { StatusList } from '@/components/layout/status/list';
-import { useConvexAuth, useMutation, useQuery } from 'convex/react';
-import Link from 'next/link';
-import { useAuthActions } from '@convex-dev/auth/react';
const Home = async () => {
const preloadedUser = await preloadQuery(api.auth.getUser);
diff --git a/src/components/layout/profile/reset-password.tsx b/src/components/layout/profile/reset-password.tsx
index 6fb5316..06b8085 100644
--- a/src/components/layout/profile/reset-password.tsx
+++ b/src/components/layout/profile/reset-password.tsx
@@ -21,7 +21,7 @@ import {
SubmitButton,
} from '@/components/ui';
import { toast } from 'sonner';
-import { PASSWORD_MIN, PASSWORD_MAX, PASSWORD_REGEX } from '@/lib/utils';
+import { PASSWORD_MIN, PASSWORD_MAX, PASSWORD_REGEX } from '@/lib/types';
const formSchema = z
.object({
diff --git a/src/components/layout/status/history/index.tsx b/src/components/layout/status/history/index.tsx
index d713bed..6f80398 100644
--- a/src/components/layout/status/history/index.tsx
+++ b/src/components/layout/status/history/index.tsx
@@ -1,10 +1,10 @@
'use client';
import Image from 'next/image';
-import Link from 'next/link';
-import { type Preloaded, usePreloadedQuery, useMutation } from 'convex/react';
+import { useEffect, useMemo, useState } from 'react';
+import { useQuery } from 'convex/react';
import { api } from '~/convex/_generated/api';
-import { type Id } from '~/convex/_generated/dataModel';
-import { useTVMode } from '@/components/providers';
+import type { Id } from '~/convex/_generated/dataModel';
+import { formatDate, formatTime } from '@/lib/utils';
import {
DrawerClose,
DrawerContent,
@@ -13,8 +13,6 @@ import {
DrawerTitle,
Pagination,
PaginationContent,
- PaginationLink,
- PaginationItem,
PaginationNext,
PaginationPrevious,
ScrollArea,
@@ -25,18 +23,183 @@ import {
TableHeader,
TableRow,
Button,
+ BasedAvatar,
} from '@/components/ui';
-import { toast } from 'sonner';
type StatusHistoryProps = {
- userId?: Id<'users'>;
+ user?: typeof api.statuses.getCurrentForAll._returnType[0]['user'],
};
-export const StatusHistory = ({
- userId,
-}: StatusHistoryProps) => {
+const PAGE_SIZE = 25;
+
+export const StatusHistory = ({ user }: StatusHistoryProps) => {
+ const [pageIndex, setPageIndex] = useState(0);
+ // cursor for page N is the continueCursor returned from page N-1
+ const [cursors, setCursors] = useState<(string | null)[]>([null]);
+
+ const args = useMemo(() => {
+ return {
+ userId: user?.id,
+ paginationOpts: {
+ numItems: PAGE_SIZE,
+ cursor: cursors[pageIndex] ?? null,
+ },
+ };
+ }, [user?.id, cursors, pageIndex]);
+
+ const data = useQuery(api.statuses.listHistory, args);
+
+ // Track loading
+ const isLoading = data === undefined;
+
+ // When a page loads, cache its "next" cursor if we don't have it yet
+ useEffect(() => {
+ if (!data) return;
+ const nextIndex = pageIndex + 1;
+ setCursors((prev) => {
+ const copy = [...prev];
+ if (copy[nextIndex] === undefined)
+ copy[nextIndex] = data.continueCursor;
+ return copy;
+ });
+ }, [data, pageIndex]);
+
+ const canPrev = pageIndex > 0;
+ const canNext = !!data && data.continueCursor !== null;
+
+ const handlePrev = () => {
+ if (!canPrev) return;
+ setPageIndex((p) => Math.max(0, p - 1));
+ };
+
+ const handleNext = () => {
+ if (!canNext) return;
+ setPageIndex((p) => p + 1);
+ };
+
+ const rows = data?.page ?? [];
return (
-
+
+
+
+
+ {user ? (
+
+ ) : (
+
+ )}
+
+ {user ? `${user.name ?? 'Technician'}'s History` : 'All History'}
+
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : rows.length === 0 ? (
+
+ ) : (
+
+
+
+ Name
+ Status
+ Updated By
+
+ Date & Time
+
+
+
+
+ {rows.map((r, idx) => (
+
+
+ {r.user.name ?? 'Technician'}
+
+
+
+ {r.status?.message ?? 'No status'}
+
+
+
+ {r.status?.updatedBy?.name ?? ''}
+
+
+ {r.status
+ ? `${formatTime(r.status.updatedAt)} · ${
+ formatDate(r.status.updatedAt)
+ }`
+ : '--:-- · --/--'}
+
+
+ ))}
+
+
+ )}
+
+
+
+
+
+
+
+ {
+ e.preventDefault();
+ handlePrev();
+ }}
+ aria-disabled={!canPrev}
+ className={!canPrev ? 'pointer-events-none opacity-50' : ''}
+ />
+
+ Page
+
+ {pageIndex + 1}
+
+
+ {
+ e.preventDefault();
+ handleNext();
+ }}
+ aria-disabled={!canNext}
+ className={!canNext ? 'pointer-events-none opacity-50' : ''}
+ />
+
+
+
+
+
+
+
+
);
};
diff --git a/src/components/layout/status/list/index.tsx b/src/components/layout/status/list/index.tsx
index b0aca43..04a2add 100644
--- a/src/components/layout/status/list/index.tsx
+++ b/src/components/layout/status/list/index.tsx
@@ -37,11 +37,10 @@ export const StatusList = ({
const [selectAll, setSelectAll] = useState(false);
const [statusInput, setStatusInput] = useState('');
const [updatingStatus, setUpdatingStatus] = useState(false);
- const [selectedHistoryUserId, setSelectedHistoryUserId] = useState>();
const bulkCreate = useMutation(api.statuses.bulkCreate);
- const toggleUser = (id: Id<'users'>) => {
+ const handleSelectUser = (id: Id<'users'>, e: React.MouseEvent) => {
setSelectedUserIds((prev) =>
prev.some((i) => i === id)
? prev.filter((prevId) => prevId !== id)
@@ -49,7 +48,7 @@ export const StatusList = ({
);
};
- const handleSelectAllClick = () => {
+ const handleSelectAll = () => {
if (selectAll) setSelectedUserIds([]);
else setSelectedUserIds(statuses.map((s) => s.user.id));
setSelectAll(!selectAll);
@@ -110,7 +109,7 @@ export const StatusList = ({
diff --git a/src/components/layout/status/table/index.tsx b/src/components/layout/status/table/index.tsx
index 91b9b33..614d0da 100644
--- a/src/components/layout/status/table/index.tsx
+++ b/src/components/layout/status/table/index.tsx
@@ -18,6 +18,7 @@ import {
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
;
@@ -39,7 +40,7 @@ export const StatusTable = ({
const bulkCreate = useMutation(api.statuses.bulkCreate);
- const toggleUser = (id: Id<'users'>) => {
+ const handleSelectUser = (id: Id<'users'>, e: React.MouseEvent) => {
setSelectedUserIds((prev) =>
prev.some((i) => i === id)
? prev.filter((prevId) => prevId !== id)
@@ -47,7 +48,7 @@ export const StatusTable = ({
);
};
- const handleSelectAllClick = () => {
+ const handleSelectAll = () => {
if (selectAll) setSelectedUserIds([]);
else setSelectedUserIds(statuses.map((s) => s.user.id));
setSelectAll(!selectAll);
@@ -61,7 +62,6 @@ export const StatusTable = ({
throw new Error('Status must be between 3 & 80 characters');
if (selectedUserIds.length === 0 && user?.id)
await bulkCreate({ message, userIds: [user.id] });
- else throw new Error("Hmm.. this shouldn't happen");
await bulkCreate({ message, userIds: selectedUserIds });
toast.success('Status updated.');
setSelectedUserIds([]);
@@ -76,224 +76,77 @@ export const StatusTable = ({
const containerCn = ccn({
context: tvMode,
- className:
- 'flex flex-col mx-auto items-center\
- sm:w-5/6 md:w-3/4 lg:w-2/3 xl:w-1/2 min-w-[450px]',
- on: 'mt-8',
- off: 'px-10',
+ className: 'mx-auto',
+ on: 'lg:w-11/12 w-full',
+ off: 'w-5/6',
});
-
const headerCn = ccn({
context: tvMode,
- className: 'w-full',
- on: 'hidden',
- off: 'flex mb-3 justify-between items-center',
+ className: 'w-full mb-2 flex justify-between',
+ on: 'mt-25',
+ off: 'mb-2',
});
-
- const selectAllIconCn = ccn({
- context: selectAll,
- className: 'w-4 h-4',
- on: 'text-green-500',
- off: '',
- });
-
- const cardContainerCn = ccn({
+ const thCn = ccn({
context: tvMode,
- className: 'w-full space-y-2',
- on: 'text-primary',
- off: '',
+ className: 'py-4 px-4 border font-semibold ',
+ on: 'lg:text-5xl xl:min-w-[420px]',
+ off: 'lg:text-4xl xl:min-w-[320px]',
});
+ const tdCn = ccn({
+ context: tvMode,
+ className: 'py-2 px-2 border',
+ on: 'lg:text-4xl',
+ off: 'lg:text-3xl',
+ });
+ const tCheckboxCn = `py-3 px-4 border`;
+ const checkBoxCn = `lg:scale-200 cursor-pointer`;
return (
-
-
+
{!tvMode && (
-
-
Miss the old table?
-
- Find it here!
+
+
Tired of the old table?
+
+ Try the new status list!
)}
-
-
- {statuses.map((status) => {
- const { user: u, status: s } = status;
- const isSelected = selectedUserIds.includes(u.id);
- const isUpdatedByOther = !!s?.updatedBy;
- return (
-
toggleUser(u.id)}
- >
- {isSelected && (
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
- {u.name ?? 'Technician'}
-
-
-
-
{s?.message ?? 'No status yet.'}
-
-
-
-
-
-
-
- {s ? formatTime(s.updatedAt) : '--:--'}
-
-
-
-
-
- {s ? formatDate(s.updatedAt) : '--/--'}
-
-
-
- {isUpdatedByOther && s.updatedBy && (
-
-
-
-
-
Updated by
- {s.updatedBy.name ?? 'User'}
-
-
-
- )}
-
-
-
-
-
-
- );
- })}
-
-
- {statuses.length === 0 && (
-
-
- No status updates have been made in the past day.
-
-
- )}
-
- {!tvMode && (
-
-
-
Update Status
-
-
);
};
diff --git a/src/lib/types.ts b/src/lib/types.ts
new file mode 100644
index 0000000..df87296
--- /dev/null
+++ b/src/lib/types.ts
@@ -0,0 +1,5 @@
+export const PASSWORD_MIN = 8;
+export const PASSWORD_MAX = 100;
+export const PASSWORD_REGEX = /^(?=.{8,100}$)(?!.*\s)(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\p{P}\p{S}]).*$/u;
+
+export type Timestamp = number | string | Date;
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index c35ee5e..dc5994f 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,5 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
+import { type Timestamp } from '@/lib/types';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
@@ -19,11 +20,6 @@ export const ccn = ({
return twMerge(className, context ? on : off);
};
-export const PASSWORD_MIN = 8;
-export const PASSWORD_MAX = 100;
-export const PASSWORD_REGEX = /^(?=.{8,100}$)(?!.*\s)(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\p{P}\p{S}]).*$/u;
-
-type Timestamp = number | string | Date;
const toDate = (ts: Timestamp): Date | null => {
if (ts instanceof Date) return isNaN(ts.getTime()) ? null : ts;