mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Refactor profile screen and organize project structure
- Extract ProfileScreen and components to features/profile - Move shared hooks and stores to top-level - Add RLS policies for admin permissions - Add RELEASING.md guide - Update .gitignore
This commit is contained in:
1 parent
df4c4d486b
commit
8f8c6e401d
79 files changed
+6689
-4944
No files matched your search
@@ -134,7 +134,7 @@ function App() {
|
||||
<Route
|
||||
path="/feedback"
|
||||
element={
|
||||
<ProtectedRoute requiredPermission={PERMISSION_KEYS.VIEW_USERS}>
|
||||
<ProtectedRoute requiredPermission={PERMISSION_KEYS.MANAGE_FEEDBACK}>
|
||||
<FeedbackPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ const usersNav: NavItem[] = [
|
||||
label: 'Feedback',
|
||||
href: '/feedback',
|
||||
icon: <MessageSquareText className="h-5 w-5" />,
|
||||
requiredPermission: PERMISSION_KEYS.VIEW_USERS,
|
||||
requiredPermission: PERMISSION_KEYS.MANAGE_FEEDBACK,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export const PERMISSION_KEYS = {
|
||||
VIEW_MATCHES: 'view_matches',
|
||||
VIEW_RESPONSES: 'view_responses',
|
||||
VIEW_ACTIVITY: 'view_activity',
|
||||
MANAGE_FEEDBACK: 'manage_feedback',
|
||||
// System permissions
|
||||
MANAGE_CODES: 'manage_codes',
|
||||
MANAGE_ADMINS: 'manage_admins',
|
||||
@@ -40,6 +41,7 @@ export const PERMISSION_METADATA: Record<PermissionKey, { label: string; descrip
|
||||
[PERMISSION_KEYS.VIEW_MATCHES]: { label: 'Can view matches', description: 'View user matches and match details', group: 'users' },
|
||||
[PERMISSION_KEYS.VIEW_RESPONSES]: { label: 'Can view responses', description: 'View user question responses', group: 'users' },
|
||||
[PERMISSION_KEYS.VIEW_ACTIVITY]: { label: 'Can view activity', description: 'View user activity feed (responses, matches, messages, signups)', group: 'users' },
|
||||
[PERMISSION_KEYS.MANAGE_FEEDBACK]: { label: 'Can manage feedback', description: 'View and respond to user feedback', group: 'users' },
|
||||
[PERMISSION_KEYS.MANAGE_CODES]: { label: 'Can manage redemption codes', description: 'Create and view redemption codes', group: 'system' },
|
||||
[PERMISSION_KEYS.MANAGE_ADMINS]: { label: 'Can manage admins', description: 'Add/remove admin users and change permissions', group: 'system' },
|
||||
[PERMISSION_KEYS.MANAGE_AI_CONFIG]: { label: 'Can manage AI settings', description: 'Configure AI models, API keys, and council mode', group: 'system' },
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
MessageCircle,
|
||||
UserPlus,
|
||||
Image as ImageIcon,
|
||||
Video as VideoIcon,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
|
||||
// Types
|
||||
@@ -75,6 +77,7 @@ interface MessageActivity {
|
||||
user_id: string;
|
||||
content: string | null;
|
||||
media_path: string | null;
|
||||
media_type: 'image' | 'video' | null;
|
||||
created_at: string;
|
||||
profile: {
|
||||
id: string;
|
||||
@@ -232,7 +235,7 @@ export function UserActivityPage() {
|
||||
const { data: messagesData, error: messagesError } = await supabase
|
||||
.from('messages')
|
||||
.select(`
|
||||
id, user_id, content, media_path, created_at,
|
||||
id, user_id, content, media_path, media_type, created_at,
|
||||
match:matches(
|
||||
id,
|
||||
question:questions(text)
|
||||
@@ -267,6 +270,7 @@ export function UserActivityPage() {
|
||||
user_id: item.user_id,
|
||||
content: item.content,
|
||||
media_path: item.media_path,
|
||||
media_type: item.media_type,
|
||||
created_at: item.created_at,
|
||||
profile: profilesMap[item.user_id] || null,
|
||||
match: item.match ? {
|
||||
@@ -545,18 +549,19 @@ export function UserActivityPage() {
|
||||
<TableHead>Message</TableHead>
|
||||
<TableHead>Context</TableHead>
|
||||
<TableHead>Time</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading.messages ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center py-8">
|
||||
<TableCell colSpan={5} className="text-center py-8">
|
||||
<div className="flex justify-center"><Skeleton className="h-6 w-32" /></div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : messages.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center py-8 text-muted-foreground">
|
||||
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
|
||||
No messages yet.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -567,8 +572,11 @@ export function UserActivityPage() {
|
||||
<TableCell className="max-w-[300px]">
|
||||
{item.media_path ? (
|
||||
<span className="inline-flex items-center gap-1 text-muted-foreground">
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
[Media]
|
||||
{item.media_type === 'video' ? (
|
||||
<><VideoIcon className="h-4 w-4" /> [Video]</>
|
||||
) : (
|
||||
<><ImageIcon className="h-4 w-4" /> [Image]</>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="truncate block">
|
||||
@@ -584,6 +592,19 @@ export function UserActivityPage() {
|
||||
<TableCell className="text-muted-foreground text-sm whitespace-nowrap">
|
||||
{formatDistanceToNow(new Date(item.created_at), { addSuffix: true })}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.match && item.profile ? (
|
||||
<Link
|
||||
to={`/users/${item.profile.id}/matches/${item.match.id}`}
|
||||
className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
View Chat
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { ArrowLeft, Check, CheckCheck, Eye } from 'lucide-react';
|
||||
import { ArrowLeft, Check, CheckCheck, Eye, Video as VideoIcon } from 'lucide-react';
|
||||
import { format, isToday, isYesterday } from 'date-fns';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription';
|
||||
@@ -32,6 +32,7 @@ interface Message {
|
||||
user_id: string;
|
||||
content: string | null;
|
||||
media_path: string | null;
|
||||
media_type: 'image' | 'video' | null;
|
||||
created_at: string;
|
||||
read_at: string | null;
|
||||
media_viewed_at: string | null;
|
||||
@@ -117,6 +118,79 @@ function ChatImage({ mediaPath }: { mediaPath: string }) {
|
||||
}
|
||||
|
||||
|
||||
function ChatVideo({ mediaPath }: { mediaPath: string }) {
|
||||
const [url, setUrl] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mediaPath) return;
|
||||
const getUrl = async () => {
|
||||
try {
|
||||
// Handle legacy full URLs
|
||||
let path = mediaPath;
|
||||
if (path.startsWith('http')) {
|
||||
const parts = path.split('/chat-media/');
|
||||
if (parts.length > 1) {
|
||||
path = decodeURIComponent(parts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase.storage
|
||||
.from('chat-media')
|
||||
.createSignedUrl(path, 3600);
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating signed URL for video:', error);
|
||||
setError(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.signedUrl) {
|
||||
setUrl(data.signedUrl);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Unexpected error loading video:', err);
|
||||
setError('Failed to load');
|
||||
}
|
||||
};
|
||||
getUrl();
|
||||
}, [mediaPath]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="h-48 w-64 rounded-lg bg-muted flex flex-col items-center justify-center border border-red-200 p-2">
|
||||
<VideoIcon className="h-8 w-8 text-red-400 mb-2" />
|
||||
<span className="text-xs text-red-500 text-center">Failed to load video</span>
|
||||
<span className="text-[10px] text-muted-foreground mt-1 text-center truncate w-full" title={error}>
|
||||
{error}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
return (
|
||||
<div className="h-48 w-64 rounded-lg bg-muted flex items-center justify-center">
|
||||
<Skeleton className="h-full w-full rounded-lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<video
|
||||
src={url}
|
||||
controls
|
||||
className="max-w-xs rounded-lg"
|
||||
preload="metadata"
|
||||
>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function MatchChatPage() {
|
||||
const { userId, matchId } = useParams<{ userId: string; matchId: string }>();
|
||||
const [user, setUser] = useState<Profile | null>(null);
|
||||
@@ -337,7 +411,11 @@ export function MatchChatPage() {
|
||||
{message.media_path && (
|
||||
<div className="mt-2">
|
||||
<div className="relative group">
|
||||
<ChatImage mediaPath={message.media_path} />
|
||||
{message.media_type === 'video' ? (
|
||||
<ChatVideo mediaPath={message.media_path} />
|
||||
) : (
|
||||
<ChatImage mediaPath={message.media_path} />
|
||||
)}
|
||||
{message.media_viewed_at && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
|
||||
Reference in new issue
Block a user