Compare commits
3 Commits
d0533b78e8
...
bb0dd14ec9
Author | SHA1 | Date | |
---|---|---|---|
bb0dd14ec9 | |||
3001f90a48 | |||
219549d35a |
@@ -1,16 +1,16 @@
|
|||||||
services:
|
services:
|
||||||
t3-template:
|
techtracker-next:
|
||||||
build:
|
build:
|
||||||
context: ../../../
|
context: ../../../
|
||||||
dockerfile: docker/development/Dockerfile
|
dockerfile: docker/development/Dockerfile
|
||||||
image: with-docker-multi-env-development
|
image: with-docker-multi-env-development
|
||||||
container_name: t3-template
|
container_name: techtracker-next
|
||||||
networks:
|
networks:
|
||||||
- nginx-bridge
|
- techtracker
|
||||||
#ports:
|
ports:
|
||||||
#- '3000:3000'
|
- '3111:3000'
|
||||||
tty: true
|
tty: true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
nginx-bridge:
|
techtracker:
|
||||||
external: true
|
external: true
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
services:
|
services:
|
||||||
t3-template:
|
techtracker-next:
|
||||||
build:
|
build:
|
||||||
context: ../../../
|
context: ../../../
|
||||||
dockerfile: docker/production/Dockerfile
|
dockerfile: docker/production/Dockerfile
|
||||||
image: with-docker-multi-env-development
|
image: with-docker-multi-env-development
|
||||||
container_name: t3-template
|
container_name: techtracker-next
|
||||||
networks:
|
networks:
|
||||||
- nginx-bridge
|
- techtracker
|
||||||
#ports:
|
ports:
|
||||||
#- '3000:3000'
|
- '3111:3000'
|
||||||
tty: true
|
tty: true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
nginx-bridge:
|
techtracker:
|
||||||
external: true
|
external: true
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import type React from 'react';
|
||||||
import { useAuth, useTVMode } from '@/components/context';
|
import { useAuth, useTVMode } from '@/components/context';
|
||||||
import type { UserWithStatus } from '@/lib/hooks';
|
import type { UserWithStatus } from '@/lib/hooks';
|
||||||
import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui';
|
import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui';
|
||||||
@@ -7,11 +8,10 @@ import { StatusMessage, SubmitButton } from '@/components/default';
|
|||||||
import { ConnectionStatus, HistoryDrawer } from '@/components/status';
|
import { ConnectionStatus, HistoryDrawer } from '@/components/status';
|
||||||
import type { Profile } from '@/utils/supabase';
|
import type { Profile } from '@/utils/supabase';
|
||||||
import { makeConditionalClassName } from '@/lib/utils';
|
import { makeConditionalClassName } from '@/lib/utils';
|
||||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { RefreshCw, Clock, Calendar } from 'lucide-react';
|
import { RefreshCw, Clock, Calendar, CheckCircle2 } from 'lucide-react';
|
||||||
import { useStatusData, useStatusSubscription } from '@/lib/hooks';
|
import { useStatusData, useStatusSubscription } from '@/lib/hooks';
|
||||||
import { formatTime, formatDate } from '@/lib/utils';
|
import { formatTime, formatDate } from '@/lib/utils';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -43,7 +43,6 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
enabled: isAuthenticated,
|
enabled: isAuthenticated,
|
||||||
});
|
});
|
||||||
|
|
||||||
// In your StatusList component
|
|
||||||
const { connectionStatus, connect: reconnect } = useStatusSubscription(() => {
|
const { connectionStatus, connect: reconnect } = useStatusSubscription(() => {
|
||||||
refetch().catch((error) => {
|
refetch().catch((error) => {
|
||||||
console.error('Error refetching statuses:', error);
|
console.error('Error refetching statuses:', error);
|
||||||
@@ -75,7 +74,12 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
setUpdateStatusMessage('');
|
setUpdateStatusMessage('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCheckboxChange = (user: UserWithStatus) => {
|
const handleCardSelect = (user: UserWithStatus, e: React.MouseEvent) => {
|
||||||
|
// Prevent selection if clicking on profile elements
|
||||||
|
if ((e.target as HTMLElement).closest('[data-profile-trigger]')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSelectedUsers((prev) =>
|
setSelectedUsers((prev) =>
|
||||||
prev.some((u) => u.user.id === user.user.id)
|
prev.some((u) => u.user.id === user.user.id)
|
||||||
? prev.filter((prevUser) => prevUser.user.id !== user.user.id)
|
? prev.filter((prevUser) => prevUser.user.id !== user.user.id)
|
||||||
@@ -121,54 +125,46 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
|
|
||||||
const containerClassName = makeConditionalClassName({
|
const containerClassName = makeConditionalClassName({
|
||||||
context: tvMode,
|
context: tvMode,
|
||||||
defaultClassName: 'flex flex-col mx-auto space-y-4 items-center',
|
defaultClassName:
|
||||||
on: 'lg:w-11/12 w-full mt-15',
|
'flex flex-col mx-auto items-center\
|
||||||
off: 'sm:w-5/6 md:3/4 lg:w-1/2',
|
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',
|
||||||
});
|
});
|
||||||
|
|
||||||
const headerClassName = makeConditionalClassName({
|
const headerClassName = makeConditionalClassName({
|
||||||
context: tvMode,
|
context: tvMode,
|
||||||
defaultClassName: 'w-full',
|
defaultClassName: 'w-full',
|
||||||
on: 'hidden',
|
on: 'hidden',
|
||||||
off: 'flex mb-4 justify-between',
|
off: 'flex mb-3 justify-between items-center',
|
||||||
});
|
});
|
||||||
|
|
||||||
const cardContainerClassName = makeConditionalClassName({
|
const cardContainerClassName = makeConditionalClassName({
|
||||||
context: tvMode,
|
context: tvMode,
|
||||||
defaultClassName: '',
|
defaultClassName: 'w-full space-y-2',
|
||||||
on: '',
|
|
||||||
off: 'space-y-3 items-center justify-center w-full',
|
|
||||||
});
|
|
||||||
|
|
||||||
const cardClassName = makeConditionalClassName({
|
|
||||||
context: tvMode,
|
|
||||||
defaultClassName:
|
|
||||||
'transition-all duration-300 hover:shadow-md hover:bg-muted/50 cursor-pointer',
|
|
||||||
on: 'lg:text-4xl',
|
|
||||||
off: 'lg:text-base lg:w-full',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClassName}>
|
<div className={containerClassName}>
|
||||||
<div className={headerClassName}>
|
<div className={headerClassName}>
|
||||||
<div className='flex items-center gap-10'>
|
<div className='flex items-center gap-4'>
|
||||||
<div className='flex gap-2'>
|
<Button
|
||||||
<Checkbox
|
variant='outline'
|
||||||
id='select-all'
|
size='sm'
|
||||||
checked={selectAll}
|
onClick={handleSelectAllChange}
|
||||||
onCheckedChange={handleSelectAllChange}
|
className='flex items-center gap-2'
|
||||||
className='size-6'
|
>
|
||||||
|
<CheckCircle2
|
||||||
|
className={`w-4 h-4 ${selectAll ? 'text-primary' : ''}`}
|
||||||
/>
|
/>
|
||||||
<label htmlFor='select-all' className='font-medium'>
|
{selectAll ? 'Deselect All' : 'Select All'}
|
||||||
Select All
|
</Button>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{!tvMode && (
|
{!tvMode && (
|
||||||
<div className='flex flex-row gap-2'>
|
<div className='flex items-center gap-2 text-xs'>
|
||||||
<p>Miss the old table?</p>
|
<span className='text-muted-foreground'>Miss the old table?</span>
|
||||||
<Link
|
<Link
|
||||||
href='/status/table'
|
href='/status/table'
|
||||||
className='italic font-semibold text-accent-foreground'
|
className='font-medium hover:underline'
|
||||||
>
|
>
|
||||||
Find it here!
|
Find it here!
|
||||||
</Link>
|
</Link>
|
||||||
@@ -198,88 +194,119 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
<Card
|
<Card
|
||||||
key={userWithStatus.user.id}
|
key={userWithStatus.user.id}
|
||||||
className={`
|
className={`
|
||||||
${cardClassName}
|
relative transition-all duration-200 cursor-pointer hover:shadow-md
|
||||||
${isSelected ? 'ring-2 ring-primary' : ''}
|
${tvMode ? 'p-4' : 'p-3'}
|
||||||
${isNewStatus ? 'animate-pulse bg-primary/5 border-primary/20' : ''}
|
${isSelected ? 'ring-2 ring-primary bg-primary/5 shadow-md' : 'hover:bg-muted/30'}
|
||||||
|
${isNewStatus ? 'animate-in slide-in-from-top-2 duration-500 bg-green-50 border-green-200' : ''}
|
||||||
`}
|
`}
|
||||||
|
onClick={(e) => handleCardSelect(userWithStatus, e)}
|
||||||
>
|
>
|
||||||
<CardHeader className='pb-3'>
|
{isSelected && (
|
||||||
<div className='flex items-center justify-between'>
|
<div className='absolute top-2 right-2 text-primary'>
|
||||||
<div className='flex items-center gap-3'>
|
<CheckCircle2
|
||||||
{!tvMode && (
|
className={`${tvMode ? 'w-6 h-6' : 'w-5 h-5'}`}
|
||||||
<Checkbox
|
/>
|
||||||
checked={isSelected}
|
</div>
|
||||||
onCheckedChange={() =>
|
)}
|
||||||
handleCheckboxChange(userWithStatus)
|
|
||||||
|
<CardContent className='p-0'>
|
||||||
|
<div className='flex items-start gap-3'>
|
||||||
|
{/* Profile Section - Clickable for history */}
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger asChild>
|
||||||
|
<div
|
||||||
|
data-profile-trigger
|
||||||
|
className='flex-shrink-0 cursor-pointer hover:opacity-80 transition-opacity'
|
||||||
|
onClick={() =>
|
||||||
|
setSelectedHistoryUser(userWithStatus.user)
|
||||||
}
|
}
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<BasedAvatar
|
|
||||||
src={userWithStatus.user.avatar_url}
|
|
||||||
fullName={userWithStatus.user.full_name}
|
|
||||||
className={tvMode ? 'w-24 h-24' : 'w-16 h-16'}
|
|
||||||
/>
|
|
||||||
<div className='my-auto'>
|
|
||||||
<h3
|
|
||||||
className={`font-semibold ${tvMode ? 'text-5xl' : 'text-2xl'}`}
|
|
||||||
>
|
>
|
||||||
{userWithStatus.user.full_name}
|
<BasedAvatar
|
||||||
</h3>
|
src={userWithStatus.user.avatar_url}
|
||||||
{isUpdatedByOther && (
|
fullName={userWithStatus.user.full_name}
|
||||||
<div className='flex items-center gap-1 text-muted-foreground'>
|
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
|
||||||
<BasedAvatar
|
/>
|
||||||
src={userWithStatus.updated_by?.avatar_url}
|
</div>
|
||||||
fullName={userWithStatus.updated_by?.full_name}
|
</DrawerTrigger>
|
||||||
className='w-5 h-5'
|
{selectedHistoryUser === userWithStatus.user && (
|
||||||
|
<HistoryDrawer user={selectedHistoryUser} />
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
{/* Content Section */}
|
||||||
|
<div className='flex-1'>
|
||||||
|
{/* Header with name and timestamp */}
|
||||||
|
<div className='flex items-start justify-between mb-2'>
|
||||||
|
<div>
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger asChild>
|
||||||
|
<h3
|
||||||
|
data-profile-trigger
|
||||||
|
className={`
|
||||||
|
font-semibold cursor-pointer hover:text-primary/80 truncate
|
||||||
|
${tvMode ? 'text-3xl' : 'text-2xl'}
|
||||||
|
`}
|
||||||
|
onClick={() =>
|
||||||
|
setSelectedHistoryUser(userWithStatus.user)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{userWithStatus.user.full_name}
|
||||||
|
</h3>
|
||||||
|
</DrawerTrigger>
|
||||||
|
{selectedHistoryUser === userWithStatus.user && (
|
||||||
|
<HistoryDrawer user={selectedHistoryUser} />
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
<div
|
||||||
|
className={`pl-2 pr-15 pt-2 ${tvMode ? 'text-2xl' : 'text-xl'}`}
|
||||||
|
>
|
||||||
|
<p>{userWithStatus.status}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col items-end px-2 gap-2 text-muted-foreground flex-shrink-0'>
|
||||||
|
<div className='flex items-center gap-2 flex-shrink-0 w-full'>
|
||||||
|
<Clock
|
||||||
|
className={`${tvMode ? 'w-6 h-6' : 'w-5 h-5'}`}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`${tvMode ? 'text-3xl' : 'text-sm'}`}
|
className={`${tvMode ? 'text-2xl' : 'text-xl'}`}
|
||||||
>
|
>
|
||||||
Updated by {userWithStatus.updated_by?.full_name}
|
{formatTime(userWithStatus.created_at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className='flex items-center gap-2 flex-shrink-0 w-full'>
|
||||||
</div>
|
<Calendar
|
||||||
</div>
|
className={`${tvMode ? 'w-6 h-6' : 'w-5 h-5'}`}
|
||||||
<div className='my-auto'>
|
/>
|
||||||
<div className='flex items-center gap-2 text-muted-foreground'>
|
<span
|
||||||
<Clock className={`${tvMode ? 'w-8 h-8' : 'w-6 h-6'}`} />
|
className={`${tvMode ? 'text-2xl' : 'text-xl'}`}
|
||||||
<span className={`${tvMode ? 'text-3xl' : 'text-xl'}`}>
|
>
|
||||||
{formatTime(userWithStatus.created_at)}
|
{formatDate(userWithStatus.created_at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2 text-muted-foreground'>
|
<div className='flex items-center gap-2 flex-shrink-0'>
|
||||||
<Calendar
|
{isUpdatedByOther && (
|
||||||
className={`${tvMode ? 'w-8 h-8' : 'w-6 h-6'}`}
|
<div className='flex items-center gap-2'>
|
||||||
/>
|
<BasedAvatar
|
||||||
<span className={`${tvMode ? 'text-3xl' : 'text-xl'}`}>
|
src={userWithStatus.updated_by?.avatar_url}
|
||||||
{formatDate(userWithStatus.created_at)}
|
fullName={userWithStatus.updated_by?.full_name}
|
||||||
</span>
|
className={`${tvMode ? 'w-6 h-6' : 'w-5 h-5'}`}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`${tvMode ? 'text-base' : 'text-sm'}`}
|
||||||
|
>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<p>Updated by</p>
|
||||||
|
{userWithStatus.updated_by?.full_name}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent className='pt-0'>
|
|
||||||
<Drawer>
|
|
||||||
<DrawerTrigger asChild>
|
|
||||||
<div
|
|
||||||
className={`
|
|
||||||
p-4 rounded-lg bg-muted/30 hover:bg-muted/50
|
|
||||||
transition-colors cursor-pointer text-left
|
|
||||||
${tvMode ? 'text-4xl' : 'text-xl'}
|
|
||||||
`}
|
|
||||||
onClick={() =>
|
|
||||||
setSelectedHistoryUser(userWithStatus.user)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<p className='font-medium'>{userWithStatus.status}</p>
|
|
||||||
</div>
|
|
||||||
</DrawerTrigger>
|
|
||||||
{selectedHistoryUser === userWithStatus.user && (
|
|
||||||
<HistoryDrawer user={selectedHistoryUser} />
|
|
||||||
)}
|
|
||||||
</Drawer>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -289,7 +316,7 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
{usersWithStatuses.length === 0 && (
|
{usersWithStatuses.length === 0 && (
|
||||||
<Card className='p-8 text-center'>
|
<Card className='p-8 text-center'>
|
||||||
<p
|
<p
|
||||||
className={`text-muted-foreground ${tvMode ? 'text-4xl' : 'text-lg'}`}
|
className={`text-muted-foreground ${tvMode ? 'text-2xl' : 'text-lg'}`}
|
||||||
>
|
>
|
||||||
No status updates have been made in the past day.
|
No status updates have been made in the past day.
|
||||||
</p>
|
</p>
|
||||||
@@ -305,8 +332,8 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
type='text'
|
type='text'
|
||||||
placeholder='Enter status'
|
placeholder='Enter status update...'
|
||||||
className='flex-1 text-base'
|
className='flex-1 text-2xl'
|
||||||
value={statusInput}
|
value={statusInput}
|
||||||
disabled={updateStatusMutation.isPending}
|
disabled={updateStatusMutation.isPending}
|
||||||
onChange={(e) => setStatusInput(e.target.value)}
|
onChange={(e) => setStatusInput(e.target.value)}
|
||||||
@@ -327,9 +354,8 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
className='px-6'
|
className='px-6'
|
||||||
>
|
>
|
||||||
{selectedUsers.length > 0
|
{selectedUsers.length > 0
|
||||||
? `Update status for ${selectedUsers.length}
|
? `Update ${selectedUsers.length} ${selectedUsers.length > 1 ? 'users' : 'user'}`
|
||||||
${selectedUsers.length > 1 ? 'users' : 'user'}`
|
: 'Update Status'}
|
||||||
: 'Update status'}
|
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
</div>
|
</div>
|
||||||
{updateStatusMessage &&
|
{updateStatusMessage &&
|
||||||
@@ -347,7 +373,7 @@ export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|||||||
<DrawerTrigger asChild>
|
<DrawerTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className={tvMode ? 'text-3xl p-6' : ''}
|
className={tvMode ? 'text-xl p-6' : ''}
|
||||||
>
|
>
|
||||||
View All Status History
|
View All Status History
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -1,388 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import type React from 'react';
|
|
||||||
|
|
||||||
import { useAuth, useTVMode } from '@/components/context';
|
|
||||||
import type { UserWithStatus } from '@/lib/hooks';
|
|
||||||
import { BasedAvatar, Drawer, DrawerTrigger, Loading } from '@/components/ui';
|
|
||||||
import { StatusMessage, SubmitButton } from '@/components/default';
|
|
||||||
import { ConnectionStatus, HistoryDrawer } from '@/components/status';
|
|
||||||
import type { Profile } from '@/utils/supabase';
|
|
||||||
import { makeConditionalClassName } from '@/lib/utils';
|
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { RefreshCw, Clock, Calendar, CheckCircle2 } from 'lucide-react';
|
|
||||||
import { useStatusData, useStatusSubscription } from '@/lib/hooks';
|
|
||||||
import { formatTime, formatDate } from '@/lib/utils';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
type ListProps = {
|
|
||||||
initialStatuses: UserWithStatus[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StatusList = ({ initialStatuses = [] }: ListProps) => {
|
|
||||||
const { isAuthenticated } = useAuth();
|
|
||||||
const { tvMode } = useTVMode();
|
|
||||||
|
|
||||||
const [selectedUsers, setSelectedUsers] = useState<UserWithStatus[]>([]);
|
|
||||||
const [selectAll, setSelectAll] = useState(false);
|
|
||||||
const [statusInput, setStatusInput] = useState('');
|
|
||||||
const [selectedHistoryUser, setSelectedHistoryUser] =
|
|
||||||
useState<Profile | null>(null);
|
|
||||||
const [updateStatusMessage, setUpdateStatusMessage] = useState('');
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: usersWithStatuses = initialStatuses,
|
|
||||||
isLoading: loading,
|
|
||||||
error,
|
|
||||||
refetch,
|
|
||||||
newStatuses,
|
|
||||||
updateStatusMutation,
|
|
||||||
} = useStatusData({
|
|
||||||
initialData: initialStatuses,
|
|
||||||
enabled: isAuthenticated,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { connectionStatus, connect: reconnect } = useStatusSubscription(() => {
|
|
||||||
refetch().catch((error) => {
|
|
||||||
console.error('Error refetching statuses:', error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleUpdateStatus = () => {
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
setUpdateStatusMessage(
|
|
||||||
'Error: You must be signed in to update technician statuses!',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusInput.length < 3 || statusInput.length > 80) {
|
|
||||||
setUpdateStatusMessage(
|
|
||||||
'Error: Your status must be between 3 & 80 characters long!',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStatusMutation.mutate({
|
|
||||||
usersWithStatuses: selectedUsers,
|
|
||||||
status: statusInput.trim(),
|
|
||||||
});
|
|
||||||
|
|
||||||
setSelectedUsers([]);
|
|
||||||
setStatusInput('');
|
|
||||||
setUpdateStatusMessage('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCardSelect = (user: UserWithStatus, e: React.MouseEvent) => {
|
|
||||||
// Prevent selection if clicking on profile elements
|
|
||||||
if ((e.target as HTMLElement).closest('[data-profile-trigger]')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedUsers((prev) =>
|
|
||||||
prev.some((u) => u.user.id === user.user.id)
|
|
||||||
? prev.filter((prevUser) => prevUser.user.id !== user.user.id)
|
|
||||||
: [...prev, user],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectAllChange = () => {
|
|
||||||
if (selectAll) {
|
|
||||||
setSelectedUsers([]);
|
|
||||||
} else {
|
|
||||||
setSelectedUsers(usersWithStatuses);
|
|
||||||
}
|
|
||||||
setSelectAll(!selectAll);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectAll(
|
|
||||||
selectedUsers.length === usersWithStatuses.length &&
|
|
||||||
usersWithStatuses.length > 0,
|
|
||||||
);
|
|
||||||
}, [selectedUsers.length, usersWithStatuses.length]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className='flex justify-center items-center min-h-[400px]'>
|
|
||||||
<Loading className='w-full' alpha={0.5} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col justify-center items-center min-h-[400px] gap-4'>
|
|
||||||
<p className='text-red-500'>Error loading status updates</p>
|
|
||||||
<Button onClick={() => refetch()} variant='outline'>
|
|
||||||
<RefreshCw className='w-4 h-4 mr-2' />
|
|
||||||
Retry
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const containerClassName = makeConditionalClassName({
|
|
||||||
context: tvMode,
|
|
||||||
defaultClassName: 'flex flex-col mx-auto space-y-3 items-center',
|
|
||||||
on: 'lg:w-11/12 w-full mt-8',
|
|
||||||
off: 'sm:w-5/6 md:w-3/4 lg:w-2/3 xl:w-1/2',
|
|
||||||
});
|
|
||||||
|
|
||||||
const headerClassName = makeConditionalClassName({
|
|
||||||
context: tvMode,
|
|
||||||
defaultClassName: 'w-full',
|
|
||||||
on: 'hidden',
|
|
||||||
off: 'flex mb-6 justify-between items-center',
|
|
||||||
});
|
|
||||||
|
|
||||||
const cardContainerClassName = makeConditionalClassName({
|
|
||||||
context: tvMode,
|
|
||||||
defaultClassName: 'w-full',
|
|
||||||
on: 'grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4',
|
|
||||||
off: 'space-y-2 w-full',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={containerClassName}>
|
|
||||||
<div className={headerClassName}>
|
|
||||||
<div className='flex items-center gap-6'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
size='sm'
|
|
||||||
onClick={handleSelectAllChange}
|
|
||||||
className='flex items-center gap-2'
|
|
||||||
>
|
|
||||||
<CheckCircle2
|
|
||||||
className={`w-4 h-4 ${selectAll ? 'text-primary' : ''}`}
|
|
||||||
/>
|
|
||||||
{selectAll ? 'Deselect All' : 'Select All'}
|
|
||||||
</Button>
|
|
||||||
{!tvMode && (
|
|
||||||
<div className='flex items-center gap-2 text-sm text-muted-foreground'>
|
|
||||||
<span>Miss the old table?</span>
|
|
||||||
<Link
|
|
||||||
href='/status/table'
|
|
||||||
className='font-medium text-primary hover:underline'
|
|
||||||
>
|
|
||||||
Find it here!
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<ConnectionStatus
|
|
||||||
status={connectionStatus}
|
|
||||||
onReconnect={reconnect}
|
|
||||||
showAsButton={connectionStatus === 'disconnected'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cardContainerClassName}>
|
|
||||||
{usersWithStatuses.map((userWithStatus) => {
|
|
||||||
const isSelected = selectedUsers.some(
|
|
||||||
(u) => u.user.id === userWithStatus.user.id,
|
|
||||||
);
|
|
||||||
const isNewStatus = newStatuses.has(userWithStatus);
|
|
||||||
const isUpdatedByOther =
|
|
||||||
userWithStatus.updated_by &&
|
|
||||||
userWithStatus.updated_by.id !== userWithStatus.user.id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={userWithStatus.user.id}
|
|
||||||
className={`
|
|
||||||
relative transition-all duration-200 cursor-pointer hover:shadow-md
|
|
||||||
${tvMode ? 'p-4' : 'p-3'}
|
|
||||||
${isSelected ? 'ring-2 ring-primary bg-primary/5 shadow-md' : 'hover:bg-muted/30'}
|
|
||||||
${isNewStatus ? 'animate-in slide-in-from-top-2 duration-500 bg-green-50 border-green-200' : ''}
|
|
||||||
`}
|
|
||||||
onClick={(e) => handleCardSelect(userWithStatus, e)}
|
|
||||||
>
|
|
||||||
{isSelected && (
|
|
||||||
<div className='absolute top-2 right-2 text-primary'>
|
|
||||||
<CheckCircle2
|
|
||||||
className={`${tvMode ? 'w-6 h-6' : 'w-5 h-5'}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CardContent className='p-0'>
|
|
||||||
<div className='flex items-start gap-3'>
|
|
||||||
{/* Profile Section - Clickable for history */}
|
|
||||||
<Drawer>
|
|
||||||
<DrawerTrigger asChild>
|
|
||||||
<div
|
|
||||||
data-profile-trigger
|
|
||||||
className='flex-shrink-0 cursor-pointer hover:opacity-80 transition-opacity'
|
|
||||||
onClick={() =>
|
|
||||||
setSelectedHistoryUser(userWithStatus.user)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<BasedAvatar
|
|
||||||
src={userWithStatus.user.avatar_url}
|
|
||||||
fullName={userWithStatus.user.full_name}
|
|
||||||
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DrawerTrigger>
|
|
||||||
{selectedHistoryUser === userWithStatus.user && (
|
|
||||||
<HistoryDrawer user={selectedHistoryUser} />
|
|
||||||
)}
|
|
||||||
</Drawer>
|
|
||||||
|
|
||||||
{/* Content Section */}
|
|
||||||
<div className='flex-1 min-w-0'>
|
|
||||||
{/* Header with name and timestamp */}
|
|
||||||
<div className='flex items-center justify-between mb-2'>
|
|
||||||
<Drawer>
|
|
||||||
<DrawerTrigger asChild>
|
|
||||||
<h3
|
|
||||||
data-profile-trigger
|
|
||||||
className={`
|
|
||||||
font-semibold cursor-pointer hover:underline truncate
|
|
||||||
${tvMode ? 'text-2xl' : 'text-base'}
|
|
||||||
`}
|
|
||||||
onClick={() =>
|
|
||||||
setSelectedHistoryUser(userWithStatus.user)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{userWithStatus.user.full_name}
|
|
||||||
</h3>
|
|
||||||
</DrawerTrigger>
|
|
||||||
{selectedHistoryUser === userWithStatus.user && (
|
|
||||||
<HistoryDrawer user={selectedHistoryUser} />
|
|
||||||
)}
|
|
||||||
</Drawer>
|
|
||||||
|
|
||||||
<div className='flex items-center gap-2 text-muted-foreground flex-shrink-0'>
|
|
||||||
<Clock
|
|
||||||
className={`${tvMode ? 'w-5 h-5' : 'w-4 h-4'}`}
|
|
||||||
/>
|
|
||||||
<span className={`${tvMode ? 'text-lg' : 'text-sm'}`}>
|
|
||||||
{formatTime(userWithStatus.created_at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status Content */}
|
|
||||||
<div
|
|
||||||
className={`
|
|
||||||
mb-2 leading-relaxed
|
|
||||||
${tvMode ? 'text-xl' : 'text-sm'}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<p>{userWithStatus.status}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer with date and updated by info */}
|
|
||||||
<div className='flex items-center justify-between text-muted-foreground'>
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<Calendar
|
|
||||||
className={`${tvMode ? 'w-4 h-4' : 'w-3 h-3'}`}
|
|
||||||
/>
|
|
||||||
<span className={`${tvMode ? 'text-base' : 'text-xs'}`}>
|
|
||||||
{formatDate(userWithStatus.created_at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isUpdatedByOther && (
|
|
||||||
<div className='flex items-center gap-1'>
|
|
||||||
<BasedAvatar
|
|
||||||
src={userWithStatus.updated_by?.avatar_url}
|
|
||||||
fullName={userWithStatus.updated_by?.full_name}
|
|
||||||
className={`${tvMode ? 'w-5 h-5' : 'w-4 h-4'}`}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={`${tvMode ? 'text-base' : 'text-xs'}`}
|
|
||||||
>
|
|
||||||
Updated by {userWithStatus.updated_by?.full_name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{usersWithStatuses.length === 0 && (
|
|
||||||
<Card className='p-8 text-center'>
|
|
||||||
<p
|
|
||||||
className={`text-muted-foreground ${tvMode ? 'text-2xl' : 'text-base'}`}
|
|
||||||
>
|
|
||||||
No status updates have been made in the past day.
|
|
||||||
</p>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!tvMode && (
|
|
||||||
<Card className='p-6 mt-6 w-full'>
|
|
||||||
<div className='flex flex-col gap-4'>
|
|
||||||
<h3 className='text-lg font-semibold'>Update Status</h3>
|
|
||||||
<div className='flex flex-col gap-4'>
|
|
||||||
<div className='flex gap-4'>
|
|
||||||
<Input
|
|
||||||
autoFocus
|
|
||||||
type='text'
|
|
||||||
placeholder='Enter status update...'
|
|
||||||
className='flex-1 text-base'
|
|
||||||
value={statusInput}
|
|
||||||
disabled={updateStatusMutation.isPending}
|
|
||||||
onChange={(e) => setStatusInput(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (
|
|
||||||
e.key === 'Enter' &&
|
|
||||||
!e.shiftKey &&
|
|
||||||
!updateStatusMutation.isPending
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleUpdateStatus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SubmitButton
|
|
||||||
onClick={handleUpdateStatus}
|
|
||||||
disabled={updateStatusMutation.isPending}
|
|
||||||
className='px-6'
|
|
||||||
>
|
|
||||||
{selectedUsers.length > 0
|
|
||||||
? `Update ${selectedUsers.length} ${selectedUsers.length > 1 ? 'users' : 'user'}`
|
|
||||||
: 'Update Status'}
|
|
||||||
</SubmitButton>
|
|
||||||
</div>
|
|
||||||
{updateStatusMessage &&
|
|
||||||
(updateStatusMessage.includes('Error') ||
|
|
||||||
updateStatusMessage.includes('error') ||
|
|
||||||
updateStatusMessage.includes('failed') ||
|
|
||||||
updateStatusMessage.includes('invalid') ? (
|
|
||||||
<StatusMessage message={{ error: updateStatusMessage }} />
|
|
||||||
) : (
|
|
||||||
<StatusMessage message={{ message: updateStatusMessage }} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className='flex justify-center mt-2'>
|
|
||||||
<Drawer>
|
|
||||||
<DrawerTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
className={tvMode ? 'text-xl p-6' : ''}
|
|
||||||
>
|
|
||||||
View All Status History
|
|
||||||
</Button>
|
|
||||||
</DrawerTrigger>
|
|
||||||
<HistoryDrawer />
|
|
||||||
</Drawer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -139,14 +139,14 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
|
|||||||
const thClassName = makeConditionalClassName({
|
const thClassName = makeConditionalClassName({
|
||||||
context: tvMode,
|
context: tvMode,
|
||||||
defaultClassName: 'py-4 px-4 border font-semibold ',
|
defaultClassName: 'py-4 px-4 border font-semibold ',
|
||||||
on: 'lg:text-6xl xl:min-w-[420px]',
|
on: 'lg:text-5xl xl:min-w-[420px]',
|
||||||
off: 'lg:text-5xl xl:min-w-[300px]',
|
off: 'lg:text-4xl xl:min-w-[320px]',
|
||||||
});
|
});
|
||||||
const tdClassName = makeConditionalClassName({
|
const tdClassName = makeConditionalClassName({
|
||||||
context: tvMode,
|
context: tvMode,
|
||||||
defaultClassName: 'py-2 px-2 border',
|
defaultClassName: 'py-2 px-2 border',
|
||||||
on: 'lg:text-5xl',
|
on: 'lg:text-4xl',
|
||||||
off: 'lg:text-4xl',
|
off: 'lg:text-3xl',
|
||||||
});
|
});
|
||||||
const tCheckboxClassName = `py-3 px-4 border`;
|
const tCheckboxClassName = `py-3 px-4 border`;
|
||||||
const checkBoxClassName = `lg:scale-200 cursor-pointer`;
|
const checkBoxClassName = `lg:scale-200 cursor-pointer`;
|
||||||
@@ -161,13 +161,13 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
|
|||||||
showAsButton={connectionStatus === 'disconnected'}
|
showAsButton={connectionStatus === 'disconnected'}
|
||||||
/>
|
/>
|
||||||
{!tvMode && (
|
{!tvMode && (
|
||||||
<div className='flex flex-row gap-2'>
|
<div className='flex flex-row gap-2 text-xs'>
|
||||||
<p>Tired of the old table? </p>
|
<p className='text-muted-foreground'>Tired of the old table? </p>
|
||||||
<Link
|
<Link
|
||||||
href='/status/list'
|
href='/status/list'
|
||||||
className='italic font-semibold text-accent-foreground'
|
className='italic font-semibold hover:text-primary/80'
|
||||||
>
|
>
|
||||||
Try out the new status list!
|
Try the new status list!
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -248,7 +248,7 @@ export const TechTable = ({ initialStatuses = [] }: TableProps) => {
|
|||||||
fullName={userWithStatus.updated_by?.full_name}
|
fullName={userWithStatus.updated_by?.full_name}
|
||||||
className='w-5 h-5'
|
className='w-5 h-5'
|
||||||
/>
|
/>
|
||||||
<span className={tvMode ? 'text-lg' : 'text-base'}>
|
<span className={tvMode ? 'text-xl' : 'text-base'}>
|
||||||
Updated by {userWithStatus.updated_by.full_name}
|
Updated by {userWithStatus.updated_by.full_name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
export * from './ConnectionStatus';
|
export * from './ConnectionStatus';
|
||||||
export * from './HistoryDrawer';
|
export * from './HistoryDrawer';
|
||||||
//export * from './List';
|
export * from './List';
|
||||||
export * from './StatusList';
|
|
||||||
export * from './Table';
|
export * from './Table';
|
||||||
|
@@ -110,6 +110,10 @@ AZURE_REDIRECT_URI=
|
|||||||
AZURE_TENANT_ID=
|
AZURE_TENANT_ID=
|
||||||
AZURE_TENANT_URL=
|
AZURE_TENANT_URL=
|
||||||
|
|
||||||
|
# Gib's Auth (Trying to set up Authentik)
|
||||||
|
#SAML_ENABLED=false
|
||||||
|
#SAML_PRIVATE_KEY=
|
||||||
|
|
||||||
|
|
||||||
############
|
############
|
||||||
# Studio - Configuration for the Dashboard
|
# Studio - Configuration for the Dashboard
|
||||||
|
@@ -5,22 +5,22 @@
|
|||||||
# Destroy: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans
|
# Destroy: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans
|
||||||
# Reset everything: ./reset.sh
|
# Reset everything: ./reset.sh
|
||||||
|
|
||||||
name: supabase
|
name: techtracker
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
supabase-network:
|
techtracker:
|
||||||
name: supabase-network
|
name: techtracker
|
||||||
driver: bridge
|
driver: bridge
|
||||||
ipam:
|
ipam:
|
||||||
config:
|
config:
|
||||||
- subnet: 172.20.0.0/16
|
- subnet: 172.19.0.0/16
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
studio:
|
studio:
|
||||||
container_name: supabase-studio
|
container_name: supabase-studio
|
||||||
image: supabase/studio:2025.05.19-sha-3487831
|
image: supabase/studio:2025.05.19-sha-3487831
|
||||||
networks: [supabase-network]
|
networks: [techtracker]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
@@ -61,7 +61,7 @@ services:
|
|||||||
kong:
|
kong:
|
||||||
container_name: supabase-kong
|
container_name: supabase-kong
|
||||||
image: kong:2.8.1
|
image: kong:2.8.1
|
||||||
networks: [supabase-network]
|
networks: [techtracker]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- ${KONG_HTTP_PORT}:8000/tcp
|
- ${KONG_HTTP_PORT}:8000/tcp
|
||||||
@@ -90,7 +90,7 @@ services:
|
|||||||
auth:
|
auth:
|
||||||
container_name: supabase-auth
|
container_name: supabase-auth
|
||||||
image: supabase/gotrue:v2.172.1
|
image: supabase/gotrue:v2.172.1
|
||||||
networks: [supabase-network]
|
networks: [techtracker]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
@@ -173,7 +173,7 @@ services:
|
|||||||
GOTRUE_EXTERNAL_AZURE_CLIENT_ID: ${AZURE_CLIENT_ID}
|
GOTRUE_EXTERNAL_AZURE_CLIENT_ID: ${AZURE_CLIENT_ID}
|
||||||
GOTRUE_EXTERNAL_AZURE_SECRET: ${AZURE_SECRET}
|
GOTRUE_EXTERNAL_AZURE_SECRET: ${AZURE_SECRET}
|
||||||
GOTRUE_EXTERNAL_AZURE_TENANT_ID: ${AZURE_TENANT_ID}
|
GOTRUE_EXTERNAL_AZURE_TENANT_ID: ${AZURE_TENANT_ID}
|
||||||
GOTRUE_EXTERNAL_AZURE_TENANT_URL: ${AZURE_TENANT_URL}
|
GOTRUE_EXTERNAL_AZURE_URL: ${AZURE_TENANT_URL}
|
||||||
GOTRUE_EXTERNAL_AZURE_REDIRECT_URI: ${AZURE_REDIRECT_URI}
|
GOTRUE_EXTERNAL_AZURE_REDIRECT_URI: ${AZURE_REDIRECT_URI}
|
||||||
|
|
||||||
# Uncomment to enable custom access token hook. Please see: https://supabase.com/docs/guides/auth/auth-hooks for full list of hooks and additional details about custom_access_token_hook
|
# Uncomment to enable custom access token hook. Please see: https://supabase.com/docs/guides/auth/auth-hooks for full list of hooks and additional details about custom_access_token_hook
|
||||||
@@ -199,7 +199,7 @@ services:
|
|||||||
rest:
|
rest:
|
||||||
container_name: supabase-rest
|
container_name: supabase-rest
|
||||||
image: postgrest/postgrest:v12.2.12
|
image: postgrest/postgrest:v12.2.12
|
||||||
networks: [supabase-network]
|
networks: [techtracker]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
@@ -224,7 +224,7 @@ services:
|
|||||||
# This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain
|
# This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain
|
||||||
container_name: realtime-dev.supabase-realtime
|
container_name: realtime-dev.supabase-realtime
|
||||||
image: supabase/realtime:v2.34.47
|
image: supabase/realtime:v2.34.47
|
||||||
networks: [supabase-network]
|
networks: [techtracker]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
@@ -270,7 +270,7 @@ services:
|
|||||||
storage:
|
storage:
|
||||||
container_name: supabase-storage
|
container_name: supabase-storage
|
||||||
image: supabase/storage-api:v1.22.17
|
image: supabase/storage-api:v1.22.17
|
||||||
networks: [supabase-network]
|
networks: [techtracker]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./volumes/storage:/var/lib/storage:z
|
- ./volumes/storage:/var/lib/storage:z
|
||||||
@@ -314,7 +314,7 @@ services:
|
|||||||
imgproxy:
|
imgproxy:
|
||||||
container_name: supabase-imgproxy
|
container_name: supabase-imgproxy
|
||||||
image: darthsim/imgproxy:v3.8.0
|
image: darthsim/imgproxy:v3.8.0
|
||||||
networks: [supabase-network]
|
networks: [techtracker]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./volumes/storage:/var/lib/storage:z
|
- ./volumes/storage:/var/lib/storage:z
|
||||||
@@ -337,7 +337,7 @@ services:
|
|||||||
meta:
|
meta:
|
||||||
container_name: supabase-meta
|
container_name: supabase-meta
|
||||||
image: supabase/postgres-meta:v0.89.0
|
image: supabase/postgres-meta:v0.89.0
|
||||||
networks: [supabase-network]
|
networks: [techtracker]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
@@ -356,7 +356,7 @@ services:
|
|||||||
functions:
|
functions:
|
||||||
container_name: supabase-edge-functions
|
container_name: supabase-edge-functions
|
||||||
image: supabase/edge-runtime:v1.67.4
|
image: supabase/edge-runtime:v1.67.4
|
||||||
networks: [supabase-network]
|
networks: [techtracker]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./volumes/functions:/home/deno/functions:Z
|
- ./volumes/functions:/home/deno/functions:Z
|
||||||
@@ -381,7 +381,7 @@ services:
|
|||||||
analytics:
|
analytics:
|
||||||
container_name: supabase-analytics
|
container_name: supabase-analytics
|
||||||
image: supabase/logflare:1.12.0
|
image: supabase/logflare:1.12.0
|
||||||
networks: [supabase-network]
|
networks: [techtracker]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- 4000:4000
|
- 4000:4000
|
||||||
@@ -430,7 +430,7 @@ services:
|
|||||||
db:
|
db:
|
||||||
container_name: supabase-db
|
container_name: supabase-db
|
||||||
image: supabase/postgres:15.8.1.060
|
image: supabase/postgres:15.8.1.060
|
||||||
networks: [supabase-network]
|
networks: [techtracker]
|
||||||
ports:
|
ports:
|
||||||
- ${POSTGRES_PORT}:${POSTGRES_PORT}
|
- ${POSTGRES_PORT}:${POSTGRES_PORT}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -490,7 +490,7 @@ services:
|
|||||||
vector:
|
vector:
|
||||||
container_name: supabase-vector
|
container_name: supabase-vector
|
||||||
image: timberio/vector:0.28.1-alpine
|
image: timberio/vector:0.28.1-alpine
|
||||||
networks: [supabase-network]
|
networks: [techtracker]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./volumes/logs/vector.yml:/etc/vector/vector.yml:ro,z
|
- ./volumes/logs/vector.yml:/etc/vector/vector.yml:ro,z
|
||||||
@@ -522,7 +522,7 @@ services:
|
|||||||
supavisor:
|
supavisor:
|
||||||
container_name: supabase-pooler
|
container_name: supabase-pooler
|
||||||
image: supabase/supavisor:2.5.1
|
image: supabase/supavisor:2.5.1
|
||||||
networks: [supabase-network]
|
networks: [techtracker]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
#- ${POSTGRES_PORT}:5432
|
#- ${POSTGRES_PORT}:5432
|
||||||
@@ -574,3 +574,4 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db-config:
|
db-config:
|
||||||
|
name: techtracker-db-config
|
||||||
|
@@ -0,0 +1,126 @@
|
|||||||
|
-- Create a table for public profiles
|
||||||
|
create table profiles (
|
||||||
|
id uuid references auth.users on delete cascade not null primary key,
|
||||||
|
updated_at timestamp with time zone,
|
||||||
|
email text unique,
|
||||||
|
full_name text,
|
||||||
|
avatar_url text,
|
||||||
|
provider text,
|
||||||
|
|
||||||
|
constraint full_name_length check (char_length(full_name) >= 3 and char_length(full_name) <= 50)
|
||||||
|
);
|
||||||
|
-- Set up Row Level Security (RLS)
|
||||||
|
-- See https://supabase.com/docs/guides/auth/row-level-security for more details.
|
||||||
|
alter table profiles
|
||||||
|
enable row level security;
|
||||||
|
|
||||||
|
create policy "Public profiles are viewable by everyone." on profiles
|
||||||
|
for select using (true);
|
||||||
|
|
||||||
|
create policy "Users can insert their own profile." on profiles
|
||||||
|
for insert with check ((select auth.uid()) = id);
|
||||||
|
|
||||||
|
create policy "Users can update own profile." on profiles
|
||||||
|
for update using ((select auth.uid()) = id);
|
||||||
|
|
||||||
|
-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.
|
||||||
|
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.
|
||||||
|
create function public.handle_new_user()
|
||||||
|
returns trigger
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
insert into public.profiles (id, email, full_name, avatar_url, provider, updated_at)
|
||||||
|
values (
|
||||||
|
new.id,
|
||||||
|
new.email,
|
||||||
|
new.raw_user_meta_data->>'full_name',
|
||||||
|
new.raw_user_meta_data->>'avatar_url',
|
||||||
|
new.raw_user_meta_data->>'provider',
|
||||||
|
now()
|
||||||
|
);
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql security definer;
|
||||||
|
create trigger on_auth_user_created
|
||||||
|
after insert on auth.users
|
||||||
|
for each row execute procedure public.handle_new_user();
|
||||||
|
|
||||||
|
-- Set up Storage!
|
||||||
|
insert into storage.buckets (id, name)
|
||||||
|
values ('avatars', 'avatars');
|
||||||
|
|
||||||
|
-- Set up access controls for storage.
|
||||||
|
-- See https://supabase.com/docs/guides/storage#policy-examples for more details.
|
||||||
|
create policy "Avatar images are publicly accessible." on storage.objects
|
||||||
|
for select using (bucket_id = 'avatars');
|
||||||
|
|
||||||
|
create policy "Anyone can upload an avatar." on storage.objects
|
||||||
|
for insert with check (bucket_id = 'avatars');
|
||||||
|
|
||||||
|
create policy "Anyone can update an avatar." on storage.objects
|
||||||
|
for update using (bucket_id = 'avatars');
|
||||||
|
|
||||||
|
create policy "Anyone can delete an avatar." on storage.objects
|
||||||
|
for delete using (bucket_id = 'avatars');
|
||||||
|
|
||||||
|
-- Create a table for public statuses
|
||||||
|
CREATE TABLE statuses (
|
||||||
|
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
user_id uuid REFERENCES public.profiles ON DELETE CASCADE NOT NULL,
|
||||||
|
updated_by_id uuid REFERENCES public.profiles ON DELETE SET NULL DEFAULT auth.uid(),
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
status text NOT NULL,
|
||||||
|
CONSTRAINT status_length CHECK (char_length(status) >= 3 AND char_length(status) <= 80)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Set up Row Level Security (RLS)
|
||||||
|
ALTER TABLE statuses
|
||||||
|
ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Policies
|
||||||
|
CREATE POLICY "Public statuses are viewable by everyone." ON statuses
|
||||||
|
FOR SELECT USING (true);
|
||||||
|
|
||||||
|
-- RECREATE it using the recommended sub-select form
|
||||||
|
CREATE POLICY "Authenticated users can insert statuses for any user."
|
||||||
|
ON public.statuses
|
||||||
|
FOR INSERT
|
||||||
|
WITH CHECK (
|
||||||
|
(SELECT auth.role()) = 'authenticated'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ADD an UPDATE policy so anyone signed-in can update *any* status
|
||||||
|
CREATE POLICY "Authenticated users can update statuses for any user."
|
||||||
|
ON public.statuses
|
||||||
|
FOR UPDATE
|
||||||
|
USING (
|
||||||
|
(SELECT auth.role()) = 'authenticated'
|
||||||
|
)
|
||||||
|
WITH CHECK (
|
||||||
|
(SELECT auth.role()) = 'authenticated'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Function to add first status
|
||||||
|
CREATE FUNCTION public.handle_first_status()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
SET search_path = ''
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO public.statuses (user_id, updated_by_id, status)
|
||||||
|
VALUES (
|
||||||
|
NEW.id,
|
||||||
|
NEW.id,
|
||||||
|
'Just joined!'
|
||||||
|
);
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- Create a separate trigger for the status
|
||||||
|
CREATE TRIGGER on_auth_user_created_add_status
|
||||||
|
AFTER INSERT ON auth.users
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE public.handle_first_status();
|
||||||
|
|
||||||
|
alter publication supabase_realtime add table profiles;
|
||||||
|
alter publication supabase_realtime add table statuses;
|
||||||
|
Reference in New Issue
Block a user