Skip to content

Commit 7e591ae

Browse files
committed
refactor the slug layout page by creating a ton of smaller components
1 parent c11cadf commit 7e591ae

11 files changed

+710
-634
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { type Segment } from "~/db/schema";
2+
import { EditVideoButton } from "./edit-video-button";
3+
import { DeleteVideoButton } from "./delete-video-button";
4+
5+
interface AdminControlsProps {
6+
currentSegment: Segment;
7+
}
8+
9+
export function AdminControls({ currentSegment }: AdminControlsProps) {
10+
return (
11+
<div className="flex items-center gap-2">
12+
<EditVideoButton currentSegment={currentSegment} />
13+
<DeleteVideoButton currentSegmentId={currentSegment.id} />
14+
</div>
15+
);
16+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { MessageSquare } from "lucide-react";
2+
import { Suspense, useState, useEffect } from "react";
3+
import { useQuery } from "@tanstack/react-query";
4+
import { getCommentsQuery } from "~/lib/queries/comments";
5+
import { CommentForm } from "./comment-form";
6+
import { CommentList } from "./comment-list";
7+
8+
interface CommentsPanelProps {
9+
currentSegmentId: number;
10+
isLoggedIn: boolean;
11+
activeTab: "content" | "comments";
12+
}
13+
14+
export function CommentsPanel({
15+
currentSegmentId,
16+
isLoggedIn,
17+
activeTab,
18+
}: CommentsPanelProps) {
19+
// Check if there are existing comments to determine initial form visibility
20+
const { data: existingComments } = useQuery(
21+
getCommentsQuery(currentSegmentId)
22+
);
23+
const [showCommentForm, setShowCommentForm] = useState(
24+
() => (existingComments && existingComments.length > 0) || false
25+
);
26+
27+
// Update form visibility when comments data changes
28+
useEffect(() => {
29+
if (existingComments && existingComments.length > 0) {
30+
setShowCommentForm(true);
31+
}
32+
}, [existingComments]);
33+
34+
const onStartDiscussion = () => setShowCommentForm(true);
35+
36+
return (
37+
<div className="animate-fade-in space-y-8">
38+
{/* Comment Form Section */}
39+
{showCommentForm && (
40+
<div className="space-y-4">
41+
<div className="flex items-center justify-between">
42+
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
43+
<MessageSquare className="h-5 w-5 text-theme-600 dark:text-theme-400" />
44+
Join the Discussion
45+
</h3>
46+
{isLoggedIn && (
47+
<div className="text-sm text-muted-foreground">
48+
Share your thoughts
49+
</div>
50+
)}
51+
</div>
52+
<CommentForm
53+
autoFocus={showCommentForm && activeTab === "comments"}
54+
/>
55+
</div>
56+
)}
57+
58+
{/* Comments List Section */}
59+
<div className={showCommentForm ? "border-t border-border/60 pt-6" : ""}>
60+
<Suspense
61+
fallback={
62+
<div className="space-y-4">
63+
{/* Loading header */}
64+
<div className="flex items-center justify-between">
65+
<div className="flex items-center gap-2">
66+
<div className="h-5 w-5 rounded bg-muted animate-pulse"></div>
67+
<div className="h-5 w-24 rounded bg-muted animate-pulse"></div>
68+
</div>
69+
<div className="h-4 w-20 rounded bg-muted animate-pulse"></div>
70+
</div>
71+
72+
{/* Loading comments */}
73+
{[1, 2, 3].map((i) => (
74+
<div key={i} className="module-card animate-pulse">
75+
<div className="p-4">
76+
<div className="flex gap-3">
77+
<div className="h-10 w-10 rounded-full bg-muted"></div>
78+
<div className="flex-1 space-y-2">
79+
<div className="flex items-center gap-2">
80+
<div className="h-4 w-24 rounded bg-muted"></div>
81+
<div className="h-3 w-16 rounded bg-muted"></div>
82+
</div>
83+
<div className="space-y-1">
84+
<div className="h-4 w-full rounded bg-muted"></div>
85+
<div className="h-4 w-3/4 rounded bg-muted"></div>
86+
</div>
87+
</div>
88+
</div>
89+
</div>
90+
</div>
91+
))}
92+
</div>
93+
}
94+
>
95+
<CommentList
96+
showCommentForm={showCommentForm}
97+
onStartDiscussion={onStartDiscussion}
98+
/>
99+
</Suspense>
100+
</div>
101+
</div>
102+
);
103+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { FileText } from "lucide-react";
2+
import { type Segment } from "~/db/schema";
3+
import { MarkdownContent } from "~/routes/learn/-components/markdown-content";
4+
5+
interface ContentPanelProps {
6+
currentSegment: Segment;
7+
}
8+
9+
export function ContentPanel({ currentSegment }: ContentPanelProps) {
10+
if (currentSegment.content) {
11+
return (
12+
<div className="animate-fade-in">
13+
<MarkdownContent content={currentSegment.content} />
14+
</div>
15+
);
16+
}
17+
18+
return (
19+
<div className="text-center py-8 text-muted-foreground">
20+
<FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
21+
<p>No lesson content available for this segment.</p>
22+
</div>
23+
);
24+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { Button } from "~/components/ui/button";
2+
import { Trash2 } from "lucide-react";
3+
import { createServerFn } from "@tanstack/react-start";
4+
import { z } from "zod";
5+
import {
6+
AlertDialog,
7+
AlertDialogAction,
8+
AlertDialogCancel,
9+
AlertDialogContent,
10+
AlertDialogDescription,
11+
AlertDialogFooter,
12+
AlertDialogHeader,
13+
AlertDialogTitle,
14+
AlertDialogTrigger,
15+
} from "~/components/ui/alert-dialog";
16+
import { adminMiddleware } from "~/lib/auth";
17+
import { deleteSegmentUseCase } from "~/use-cases/segments";
18+
import { useToast } from "~/hooks/use-toast";
19+
20+
// TODO: there is a bug when trying to delet a segment
21+
export const deleteSegmentFn = createServerFn()
22+
.middleware([adminMiddleware])
23+
.validator(z.object({ segmentId: z.coerce.number() }))
24+
.handler(async ({ data }) => {
25+
await deleteSegmentUseCase(data.segmentId);
26+
});
27+
28+
interface DeleteVideoButtonProps {
29+
currentSegmentId: number;
30+
}
31+
32+
export function DeleteVideoButton({
33+
currentSegmentId,
34+
}: DeleteVideoButtonProps) {
35+
const { toast } = useToast();
36+
37+
const handleDeleteSegment = async () => {
38+
try {
39+
await deleteSegmentFn({ data: { segmentId: currentSegmentId } });
40+
toast({
41+
title: "Content deleted successfully!",
42+
description: "You will be redirected to the content list.",
43+
});
44+
} catch (error) {
45+
toast({
46+
title: "Failed to delete content",
47+
description: "Please try again.",
48+
variant: "destructive",
49+
});
50+
}
51+
};
52+
53+
return (
54+
<AlertDialog>
55+
<AlertDialogTrigger asChild>
56+
<Button
57+
variant="outline"
58+
className="btn-red-border-gradient px-4 py-2 flex items-center gap-2 text-sm font-medium rounded-md !bg-transparent !border-red-500"
59+
>
60+
<Trash2 className="h-4 w-4" />
61+
Delete
62+
</Button>
63+
</AlertDialogTrigger>
64+
<AlertDialogContent className="bg-background border border-border shadow-elevation-3 rounded-xl max-w-md mx-auto">
65+
<AlertDialogHeader className="space-y-4 p-6">
66+
<div className="flex items-center gap-3">
67+
<div className="p-2 rounded-lg bg-gradient-to-br from-red-100 to-red-200 dark:from-red-900 dark:to-red-800">
68+
<Trash2 className="h-5 w-5 text-red-600 dark:text-red-400" />
69+
</div>
70+
<AlertDialogTitle className="text-xl font-semibold text-foreground leading-tight">
71+
Are you absolutely sure?
72+
</AlertDialogTitle>
73+
</div>
74+
<AlertDialogDescription className="text-muted-foreground text-sm leading-relaxed">
75+
This action cannot be undone. This will permanently delete this
76+
content and all its associated files and attachments.
77+
</AlertDialogDescription>
78+
</AlertDialogHeader>
79+
<AlertDialogFooter className="flex gap-3 p-6 pt-0">
80+
<AlertDialogCancel className="px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-all duration-200 border border-border rounded-md hover:bg-muted">
81+
Cancel
82+
</AlertDialogCancel>
83+
<AlertDialogAction
84+
onClick={handleDeleteSegment}
85+
className="btn-gradient-red px-4 py-2 text-sm font-medium rounded-md"
86+
>
87+
Delete
88+
</AlertDialogAction>
89+
</AlertDialogFooter>
90+
</AlertDialogContent>
91+
</AlertDialog>
92+
);
93+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Link } from "@tanstack/react-router";
2+
import { Button } from "~/components/ui/button";
3+
import { Edit } from "lucide-react";
4+
import { type Segment } from "~/db/schema";
5+
6+
interface EditVideoButtonProps {
7+
currentSegment: Segment;
8+
}
9+
10+
export function EditVideoButton({ currentSegment }: EditVideoButtonProps) {
11+
return (
12+
<Link to="/learn/$slug/edit" params={{ slug: currentSegment.slug }}>
13+
<Button className="module-card px-4 py-2 flex items-center gap-2 text-sm font-medium text-theme-700 dark:text-theme-300 hover:text-theme-800 dark:hover:text-theme-200 transition-all duration-200 hover:shadow-elevation-3">
14+
<Edit className="h-4 w-4" />
15+
Edit
16+
</Button>
17+
</Link>
18+
);
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Link } from "@tanstack/react-router";
2+
import { MessageSquare } from "lucide-react";
3+
import { Button } from "~/components/ui/button";
4+
5+
export function FloatingFeedbackButton() {
6+
return (
7+
<Link to="/create-testimonial" className="fixed bottom-6 right-6 z-50">
8+
<Button
9+
size="lg"
10+
className="mt-4 module-card px-4 py-2 flex items-center gap-2 text-sm font-medium text-theme-700 dark:text-theme-300 hover:text-theme-800 dark:hover:text-theme-200 transition-all duration-200 hover:shadow-elevation-3"
11+
>
12+
<MessageSquare className="w-5 h-5 mr-2" />
13+
Leave a Testimonial
14+
</Button>
15+
</Link>
16+
);
17+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { Link } from "@tanstack/react-router";
2+
import { Badge } from "~/components/ui/badge";
3+
import { ArrowRight, CheckCircle, Lock, BookOpen } from "lucide-react";
4+
import { type Segment } from "~/db/schema";
5+
6+
interface UpgradePlaceholderProps {
7+
currentSegment: Segment;
8+
}
9+
10+
export function UpgradePlaceholder({
11+
currentSegment,
12+
}: UpgradePlaceholderProps) {
13+
return (
14+
<div className="max-w-5xl mx-auto space-y-6">
15+
{/* Premium Content Header */}
16+
<div className="module-card p-8 text-center">
17+
<div className="relative">
18+
{/* Background decoration */}
19+
<div className="absolute inset-0 bg-gradient-to-br from-amber-50/50 to-orange-50/50 dark:from-amber-950/20 dark:to-orange-950/20 rounded-xl"></div>
20+
21+
{/* Content */}
22+
<div className="py-8 relative space-y-6">
23+
{/* Premium badge and lock icon */}
24+
<div className="flex items-center justify-center space-x-4">
25+
<div className="p-4 rounded-full bg-gradient-to-br from-amber-100 to-orange-100 dark:from-amber-900 dark:to-orange-900 shadow-elevation-2">
26+
<Lock className="h-8 w-8 text-amber-600 dark:text-amber-400" />
27+
</div>
28+
<Badge
29+
variant="outline"
30+
className="bg-amber-50 dark:bg-amber-950 text-amber-700 dark:text-amber-300 border-amber-200 dark:border-amber-800 px-4 py-2 text-sm font-semibold"
31+
>
32+
PREMIUM CONTENT
33+
</Badge>
34+
</div>
35+
36+
{/* Title */}
37+
<div className="space-y-2">
38+
<h1 className="text-3xl font-bold text-foreground leading-tight">
39+
{currentSegment.title}
40+
</h1>
41+
<p className="text-lg text-muted-foreground">
42+
This lesson is part of our premium curriculum
43+
</p>
44+
</div>
45+
46+
{/* Description */}
47+
<div className="max-w-lg mx-auto space-y-4">
48+
<p className="text-muted-foreground leading-relaxed">
49+
Unlock access to this exclusive content and enhance your
50+
learning journey. Premium members get access to advanced topics,
51+
downloadable resources, and priority support.
52+
</p>
53+
54+
{/* Benefits list */}
55+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
56+
<div className="flex items-center gap-2 text-muted-foreground">
57+
<CheckCircle className="h-4 w-4 text-theme-500" />
58+
<span>Advanced content</span>
59+
</div>
60+
<div className="flex items-center gap-2 text-muted-foreground">
61+
<CheckCircle className="h-4 w-4 text-theme-500" />
62+
<span>Downloadable resources</span>
63+
</div>
64+
<div className="flex items-center gap-2 text-muted-foreground">
65+
<CheckCircle className="h-4 w-4 text-theme-500" />
66+
<span>Priority support</span>
67+
</div>
68+
<div className="flex items-center gap-2 text-muted-foreground">
69+
<CheckCircle className="h-4 w-4 text-theme-500" />
70+
<span>All future updates</span>
71+
</div>
72+
</div>
73+
</div>
74+
75+
{/* CTA Button */}
76+
<div className="space-y-3">
77+
<Link
78+
to="/purchase"
79+
className="inline-flex items-center gap-2 px-8 py-4 bg-gradient-to-r from-theme-500 to-theme-600 hover:from-theme-600 hover:to-theme-700 text-white font-semibold rounded-lg shadow-elevation-2 hover:shadow-elevation-3 transition-all duration-300 text-lg"
80+
>
81+
Upgrade to Premium
82+
<ArrowRight className="h-5 w-5" />
83+
</Link>
84+
<p className="text-xs text-muted-foreground">
85+
30-day money-back guarantee
86+
</p>
87+
</div>
88+
</div>
89+
</div>
90+
</div>
91+
92+
{/* Additional info card */}
93+
<div className="module-card p-6">
94+
<div className="flex items-start gap-4">
95+
<div className="p-2 rounded-lg bg-gradient-to-br from-theme-100 to-theme-200 dark:from-theme-900 dark:to-theme-800">
96+
<BookOpen className="h-5 w-5 text-theme-600 dark:text-theme-400" />
97+
</div>
98+
<div className="flex-1">
99+
<h3 className="font-semibold text-foreground mb-2">
100+
What you'll learn
101+
</h3>
102+
<p className="text-sm text-muted-foreground leading-relaxed">
103+
This premium lesson covers advanced concepts that will take your
104+
skills to the next level. Join thousands of developers who have
105+
upgraded their knowledge with our comprehensive curriculum.
106+
</p>
107+
</div>
108+
</div>
109+
</div>
110+
</div>
111+
);
112+
}

0 commit comments

Comments
 (0)