So its like broken but we are rewriting status.ts & TechTable & HistoryTable

This commit is contained in:
2025-06-12 16:55:24 -05:00
parent 185a7ea500
commit 653fe64bbf
23 changed files with 2536 additions and 647 deletions

View File

@ -20,11 +20,14 @@
"@hookform/resolvers": "^5.1.1", "@hookform/resolvers": "^5.1.1",
"@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@sentry/nextjs": "^9.28.0", "@sentry/nextjs": "^9.28.1",
"@supabase/ssr": "^0.6.1", "@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.50.0", "@supabase/supabase-js": "^2.50.0",
"@t3-oss/env-nextjs": "^0.12.0", "@t3-oss/env-nextjs": "^0.12.0",
@ -39,26 +42,27 @@
"react-hook-form": "^7.57.0", "react-hook-form": "^7.57.0",
"require-in-the-middle": "^7.5.2", "require-in-the-middle": "^7.5.2",
"sonner": "^2.0.5", "sonner": "^2.0.5",
"zod": "^3.25.58" "vaul": "^1.1.2",
"zod": "^3.25.63"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.1.8", "@tailwindcss/postcss": "^4.1.10",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.3", "@types/express": "^5.0.3",
"@types/node": "^20.19.0", "@types/node": "^20.19.0",
"@types/react": "^19.1.7", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"eslint": "^9.28.0", "eslint": "^9.28.0",
"eslint-config-next": "^15.3.3", "eslint-config-next": "^15.3.3",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.4.1", "eslint-plugin-prettier": "^5.4.1",
"import-in-the-middle": "^1.14.0", "import-in-the-middle": "^1.14.0",
"postcss": "^8.5.4", "postcss": "^8.5.5",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.12", "prettier-plugin-tailwindcss": "^0.6.12",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.8", "tailwindcss": "^4.1.10",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.3.4", "tw-animate-css": "^1.3.4",
"typescript": "^5.8.3", "typescript": "^5.8.3",

1064
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -406,11 +406,11 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
selfHosted={true} selfHosted={true}
> >
<TVModeProvider> <TVModeProvider>
<div className='min-h-screen'> <main className='min-h-screen'>
<Header /> <Header />
{children} {children}
<Toaster /> <Toaster />
</div> </main>
<Footer /> <Footer />
</TVModeProvider> </TVModeProvider>
</PlausibleProvider> </PlausibleProvider>

View File

@ -1,19 +1,29 @@
'use server'; 'use server';
import { getUser } from '@/lib/actions';
import type { User } from '@/utils/supabase';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { getUser } from '@/lib/actions';
const HomePage = async () => { const Home = async () => {
const response = await getUser(); const userResponse = await getUser();
if (!response.success || !response.data) { if (!userResponse.success) {
redirect('/sign-in'); redirect('/sign-in');
} else if (userResponse.data) {
redirect('/status');
} else return <div/>;
} }
const user: User = response.data; export default Home;
return (
<div> //'use client';
<h1>Hello {user.email}</h1>
</div> ////import { TechTable } from '@/components/status';
) //import { redirect } from 'next/navigation';
}; //import { useAuth } from '@/components/context';
export default HomePage;
//const HomePage = () => {
//const { isAuthenticated } = useAuth();
//if (!isAuthenticated) {
//redirect('/sign-in');
//}
//redirect('/profile');
//};
//export default HomePage;

15
src/app/status/page.tsx Normal file
View File

@ -0,0 +1,15 @@
'use client';
import { TechTable } from '@/components/status';
import { useAuth } from '@/components/context';
import { redirect } from 'next/navigation';
const Status = () => {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
redirect('/sign-in');
} else {
return <TechTable />;
}
};
export default Status;

View File

@ -0,0 +1,236 @@
'use client';
import React, { useState, useEffect, useCallback } from 'react';
import Image from 'next/image';
import {
DrawerClose,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerTitle,
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationNext,
PaginationPrevious,
ScrollArea,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Button,
} from '@/components/ui';
import {
getUserHistory,
getAllHistory,
type HistoryEntry,
} from '@/lib/hooks';
import { toast } from 'sonner';
type HistoryDrawerProps = {
user_id: string;
};
export const HistoryDrawer: React.FC<HistoryDrawerProps> = ({ user_id }) => {
const [history, setHistory] = useState<HistoryEntry[]>([]);
const [page, setPage] = useState<number>(1);
const [totalPages, setTotalPages] = useState<number>(1);
const [totalCount, setTotalCount] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(true);
const perPage = 50;
const fetchHistory = useCallback(async (currentPage: number) => {
setLoading(true);
try {
let response;
if (user_id && user_id !== '') {
response = await getUserHistory(user_id, currentPage, perPage);
} else {
response = await getAllHistory(currentPage, perPage);
}
if (!response.success) {
throw new Error(response.error);
}
setHistory(response.data.data);
setTotalPages(response.data.meta.total_pages);
setTotalCount(response.data.meta.total_count);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
toast.error(`Error fetching history: ${errorMessage}`);
setHistory([]);
setTotalPages(1);
setTotalCount(0);
} finally {
setLoading(false);
}
}, [user_id, perPage]);
useEffect(() => {
void fetchHistory(page);
}, [page, fetchHistory]);
const handlePageChange = (newPage: number) => {
if (newPage >= 1 && newPage <= totalPages) {
setPage(newPage);
}
};
const formatTime = (timestamp: string) => {
const date = new Date(timestamp);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
});
};
const getDisplayName = (entry: HistoryEntry) => {
return entry.user_profile?.full_name || 'Unknown User';
};
const getUpdatedByName = (entry: HistoryEntry) => {
if (!entry.updated_by) return 'Self';
return entry.updated_by.full_name || 'Unknown';
};
return (
<DrawerContent className='max-w-4xl mx-auto'>
<DrawerHeader>
<DrawerTitle>
<div className='flex flex-row items-center justify-center py-4'>
<Image
src='/favicon.png'
alt='Tech Tracker Logo'
width={60}
height={60}
className='w-8 h-8 md:w-12 md:h-12'
/>
<h1 className='text-lg md:text-2xl lg:text-4xl font-bold pl-2 md:pl-4'>
{user_id && user_id !== '' ? 'User History' : 'All History'}
</h1>
</div>
{totalCount > 0 && (
<p className='text-sm text-muted-foreground text-center'>
{totalCount} total entries
</p>
)}
</DrawerTitle>
</DrawerHeader>
<div className='px-4'>
<ScrollArea className='h-96 w-full'>
{loading ? (
<div className='flex justify-center items-center h-32'>
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-primary'></div>
</div>
) : history.length === 0 ? (
<div className='flex justify-center items-center h-32'>
<p className='text-muted-foreground'>No history found</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className='font-semibold'>Name</TableHead>
<TableHead className='font-semibold'>Status</TableHead>
<TableHead className='font-semibold'>Updated By</TableHead>
<TableHead className='font-semibold text-right'>Date & Time</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{history.map((entry) => (
<TableRow key={entry.id}>
<TableCell className='font-medium'>
{getDisplayName(entry)}
</TableCell>
<TableCell className='max-w-xs'>
<div className='truncate' title={entry.status}>
{entry.status}
</div>
</TableCell>
<TableCell className='text-sm text-muted-foreground'>
{getUpdatedByName(entry)}
</TableCell>
<TableCell className='text-right text-sm'>
{formatTime(entry.created_at)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</ScrollArea>
</div>
<DrawerFooter>
{totalPages > 1 && (
<Pagination>
<PaginationContent>
{page > 1 && (
<PaginationItem>
<PaginationPrevious
href='#'
onClick={(e) => {
e.preventDefault();
handlePageChange(page - 1);
}}
/>
</PaginationItem>
)}
{totalPages > 10 ? (
<div className='flex items-center gap-2 text-sm text-muted-foreground'>
<span>Page</span>
<span className='font-bold text-foreground'>{page}</span>
<span>of</span>
<span className='font-semibold text-foreground'>{totalPages}</span>
</div>
) : (
Array.from({ length: totalPages }).map((_, idx) => (
<PaginationItem key={idx}>
<PaginationLink
href='#'
isActive={page === idx + 1}
onClick={(e) => {
e.preventDefault();
handlePageChange(idx + 1);
}}
>
{idx + 1}
</PaginationLink>
</PaginationItem>
))
)}
{page < totalPages && (
<PaginationItem>
<PaginationNext
href='#'
onClick={(e) => {
e.preventDefault();
handlePageChange(page + 1);
}}
/>
</PaginationItem>
)}
</PaginationContent>
</Pagination>
)}
<DrawerClose asChild>
<Button variant='outline' className='mt-4'>
Close
</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
);
};

View File

@ -0,0 +1,333 @@
'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
import { useAuth, useTVMode } from '@/components/context';
import {
getUserStatuses,
updateUserStatus,
updateCurrentUserStatus,
type UserStatus,
} from '@/lib/hooks';
import {
Drawer,
DrawerTrigger,
Progress,
} from '@/components/ui';
import { toast } from 'sonner';
import { HistoryDrawer } from '@/components/status';
import { createClient } from '@/utils/supabase';
import type { RealtimeChannel } from '@supabase/supabase-js';
export const TechTable = () => {
const { isAuthenticated, profile } = useAuth();
const { tvMode } = useTVMode();
const [loading, setLoading] = useState(true);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [selectAll, setSelectAll] = useState(false);
const [statusInput, setStatusInput] = useState('');
const [technicianData, setTechnicianData] = useState<UserStatus[]>([]);
const [selectedUserId, setSelectedUserId] = useState('');
const [recentlyUpdatedIds, setRecentlyUpdatedIds] = useState<Set<string>>(new Set());
const channelRef = useRef<RealtimeChannel | null>(null);
const supabase = createClient();
const fetchTechnicians = useCallback(async () => {
try {
const response = await getUserStatuses();
if (!response.success) throw new Error(response.error);
return response.data;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
toast.error(`Error fetching technicians: ${errorMessage}`);
return [];
}
}, []);
// Setup realtime broadcast subscription
const setupRealtimeSubscription = useCallback(() => {
console.log('Setting up realtime broadcast subscription');
const channel = supabase.channel('status_updates');
channel
.on('broadcast', { event: 'status_updated' }, (payload) => {
console.log('Status update received:', payload);
const userStatus = payload.payload.user_status as UserStatus;
// Update the technician data
setTechnicianData(prevData => {
const newData = [...prevData];
const existingIndex = newData.findIndex(tech => tech.id === userStatus.id);
if (existingIndex !== -1) {
// Update existing user if this status is more recent
if (new Date(userStatus.created_at) > new Date(newData[existingIndex].created_at)) {
newData[existingIndex] = userStatus;
// Mark as recently updated
setRecentlyUpdatedIds(prev => {
const newSet = new Set(prev);
newSet.add(userStatus.id);
return newSet;
});
// Remove highlight after 3 seconds
setTimeout(() => {
setRecentlyUpdatedIds(current => {
const updatedSet = new Set(current);
updatedSet.delete(userStatus.id);
return updatedSet;
});
}, 3000);
}
} else {
// Add new user
newData.push(userStatus);
// Mark as recently updated
setRecentlyUpdatedIds(prev => {
const newSet = new Set(prev);
newSet.add(userStatus.id);
return newSet;
});
// Remove highlight after 3 seconds
setTimeout(() => {
setRecentlyUpdatedIds(current => {
const updatedSet = new Set(current);
updatedSet.delete(userStatus.id);
return updatedSet;
});
}, 3000);
}
// Sort by most recent
newData.sort((a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
return newData;
});
})
.subscribe((status) => {
console.log('Realtime subscription status:', status);
});
channelRef.current = channel;
return channel;
}, [supabase]);
const updateStatus = useCallback(async () => {
if (!isAuthenticated) {
toast.error('You must be signed in to update status.');
return;
}
if (!statusInput.trim()) {
toast.error('Please enter a status.');
return;
}
try {
if (selectedIds.length === 0) {
// Update current user - find them by profile match
let targetUserId = null;
if (profile?.full_name) {
const currentUserInTable = technicianData.find(tech =>
tech.full_name === profile.full_name || tech.id === profile.id
);
targetUserId = currentUserInTable?.id;
}
if (targetUserId) {
const result = await updateUserStatus([targetUserId], statusInput);
if (!result.success) throw new Error(result.error);
toast.success('Your status has been updated.');
} else {
const result = await updateCurrentUserStatus(statusInput);
if (!result.success) throw new Error(result.error);
toast.success('Your status has been updated.');
}
} else {
const result = await updateUserStatus(selectedIds, statusInput);
if (!result.success) throw new Error(result.error);
toast.success(`Status updated for ${selectedIds.length} technician${selectedIds.length > 1 ? 's' : ''}.`);
}
setSelectedIds([]);
setStatusInput('');
// No need to manually fetch - broadcast will handle updates
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
toast.error(`Failed to update status: ${errorMessage}`);
}
}, [isAuthenticated, statusInput, selectedIds, technicianData, profile]);
const handleCheckboxChange = (id: string) => {
setSelectedIds(prev =>
prev.includes(id) ? prev.filter(prevId => prevId !== id) : [...prev, id]
);
};
const handleSelectAllChange = () => {
if (selectAll) {
setSelectedIds([]);
} else {
setSelectedIds(technicianData.map(tech => tech.id));
}
setSelectAll(!selectAll);
};
const formatTime = (timestamp: string) => {
const date = new Date(timestamp);
const time = date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: 'numeric',
});
const day = date.getDate();
const month = date.toLocaleString('default', { month: 'long' });
return `${time} - ${month} ${day}`;
};
// Initial load and setup realtime subscription
useEffect(() => {
const loadData = async () => {
const data = await fetchTechnicians();
setTechnicianData(data);
setLoading(false);
};
void loadData();
// Setup realtime subscription
const channel = setupRealtimeSubscription();
// Cleanup function
return () => {
if (channel) {
console.log('Removing broadcast channel');
void supabase.removeChannel(channel);
channelRef.current = null;
}
};
}, [fetchTechnicians, setupRealtimeSubscription, supabase]);
// Update select all state
useEffect(() => {
setSelectAll(
selectedIds.length === technicianData.length &&
technicianData.length > 0
);
}, [selectedIds.length, technicianData.length]);
if (loading) {
return (
<div className='flex justify-center items-center min-h-[400px]'>
<Progress value={33} className='w-64' />
</div>
);
}
return (
<div className='w-full max-w-7xl mx-auto px-4'>
<table className={`w-full text-center border-collapse ${tvMode ? 'text-4xl lg:text-5xl' : 'text-base lg:text-lg'}`}>
<thead>
<tr className='bg-muted'>
{!tvMode && (
<th className='py-3 px-3 border'>
<input
type='checkbox'
className='scale-125 cursor-pointer'
checked={selectAll}
onChange={handleSelectAllChange}
/>
</th>
)}
<th className='py-3 px-4 border font-semibold'>Name</th>
<th className='py-3 px-4 border font-semibold'>
<Drawer>
<DrawerTrigger className='hover:underline'>
Status
</DrawerTrigger>
<HistoryDrawer user_id='' />
</Drawer>
</th>
<th className='py-3 px-4 border font-semibold'>Updated At</th>
</tr>
</thead>
<tbody>
{technicianData.map((technician, index) => (
<tr
key={technician.id}
className={`
${index % 2 === 0 ? 'bg-muted/50' : 'bg-background'}
hover:bg-muted/75 transition-all duration-300
${recentlyUpdatedIds.has(technician.id) ? 'ring-2 ring-blue-500 ring-opacity-75 bg-blue-50 dark:bg-blue-900/20' : ''}
`}
>
{!tvMode && (
<td className='py-2 px-3 border'>
<input
type='checkbox'
className='scale-125 cursor-pointer'
checked={selectedIds.includes(technician.id)}
onChange={() => handleCheckboxChange(technician.id)}
/>
</td>
)}
<td className='py-3 px-4 border font-medium'>
{technician.full_name ?? 'Unknown User'}
</td>
<td className='py-3 px-4 border'>
<Drawer>
<DrawerTrigger
className='text-left w-full p-2 rounded hover:bg-muted transition-colors'
onClick={() => setSelectedUserId(technician.id)}
>
{technician.status}
</DrawerTrigger>
{selectedUserId === technician.id && (
<HistoryDrawer
key={selectedUserId}
user_id={selectedUserId}
/>
)}
</Drawer>
</td>
<td className='py-3 px-4 border text-muted-foreground'>
{formatTime(technician.created_at)}
</td>
</tr>
))}
</tbody>
</table>
{!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:min-w-[400px] py-2 px-3 rounded-xl border bg-background lg:text-2xl focus:outline-none focus:ring-2 focus:ring-primary'
value={statusInput}
onChange={(e) => setStatusInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
void updateStatus();
}
}}
/>
<button
type='submit'
className='min-w-[100px] lg:min-w-[160px] py-2 px-4 rounded-xl text-center font-semibold lg:text-2xl bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed'
onClick={() => void updateStatus()}
disabled={!statusInput.trim()}
>
Update
</button>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,2 @@
export * from './HistoryDrawer';
export * from './TechTable';

View File

@ -0,0 +1,135 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn(
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
className
)}
{...props}
/>
)
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View File

@ -1,11 +1,16 @@
export * from '@/components/ui/avatar'; export * from './avatar';
export * from '@/components/ui/badge'; export * from './badge';
export * from '@/components/ui/button'; export * from './button';
export * from '@/components/ui/card'; export * from './card';
export * from '@/components/ui/checkbox'; export * from './checkbox';
export * from '@/components/ui/dropdown-menu'; export * from './drawer';
export * from '@/components/ui/form'; export * from './dropdown-menu';
export * from '@/components/ui/input'; export * from './form';
export * from '@/components/ui/label'; export * from './input';
export * from '@/components/ui/separator'; export * from './label';
export * from '@/components/ui/sonner'; export * from './pagination';
export * from './progress';
export * from './scroll-area';
export * from './separator';
export * from './sonner';
export * from './table';

View File

@ -0,0 +1,127 @@
import * as React from "react"
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
)
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />
}
type PaginationLinkProps = {
isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
)
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
)
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
)
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}

View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }

View File

@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

116
src/components/ui/table.tsx Normal file
View File

@ -0,0 +1,116 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

142
src/lib/actions/_status.ts Normal file
View File

@ -0,0 +1,142 @@
'use server';
import 'server-only';
import { createServerClient } from '@/utils/supabase';
import type { Result } from '.';
import type { Profile } from '@/utils/supabase';
export type UserStatus = Profile & {
status: string;
created_at: string;
updated_by?: Profile;
}
export const getRecentUsers = async (): Promise<Result<string[]>> => {
try {
const supabase = await createServerClient();
// Get users who have had status updates in the last week
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
const { data, error } = await supabase
.from('statuses')
.select('user_id')
.gte('created_at', oneWeekAgo.toISOString())
.order('created_at', { ascending: false });
if (error) throw error;
// Get unique user IDs
const uniqueUserIds = [...new Set(data.map(status => status.user_id))];
return { success: true, data: uniqueUserIds };
} catch (error) {
return {
success: false,
error:
error instanceof Error
? error.message
: 'Unknown error getting recent users',
};
}
};
export const getUserStatuses = async (): Promise<Result<UserStatus[]>> => {
try {
const supabase = await createServerClient();
// First get the recent users
const recentUsersResult = await getRecentUsers();
if (!recentUsersResult.success) {
throw new Error(recentUsersResult.error);
}
const userIds = recentUsersResult.data;
if (userIds.length === 0) {
return { success: true, data: [] };
}
// Get the most recent status for each user
const { data: statusData, error: statusError } = await supabase
.from('statuses')
.select('user_id, status, created_at, updated_by_id')
.in('user_id', userIds)
.order('created_at', { ascending: false });
if (statusError) throw statusError;
if (!statusData) {
return { success: true, data: [] };
}
// Group by user_id and get the most recent status for each user
const userStatusMap = new Map<string, typeof statusData[0]>();
statusData.forEach(status => {
if (!userStatusMap.has(status.user_id)) {
userStatusMap.set(status.user_id, status);
}
});
// Get all unique user IDs from the status data
const statusUserIds = Array.from(userStatusMap.keys());
// Get profile information for these users
const { data: profileData, error: profileError } = await supabase
.from('profiles')
.select('id, full_name, email, avatar_url, provider, updated_at')
.in('id', statusUserIds);
if (profileError) throw profileError;
// Get updated_by profile information
const updatedByIds = Array.from(userStatusMap.values())
.map(status => status.updated_by_id)
.filter((id): id is string => id !== null);
const { data: updatedByData, error: updatedByError } = await supabase
.from('profiles')
.select('id, full_name, email, avatar_url, provider, updated_at')
.in('id', updatedByIds);
if (updatedByError) throw updatedByError;
// Create maps for easy lookup
const profileMap = new Map(profileData?.map(profile => [profile.id, profile]) ?? []);
const updatedByMap = new Map(updatedByData?.map(profile => [profile.id, profile]) ?? []);
// Transform the data to match UserStatus type
const userStatuses: UserStatus[] = [];
for (const status of userStatusMap.values()) {
const profile = profileMap.get(status.user_id);
const updatedBy = status.updated_by_id ? updatedByMap.get(status.updated_by_id) : undefined;
if (!profile) continue; // Skip if no profile found
userStatuses.push({
// Profile fields
id: profile.id,
full_name: profile.full_name,
email: profile.email,
avatar_url: profile.avatar_url,
provider: profile.provider,
updated_at: profile.updated_at,
// Status fields
status: status.status,
created_at: status.created_at,
updated_by: updatedBy,
});
}
return { success: true, data: userStatuses };
} catch (error) {
return {
success: false,
error:
error instanceof Error
? error.message
: 'Unknown error getting user statuses',
};
}
};

View File

@ -1,6 +1,7 @@
export * from './auth'; export * from './auth';
export * from './storage';
export * from './public'; export * from './public';
export * from './status';
export * from './storage';
export type Result<T> = export type Result<T> =
| { success: true; data: T } | { success: true; data: T }

53
src/lib/actions/status.ts Normal file
View File

@ -0,0 +1,53 @@
'use server'
import 'server-only';
import {
createServerClient,
type Profile,
type Result,
type Status,
} from '@/utils/supabase';
type UserWithStatus = {
user: Profile;
status: string;
created_at: string;
updated_by: Profile;
};
type PaginatedHistory = UserWithStatus[] & {
meta: {
current_page: number;
per_page: number;
total_pages: number;
total_count: number;
};
};
export const getUsersWithStatuses = async () => {
try {
const supabase = await createServerClient();
// Get only users with recent statuses (Past 7 days)
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
const { data: recentStatuses, error } = await supabase
.from('statuses')
.select(`
user:profiles!user_id(*),
status,
created_at,
updated_by:profiles!updated_by_id(*)
`)
.gte('created_at', oneWeekAgo.toISOString())
.order('created_at', { ascending: false });
if (error) throw error;
if (!recentStatuses.length) return { success: true, data: []};
return { success: true, data: recentStatuses };
} catch (error) {
return { success: false, error: `Error: ${error as string}` };
}
};

401
src/lib/hooks/_status.ts Normal file
View File

@ -0,0 +1,401 @@
'use client';
import { createClient } from '@/utils/supabase';
import type { Result } from '.';
import type { Profile, Status } from '@/utils/supabase';
export type UserStatus = Profile & {
status: string;
created_at: string;
updated_by?: Profile;
};
export type HistoryEntry = {
id: string;
status: string;
created_at: string;
updated_by?: Profile;
user_profile: Profile;
};
export type PaginatedHistory = {
data: HistoryEntry[];
meta: {
current_page: number;
per_page: number;
total_pages: number;
total_count: number;
};
};
export const getUserStatuses = async (): Promise<Result<UserStatus[]>> => {
try {
const supabase = createClient();
// Get users with recent activity (last 7 days)
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
const { data: recentStatuses, error: statusError } = await supabase
.from('statuses')
.select('*')
.gte('created_at', oneWeekAgo.toISOString())
.order('created_at', { ascending: false });
if (statusError) throw statusError;
if (!recentStatuses?.length) return { success: true, data: [] };
// Properly type the status data
const typedStatuses: Status[] = recentStatuses;
// Get most recent status per user
const userStatusMap = new Map<string, Status>();
typedStatuses.forEach(status => {
if (!userStatusMap.has(status.user_id)) {
userStatusMap.set(status.user_id, status);
}
});
const userIds = Array.from(userStatusMap.keys());
// Get profiles
const { data: profiles, error: profileError } = await supabase
.from('profiles')
.select('*')
.in('id', userIds);
if (profileError) throw profileError;
// Get updated_by profiles - filter out nulls properly
const updatedByIds = Array.from(userStatusMap.values())
.map(s => s.updated_by_id)
.filter((id): id is string => id !== null);
let updatedByProfiles: Profile[] = [];
if (updatedByIds.length > 0) {
const { data, error } = await supabase
.from('profiles')
.select('*')
.in('id', updatedByIds);
if (error) throw error;
updatedByProfiles = data ?? [];
}
const profileMap = new Map((profiles ?? []).map(p => [p.id, p]));
const updatedByMap = new Map(updatedByProfiles.map(p => [p.id, p]));
const userStatuses: UserStatus[] = [];
for (const [userId, status] of userStatusMap) {
const profile = profileMap.get(userId);
if (!profile) continue;
userStatuses.push({
...profile,
status: status.status,
created_at: status.created_at,
updated_by: status.updated_by_id ? updatedByMap.get(status.updated_by_id) : undefined,
});
}
return { success: true, data: userStatuses };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};
export const broadcastStatusUpdate = async (
userStatus: UserStatus
): Promise<Result<void>> => {
try {
const supabase = createClient();
const broadcast = await supabase.channel('status_updates').send({
type: 'broadcast',
event: 'status_updated',
payload: {
user_status: userStatus,
timestamp: new Date().toISOString(),
}
});
if (broadcast === 'error') throw new Error('Failed to broadcast status update');
if (broadcast === 'ok') return { success: true, data: undefined };
else throw new Error('Broadcast timed out!')
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};
// Update your existing functions to broadcast after database updates
export const updateUserStatus = async (
userIds: string[],
status: string,
): Promise<Result<void>> => {
try {
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Not authenticated');
const inserts = userIds.map(userId => ({
user_id: userId,
status,
updated_by_id: user.id,
}));
const { data: insertedStatuses, error } = await supabase
.from('statuses')
.insert(inserts)
.select();
if (error) throw error;
// Broadcast the updates
if (insertedStatuses) {
for (const insertedStatus of insertedStatuses) {
// Get the user profile for broadcasting
const { data: profile } = await supabase
.from('profiles')
.select('*')
.eq('id', insertedStatus.user_id)
.single();
if (profile) {
const userStatus: UserStatus = {
...profile,
status: insertedStatus.status,
created_at: insertedStatus.created_at,
};
await broadcastStatusUpdate(userStatus);
}
}
}
return { success: true, data: undefined };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};
export const updateCurrentUserStatus = async (
status: string,
): Promise<Result<void>> => {
try {
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Not authenticated');
const { data: insertedStatus, error } = await supabase
.from('statuses')
.insert({
user_id: user.id,
status,
updated_by_id: user.id,
})
.select()
.single();
if (error) throw error;
// Get the user profile for broadcasting
const { data: profile } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.single();
if (profile && insertedStatus) {
const userStatus: UserStatus = {
...profile,
status: insertedStatus.status,
created_at: insertedStatus.created_at,
};
await broadcastStatusUpdate(userStatus);
}
return { success: true, data: undefined };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};
export const getUserHistory = async (
userId: string,
page = 1,
perPage = 50,
): Promise<Result<PaginatedHistory>> => {
try {
const supabase = createClient();
const offset = (page - 1) * perPage;
// Get count
const { count } = await supabase
.from('statuses')
.select('*', { count: 'exact', head: true })
.eq('user_id', userId);
// Get data
const { data: statuses, error } = await supabase
.from('statuses')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false })
.range(offset, offset + perPage - 1);
if (error) throw error;
const typedStatuses: Status[] = statuses ?? [];
// Get user profile
const { data: userProfile, error: userProfileError } = await supabase
.from('profiles')
.select('*')
.eq('id', userId)
.single();
if (userProfileError) throw userProfileError;
if (!userProfile) throw new Error('User profile not found');
// Get updated_by profiles - filter out nulls properly
const updatedByIds = typedStatuses
.map(s => s.updated_by_id)
.filter((id): id is string => id !== null);
let updatedByProfiles: Profile[] = [];
if (updatedByIds.length > 0) {
const { data, error: updatedByError } = await supabase
.from('profiles')
.select('*')
.in('id', updatedByIds);
if (updatedByError) throw updatedByError;
updatedByProfiles = data ?? [];
}
const updatedByMap = new Map(updatedByProfiles.map(p => [p.id, p]));
const historyEntries: HistoryEntry[] = typedStatuses.map(entry => ({
id: entry.id,
status: entry.status,
created_at: entry.created_at,
updated_by: entry.updated_by_id ? updatedByMap.get(entry.updated_by_id) : undefined,
user_profile: userProfile,
}));
const totalCount = count ?? 0;
const totalPages = Math.ceil(totalCount / perPage);
return {
success: true,
data: {
data: historyEntries,
meta: {
current_page: page,
per_page: perPage,
total_pages: totalPages,
total_count: totalCount
},
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};
export const getAllHistory = async (
page = 1,
perPage = 50,
): Promise<Result<PaginatedHistory>> => {
try {
const supabase = createClient();
const offset = (page - 1) * perPage;
// Get count
const { count } = await supabase
.from('statuses')
.select('*', { count: 'exact', head: true });
// Get data
const { data: statuses, error } = await supabase
.from('statuses')
.select('*')
.order('created_at', { ascending: false })
.range(offset, offset + perPage - 1);
if (error) throw error;
const typedStatuses: Status[] = statuses ?? [];
// Get all profiles - filter out nulls properly
const userIds = [...new Set(typedStatuses.map(s => s.user_id))];
const updatedByIds = typedStatuses
.map(s => s.updated_by_id)
.filter((id): id is string => id !== null);
const allIds = [...new Set([...userIds, ...updatedByIds])];
const { data: profiles, error: profileError } = await supabase
.from('profiles')
.select('*')
.in('id', allIds);
if (profileError) throw profileError;
const profileMap = new Map((profiles ?? []).map(p => [p.id, p]));
const historyEntries: HistoryEntry[] = typedStatuses.map(entry => {
const userProfile = profileMap.get(entry.user_id);
if (!userProfile) {
throw new Error(`User profile not found for ID: ${entry.user_id}`);
}
return {
id: entry.id,
status: entry.status,
created_at: entry.created_at,
updated_by: entry.updated_by_id ? profileMap.get(entry.updated_by_id) : undefined,
user_profile: userProfile,
};
});
const totalCount = count ?? 0;
const totalPages = Math.ceil(totalCount / perPage);
return {
success: true,
data: {
data: historyEntries,
meta: {
current_page: page,
per_page: perPage,
total_pages: totalPages,
total_count: totalCount
},
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};

View File

@ -1,5 +1,6 @@
export * from './auth'; export * from './auth';
export * from './public'; export * from './public';
export * from './status';
export * from './storage'; export * from './storage';
export * from './useFileUpload'; export * from './useFileUpload';

52
src/lib/hooks/status.ts Normal file
View File

@ -0,0 +1,52 @@
'use client';
import {
createClient,
type Profile,
type Result,
type Status,
} from '@/utils/supabase';
type UserWithStatus = {
user: Profile;
status: string;
created_at: string;
updated_by: Profile;
};
type PaginatedHistory = UserWithStatus[] & {
meta: {
current_page: number;
per_page: number;
total_pages: number;
total_count: number;
};
};
export const getUsersWithStatuses = async (): Promise<Result<UserWithStatus[]>> => {
try {
const supabase = createClient();
// Get only users with recent statuses (Past 7 days)
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
const { data: recentStatuses, error } = await supabase
.from('statuses')
.select(`
user:profiles!user_id(*),
status,
created_at,
updated_by:profiles!updated_by_id(*)
`)
.gte('created_at', oneWeekAgo.toISOString())
.order('created_at', { ascending: false }) as {data: UserWithStatus[], error: unknown};
if (error) throw error;
if (!recentStatuses.length) return { success: true, data: []};
return { success: true, data: recentStatuses };
} catch (error) {
return { success: false, error: `Error: ${error as string}` };
}
};

View File

@ -64,62 +64,63 @@ create policy "Anyone can update an avatar." on storage.objects
create policy "Anyone can delete an avatar." on storage.objects create policy "Anyone can delete an avatar." on storage.objects
for delete using (bucket_id = 'avatars'); for delete using (bucket_id = 'avatars');
-- -- Create a table for public statuses -- Create a table for public statuses
-- CREATE TABLE statuses ( CREATE TABLE statuses (
-- id uuid DEFAULT gen_random_uuid() PRIMARY KEY, id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
-- user_id uuid REFERENCES auth.users ON DELETE CASCADE NOT NULL, user_id uuid REFERENCES public.profiles ON DELETE CASCADE NOT NULL,
-- updated_by_id uuid REFERENCES auth.users ON DELETE SET NULL DEFAULT auth.uid(), updated_by_id uuid REFERENCES public.profiles ON DELETE SET NULL DEFAULT auth.uid(),
-- created_at timestamp with time zone DEFAULT now() NOT NULL, created_at timestamp with time zone DEFAULT now() NOT NULL,
-- status text NOT NULL, status text NOT NULL,
-- CONSTRAINT status_length CHECK (char_length(status) >= 3 AND char_length(status) <= 80) CONSTRAINT status_length CHECK (char_length(status) >= 3 AND char_length(status) <= 80)
-- ); );
-- -- Set up Row Level Security (RLS) -- Set up Row Level Security (RLS)
-- ALTER TABLE statuses ALTER TABLE statuses
-- ENABLE ROW LEVEL SECURITY; ENABLE ROW LEVEL SECURITY;
-- -- Policies -- Policies
-- CREATE POLICY "Public statuses are viewable by everyone." ON statuses CREATE POLICY "Public statuses are viewable by everyone." ON statuses
-- FOR SELECT USING (true); FOR SELECT USING (true);
-- -- RECREATE it using the recommended sub-select form -- RECREATE it using the recommended sub-select form
-- CREATE POLICY "Authenticated users can insert statuses for any user." CREATE POLICY "Authenticated users can insert statuses for any user."
-- ON public.statuses ON public.statuses
-- FOR INSERT FOR INSERT
-- WITH CHECK ( WITH CHECK (
-- (SELECT auth.role()) = 'authenticated' (SELECT auth.role()) = 'authenticated'
-- ); );
-- -- ADD an UPDATE policy so anyone signed-in can update *any* status -- ADD an UPDATE policy so anyone signed-in can update *any* status
-- CREATE POLICY "Authenticated users can update statuses for any user." CREATE POLICY "Authenticated users can update statuses for any user."
-- ON public.statuses ON public.statuses
-- FOR UPDATE FOR UPDATE
-- USING ( USING (
-- (SELECT auth.role()) = 'authenticated' (SELECT auth.role()) = 'authenticated'
-- ) )
-- WITH CHECK ( WITH CHECK (
-- (SELECT auth.role()) = 'authenticated' (SELECT auth.role()) = 'authenticated'
-- ); );
-- -- Function to add first status -- Function to add first status
-- CREATE FUNCTION public.handle_first_status() CREATE FUNCTION public.handle_first_status()
-- RETURNS TRIGGER RETURNS TRIGGER
-- SET search_path = '' SET search_path = ''
-- AS $$ AS $$
-- BEGIN BEGIN
-- INSERT INTO public.statuses (user_id, updated_by_id, status) INSERT INTO public.statuses (user_id, updated_by_id, status)
-- VALUES ( VALUES (
-- NEW.id, NEW.id,
-- NEW.id, NEW.id,
-- 'Just joined!' 'Just joined!'
-- ); );
-- RETURN NEW; RETURN NEW;
-- END; END;
-- $$ LANGUAGE plpgsql SECURITY DEFINER; $$ LANGUAGE plpgsql SECURITY DEFINER;
-- -- Create a separate trigger for the status -- Create a separate trigger for the status
-- CREATE TRIGGER on_auth_user_created_add_status CREATE TRIGGER on_auth_user_created_add_status
-- AFTER INSERT ON auth.users AFTER INSERT ON auth.users
-- FOR EACH ROW EXECUTE PROCEDURE public.handle_first_status(); FOR EACH ROW EXECUTE PROCEDURE public.handle_first_status();
-- alter publication supabase_realtime add table statuses; alter publication supabase_realtime add table profiles;
alter publication supabase_realtime add table statuses;

View File

@ -4,185 +4,200 @@ export type Json =
| boolean | boolean
| null | null
| { [key: string]: Json | undefined } | { [key: string]: Json | undefined }
| Json[]; | Json[]
export type Database = { export type Database = {
public: { public: {
Tables: { Tables: {
profiles: { profiles: {
Row: { Row: {
avatar_url: string | null; avatar_url: string | null
email: string | null; email: string | null
full_name: string | null; full_name: string | null
id: string; id: string
provider: string | null; provider: string | null
updated_at: string | null; updated_at: string | null
}; }
Insert: { Insert: {
avatar_url?: string | null; avatar_url?: string | null
email?: string | null; email?: string | null
full_name?: string | null; full_name?: string | null
id: string; id: string
provider?: string | null; provider?: string | null
updated_at?: string | null; updated_at?: string | null
}; }
Update: { Update: {
avatar_url?: string | null; avatar_url?: string | null
email?: string | null; email?: string | null
full_name?: string | null; full_name?: string | null
id?: string; id?: string
provider?: string | null; provider?: string | null
updated_at?: string | null; updated_at?: string | null
}; }
Relationships: []; Relationships: []
}; }
statuses: { statuses: {
Row: { Row: {
created_at: string; created_at: string
id: string; id: string
status: string; status: string
updated_by_id: string | null; updated_by_id: string | null
user_id: string; user_id: string
}; }
Insert: { Insert: {
created_at?: string; created_at?: string
id?: string; id?: string
status: string; status: string
updated_by_id?: string | null; updated_by_id?: string | null
user_id: string; user_id: string
}; }
Update: { Update: {
created_at?: string; created_at?: string
id?: string; id?: string
status?: string; status?: string
updated_by_id?: string | null; updated_by_id?: string | null
user_id?: string; user_id?: string
}; }
Relationships: []; Relationships: [
}; {
}; foreignKeyName: "statuses_updated_by_id_fkey"
columns: ["updated_by_id"]
isOneToOne: false
referencedRelation: "profiles"
referencedColumns: ["id"]
},
{
foreignKeyName: "statuses_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "profiles"
referencedColumns: ["id"]
},
]
}
}
Views: { Views: {
[_ in never]: never; [_ in never]: never
}; }
Functions: { Functions: {
[_ in never]: never; [_ in never]: never
}; }
Enums: { Enums: {
[_ in never]: never; [_ in never]: never
}; }
CompositeTypes: { CompositeTypes: {
[_ in never]: never; [_ in never]: never
}; }
}; }
}; }
type DefaultSchema = Database[Extract<keyof Database, 'public'>]; type DefaultSchema = Database[Extract<keyof Database, "public">]
export type Tables< export type Tables<
DefaultSchemaTableNameOrOptions extends DefaultSchemaTableNameOrOptions extends
| keyof (DefaultSchema['Tables'] & DefaultSchema['Views']) | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
| { schema: keyof Database }, | { schema: keyof Database },
TableName extends DefaultSchemaTableNameOrOptions extends { TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof Database; schema: keyof Database
} }
? keyof (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] & ? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
Database[DefaultSchemaTableNameOrOptions['schema']]['Views']) Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
: never = never, : never = never,
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } > = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
? (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] & ? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends { Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
Row: infer R; Row: infer R
} }
? R ? R
: never : never
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema['Tables'] & : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
DefaultSchema['Views']) DefaultSchema["Views"])
? (DefaultSchema['Tables'] & ? (DefaultSchema["Tables"] &
DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends { DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
Row: infer R; Row: infer R
} }
? R ? R
: never : never
: never; : never
export type TablesInsert< export type TablesInsert<
DefaultSchemaTableNameOrOptions extends DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema['Tables'] | keyof DefaultSchema["Tables"]
| { schema: keyof Database }, | { schema: keyof Database },
TableName extends DefaultSchemaTableNameOrOptions extends { TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof Database; schema: keyof Database
} }
? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] ? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
: never = never, : never = never,
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } > = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { ? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Insert: infer I; Insert: infer I
} }
? I ? I
: never : never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables'] : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends { ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
Insert: infer I; Insert: infer I
} }
? I ? I
: never : never
: never; : never
export type TablesUpdate< export type TablesUpdate<
DefaultSchemaTableNameOrOptions extends DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema['Tables'] | keyof DefaultSchema["Tables"]
| { schema: keyof Database }, | { schema: keyof Database },
TableName extends DefaultSchemaTableNameOrOptions extends { TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof Database; schema: keyof Database
} }
? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] ? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
: never = never, : never = never,
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database } > = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends { ? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Update: infer U; Update: infer U
} }
? U ? U
: never : never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables'] : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends { ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
Update: infer U; Update: infer U
} }
? U ? U
: never : never
: never; : never
export type Enums< export type Enums<
DefaultSchemaEnumNameOrOptions extends DefaultSchemaEnumNameOrOptions extends
| keyof DefaultSchema['Enums'] | keyof DefaultSchema["Enums"]
| { schema: keyof Database }, | { schema: keyof Database },
EnumName extends DefaultSchemaEnumNameOrOptions extends { EnumName extends DefaultSchemaEnumNameOrOptions extends {
schema: keyof Database; schema: keyof Database
} }
? keyof Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'] ? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
: never = never, : never = never,
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database } > = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
? Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName] ? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums'] : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions] ? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
: never; : never
export type CompositeTypes< export type CompositeTypes<
PublicCompositeTypeNameOrOptions extends PublicCompositeTypeNameOrOptions extends
| keyof DefaultSchema['CompositeTypes'] | keyof DefaultSchema["CompositeTypes"]
| { schema: keyof Database }, | { schema: keyof Database },
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
schema: keyof Database; schema: keyof Database
} }
? keyof Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'] ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
: never = never, : never = never,
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } > = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
? Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName] ? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes'] : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions] ? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
: never; : never
export const Constants = { export const Constants = {
public: { public: {
Enums: {}, Enums: {},
}, },
} as const; } as const

View File

@ -1,6 +1,11 @@
import type { Database } from '@/utils/supabase/types'; import type { Database } from '@/utils/supabase/types';
export type { User } from '@supabase/supabase-js'; export type { User } from '@supabase/supabase-js';
// Result type for API calls
export type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
// Table row types // Table row types
export type Profile = Database['public']['Tables']['profiles']['Row']; export type Profile = Database['public']['Tables']['profiles']['Row'];
export type Status = Database['public']['Tables']['statuses']['Row']; export type Status = Database['public']['Tables']['statuses']['Row'];