Skip to content

Commit ad253a9

Browse files
authored
Merge branch 'main' into copilot/edit-calendar-dates-notifications
2 parents a0c65ae + ea00bb8 commit ad253a9

10 files changed

Lines changed: 737 additions & 379 deletions

File tree

package-lock.json

Lines changed: 466 additions & 314 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"cmdk": "^1.0.0",
4949
"date-fns": "^3.6.0",
5050
"embla-carousel-react": "^8.3.0",
51-
"firebase": "^10.9.0",
51+
"firebase": "^12.9.0",
5252
"framer-motion": "^11.2.10",
5353
"input-otp": "^1.2.4",
5454
"leaflet": "^1.9.4",

src/components/BreathingExerciseCard.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ interface BreathingExerciseCardProps {
3030
breathInDuration?: number; // in seconds
3131
holdDuration?: number; // in seconds
3232
breathOutDuration?: number; // in seconds
33+
comingSoon?: boolean;
3334
}
3435

3536
// Fallback audio URLs
@@ -55,6 +56,7 @@ const BreathingExerciseCard: React.FC<BreathingExerciseCardProps> = ({
5556
breathInDuration = 4,
5657
holdDuration = 4,
5758
breathOutDuration = 6,
59+
comingSoon = false,
5860
}) => {
5961
const [isPlaying, setIsPlaying] = useState(false);
6062
const [isFavorited, setIsFavorited] = useState(favorite);
@@ -326,7 +328,8 @@ const BreathingExerciseCard: React.FC<BreathingExerciseCardProps> = ({
326328
};
327329

328330
return (
329-
<Card className={cn("overflow-hidden transition-all duration-300 ease-apple hover:shadow-md", className)}>
331+
<div className="relative">
332+
<Card className={cn("overflow-hidden transition-all duration-300 ease-apple hover:shadow-md", className, comingSoon && "blur-sm pointer-events-none select-none")} aria-hidden={comingSoon || undefined}>
330333
<CardHeader className="pb-2">
331334
<div className="flex justify-between items-start">
332335
<div>
@@ -438,6 +441,14 @@ const BreathingExerciseCard: React.FC<BreathingExerciseCardProps> = ({
438441
</Button>
439442
</CardFooter>
440443
</Card>
444+
{comingSoon && (
445+
<div className="absolute inset-0 flex items-center justify-center z-10 rounded-lg" role="status" aria-label="Coming soon">
446+
<Badge className="text-sm px-4 py-2 bg-background/90 border-2 shadow-lg">
447+
🔒 Coming Soon
448+
</Badge>
449+
</div>
450+
)}
451+
</div>
441452
);
442453
};
443454

src/components/MeditationCard.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export interface MeditationCardProps {
3434
breathInDuration?: number;
3535
holdDuration?: number;
3636
breathOutDuration?: number;
37+
comingSoon?: boolean;
3738
}
3839

3940
// Fallback audio URLs in case the provided ones don't work
@@ -55,6 +56,7 @@ const MeditationCard: React.FC<MeditationCardProps> = ({
5556
audioUrl,
5657
type = 'meditation',
5758
className,
59+
comingSoon = false,
5860
}) => {
5961
const [isPlaying, setIsPlaying] = useState(false);
6062
const [isFavorited, setIsFavorited] = useState(false);
@@ -280,7 +282,8 @@ const MeditationCard: React.FC<MeditationCardProps> = ({
280282
};
281283

282284
return (
283-
<Card className={cn("overflow-hidden transition-all duration-300 ease-apple hover:shadow-md", className)}>
285+
<div className="relative">
286+
<Card className={cn("overflow-hidden transition-all duration-300 ease-apple hover:shadow-md", className, comingSoon && "blur-sm pointer-events-none select-none")} aria-hidden={comingSoon || undefined}>
284287
{imageUrl && (
285288
<div className="h-48 overflow-hidden">
286289
<img
@@ -373,6 +376,14 @@ const MeditationCard: React.FC<MeditationCardProps> = ({
373376
</Button>
374377
</CardFooter>
375378
</Card>
379+
{comingSoon && (
380+
<div className="absolute inset-0 flex items-center justify-center z-10 rounded-lg" role="status" aria-label="Coming soon">
381+
<Badge className="text-sm px-4 py-2 bg-background/90 border-2 shadow-lg">
382+
🔒 Coming Soon
383+
</Badge>
384+
</div>
385+
)}
386+
</div>
376387
);
377388
};
378389

src/components/PartnerProgress.tsx

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
Bar
3131
} from 'recharts';
3232
import { useIsMobile } from "@/hooks/use-mobile";
33-
import { format } from 'date-fns';
33+
import { format, differenceInDays } from 'date-fns';
3434

3535
const PartnerProgress: React.FC = () => {
3636
const { accountabilityPartners } = useAuth();
@@ -145,45 +145,69 @@ const PartnerProgress: React.FC = () => {
145145
{/* Partner details */}
146146
{partner && (
147147
<div className="p-4 border rounded-md bg-muted/10">
148-
<div className="flex items-center mb-4">
149-
<Avatar className="h-12 w-12 mr-3">
150-
<AvatarFallback className="text-lg">
151-
{getInitials(partner)}
152-
</AvatarFallback>
153-
</Avatar>
154-
<div>
155-
<h3 className="font-medium">{partner.firstName} {partner.lastName}</h3>
156-
<p className="text-sm text-muted-foreground">{partner.email}</p>
157-
</div>
158-
</div>
159-
160-
{partnerData && (
161-
<>
162-
<div className="grid grid-cols-2 gap-2 mb-4">
163-
<div className="p-2 bg-secondary/50 rounded-md text-center">
164-
<p className="text-sm text-muted-foreground">Clean Days</p>
165-
<p className="text-xl font-bold">{partnerData.cleanDays}</p>
166-
</div>
167-
<div className="p-2 bg-secondary/50 rounded-md text-center">
168-
<p className="text-sm text-muted-foreground">Current Streak</p>
169-
<p className="text-xl font-bold">{partnerData.streakData[partnerData.streakData.length - 1]?.streak || 0}</p>
170-
</div>
171-
</div>
172-
173-
<div className="grid grid-cols-2 gap-2">
174-
<div className="p-2 bg-secondary/50 rounded-md text-center">
175-
<p className="text-sm text-muted-foreground">Longest Streak</p>
176-
<p className="text-xl font-bold">{partnerData.longestStreak}</p>
177-
</div>
178-
<div className="p-2 bg-secondary/50 rounded-md text-center">
179-
<p className="text-sm text-muted-foreground">Net Growth</p>
180-
<p className={`text-xl font-bold ${partnerData.netGrowth >= 0 ? 'text-green-500' : 'text-red-500'}`}>
181-
{partnerData.netGrowth}
182-
</p>
148+
{(() => {
149+
const lastActive = partner.lastCheckIn?.toDate?.();
150+
const daysInactive = lastActive ? differenceInDays(new Date(), lastActive) : null;
151+
const isInactive = daysInactive !== null && daysInactive >= 7;
152+
const lastActiveFormatted = lastActive ? format(lastActive, 'MMM d, yyyy') : null;
153+
return (
154+
<>
155+
<div className="flex items-center mb-4">
156+
<Avatar className="h-12 w-12 mr-3">
157+
<AvatarFallback className="text-lg">
158+
{getInitials(partner)}
159+
</AvatarFallback>
160+
</Avatar>
161+
<div>
162+
<h3 className="font-medium">{partner.firstName} {partner.lastName}</h3>
163+
<p className="text-sm text-muted-foreground">@{partner.username}</p>
164+
{isInactive && lastActiveFormatted && (
165+
<p className="text-xs text-muted-foreground mt-0.5">
166+
Last active: {lastActiveFormatted}
167+
</p>
168+
)}
169+
</div>
183170
</div>
184-
</div>
185-
</>
186-
)}
171+
172+
{partnerData && (
173+
<>
174+
<div className="grid grid-cols-2 gap-2 mb-4">
175+
<div className="p-2 bg-secondary/50 rounded-md text-center">
176+
<p className="text-sm text-muted-foreground">Clean Days</p>
177+
<p className="text-xl font-bold">{partnerData.cleanDays}</p>
178+
</div>
179+
<div className="p-2 bg-secondary/50 rounded-md text-center">
180+
{isInactive && lastActiveFormatted ? (
181+
<>
182+
<p className="text-sm text-muted-foreground">Last Active</p>
183+
<p className="text-sm font-bold">{lastActiveFormatted}</p>
184+
</>
185+
) : (
186+
<>
187+
<p className="text-sm text-muted-foreground">Current Streak</p>
188+
<p className="text-xl font-bold">{partnerData.streakData[partnerData.streakData.length - 1]?.streak || 0}</p>
189+
</>
190+
)}
191+
</div>
192+
</div>
193+
194+
<div className="grid grid-cols-2 gap-2">
195+
<div className="p-2 bg-secondary/50 rounded-md text-center">
196+
<p className="text-sm text-muted-foreground">Longest Streak</p>
197+
<p className="text-xl font-bold">{partnerData.longestStreak}</p>
198+
</div>
199+
<div className="p-2 bg-secondary/50 rounded-md text-center">
200+
<p className="text-sm text-muted-foreground">Net Growth</p>
201+
<p className={`text-xl font-bold ${partnerData.netGrowth >= 0 ? 'text-green-500' : 'text-red-500'}`}>
202+
{partnerData.netGrowth}
203+
</p>
204+
</div>
205+
</div>
206+
</>
207+
)}
208+
</>
209+
);
210+
})()}
187211
</div>
188212
)}
189213

src/pages/Admin.tsx

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { Textarea } from '@/components/ui/textarea';
99
import { Switch } from '@/components/ui/switch';
1010
import { Badge } from '@/components/ui/badge';
1111
import { CustomBadge } from '@/components/ui/custom-badge';
12+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
13+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
1214
import {
1315
Users,
1416
Lock,
@@ -21,8 +23,9 @@ import {
2123
Clock
2224
} from 'lucide-react';
2325
import { motion } from 'framer-motion';
24-
import { db } from '../utils/firebase';
26+
import { db, adminUpdateUser, adminSuspendUser } from '../utils/firebase';
2527
import { collection, getDocs, query, where, Timestamp } from 'firebase/firestore';
28+
import { toast } from 'sonner';
2629
import {
2730
BarChart,
2831
Bar,
@@ -96,6 +99,12 @@ const Admin: React.FC = () => {
9699
const [users, setUsers] = useState<User[]>([]);
97100
const [isLoading, setIsLoading] = useState(true);
98101
const [checkInTimes, setCheckInTimes] = useState<CheckInTime[]>([]);
102+
const [editUser, setEditUser] = useState<User | null>(null);
103+
const [editRole, setEditRole] = useState<string>('member');
104+
const [editStreak, setEditStreak] = useState<number>(0);
105+
const [isSaving, setIsSaving] = useState(false);
106+
const [suspendUser, setSuspendUser] = useState<User | null>(null);
107+
const [isSuspending, setIsSuspending] = useState(false);
99108
const isMobile = useIsMobile();
100109

101110
useEffect(() => {
@@ -204,7 +213,48 @@ const Admin: React.FC = () => {
204213

205214
return colors[day as keyof typeof colors] || '#a1a1aa';
206215
};
207-
216+
217+
const handleEditOpen = (user: User) => {
218+
setEditUser(user);
219+
setEditRole(user.role || 'member');
220+
setEditStreak(user.streakDays || 0);
221+
};
222+
223+
const handleEditSave = async () => {
224+
if (!editUser) return;
225+
setIsSaving(true);
226+
const success = await adminUpdateUser(editUser.id, {
227+
role: editRole as 'admin' | 'member',
228+
streakDays: editStreak
229+
});
230+
if (success) {
231+
setUsers(prev => prev.map(u =>
232+
u.id === editUser.id ? { ...u, role: editRole, streakDays: editStreak } : u
233+
));
234+
toast.success('User updated successfully');
235+
} else {
236+
toast.error('Failed to update user');
237+
}
238+
setIsSaving(false);
239+
setEditUser(null);
240+
};
241+
242+
const handleSuspendConfirm = async () => {
243+
if (!suspendUser) return;
244+
setIsSuspending(true);
245+
const success = await adminSuspendUser(suspendUser.id);
246+
if (success) {
247+
setUsers(prev => prev.map(u =>
248+
u.id === suspendUser.id ? { ...u, streakDays: 0, status: 'inactive' } : u
249+
));
250+
toast.success('User suspended successfully');
251+
} else {
252+
toast.error('Failed to suspend user');
253+
}
254+
setIsSuspending(false);
255+
setSuspendUser(null);
256+
};
257+
208258
return (
209259
<motion.div
210260
className="container max-w-6xl py-8 pb-16"
@@ -324,8 +374,8 @@ const Admin: React.FC = () => {
324374
<td className="py-3 px-4">{user.streakDays} days</td>
325375
<td className="py-3 px-4">
326376
<div className="flex gap-2">
327-
<Button variant="ghost" size="sm">Edit</Button>
328-
<Button variant="ghost" size="sm" className="text-destructive">Suspend</Button>
377+
<Button variant="ghost" size="sm" onClick={() => handleEditOpen(user)}>Edit</Button>
378+
<Button variant="ghost" size="sm" className="text-destructive" onClick={() => setSuspendUser(user)}>Suspend</Button>
329379
</div>
330380
</td>
331381
</tr>
@@ -783,6 +833,66 @@ const Admin: React.FC = () => {
783833
</motion.div>
784834
</TabsContent>
785835
</Tabs>
836+
837+
{/* Edit User Dialog */}
838+
<Dialog open={!!editUser} onOpenChange={(open) => { if (!open) setEditUser(null); }}>
839+
<DialogContent>
840+
<DialogHeader>
841+
<DialogTitle>Edit User</DialogTitle>
842+
<DialogDescription>
843+
Update the role or streak days for {editUser?.name}.
844+
</DialogDescription>
845+
</DialogHeader>
846+
<div className="space-y-4 py-2">
847+
<div className="space-y-2">
848+
<Label htmlFor="edit-role">Role</Label>
849+
<Select value={editRole} onValueChange={setEditRole}>
850+
<SelectTrigger id="edit-role">
851+
<SelectValue />
852+
</SelectTrigger>
853+
<SelectContent>
854+
<SelectItem value="member">Member</SelectItem>
855+
<SelectItem value="admin">Admin</SelectItem>
856+
</SelectContent>
857+
</Select>
858+
</div>
859+
<div className="space-y-2">
860+
<Label htmlFor="edit-streak">Streak Days</Label>
861+
<Input
862+
id="edit-streak"
863+
type="number"
864+
min={0}
865+
value={editStreak}
866+
onChange={e => setEditStreak(Number(e.target.value))}
867+
/>
868+
</div>
869+
</div>
870+
<DialogFooter>
871+
<Button variant="outline" onClick={() => setEditUser(null)} disabled={isSaving}>Cancel</Button>
872+
<Button onClick={handleEditSave} disabled={isSaving}>
873+
{isSaving ? 'Saving...' : 'Save'}
874+
</Button>
875+
</DialogFooter>
876+
</DialogContent>
877+
</Dialog>
878+
879+
{/* Suspend User Confirmation */}
880+
<AlertDialog open={!!suspendUser} onOpenChange={(open) => { if (!open) setSuspendUser(null); }}>
881+
<AlertDialogContent>
882+
<AlertDialogHeader>
883+
<AlertDialogTitle>Suspend User</AlertDialogTitle>
884+
<AlertDialogDescription>
885+
This will delete all entries (meditations, journal, relapses) and reset the streak for {suspendUser?.name}. This action cannot be undone.
886+
</AlertDialogDescription>
887+
</AlertDialogHeader>
888+
<AlertDialogFooter>
889+
<AlertDialogCancel disabled={isSuspending}>Cancel</AlertDialogCancel>
890+
<AlertDialogAction onClick={handleSuspendConfirm} disabled={isSuspending} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
891+
{isSuspending ? 'Suspending...' : 'Suspend'}
892+
</AlertDialogAction>
893+
</AlertDialogFooter>
894+
</AlertDialogContent>
895+
</AlertDialog>
786896
</motion.div>
787897
);
788898
};

0 commit comments

Comments
 (0)