diff --git a/backend/app/models/video.py b/backend/app/models/video.py
index 3efc52e..e8c6cff 100644
--- a/backend/app/models/video.py
+++ b/backend/app/models/video.py
@@ -1,4 +1,4 @@
-from typing import Optional
+from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict
@@ -10,6 +10,7 @@ class VideoHighlight(BaseModel):
date: str
video_url: Optional[str] = None
thumbnail_url: Optional[str] = None
+ video_type: Optional[Literal["regular", "short"]] = "regular"
model_config = ConfigDict(
json_schema_extra={
@@ -19,6 +20,7 @@ class VideoHighlight(BaseModel):
"date": "2025-11-08",
"video_url": "https://youtube.com/watch?v=...",
"thumbnail_url": "https://example.com/thumbnail.jpg",
+ "video_type": "regular",
}
}
)
diff --git a/backend/app/services/video_service.py b/backend/app/services/video_service.py
index 1cf3d93..1d57dd3 100644
--- a/backend/app/services/video_service.py
+++ b/backend/app/services/video_service.py
@@ -22,8 +22,9 @@ def get_all_videos(self) -> List[VideoHighlight]:
"date": 1,
"video_url": 1,
"thumbnail_url": 1,
+ "video_type": 1,
},
- )
+ ).sort("date", -1)
)
results: List[VideoHighlight] = []
for d in docs:
@@ -34,6 +35,7 @@ def get_all_videos(self) -> List[VideoHighlight]:
date=d.get("date"),
video_url=d.get("video_url"),
thumbnail_url=d.get("thumbnail_url"),
+ video_type=d.get("video_type", "regular"),
)
results.append(vid)
return results
@@ -52,6 +54,7 @@ def get_video_by_id(self, video_id: str) -> Optional[VideoHighlight]:
"date": 1,
"video_url": 1,
"thumbnail_url": 1,
+ "video_type": 1,
},
)
if not doc:
@@ -63,6 +66,7 @@ def get_video_by_id(self, video_id: str) -> Optional[VideoHighlight]:
date=doc.get("date"),
video_url=doc.get("video_url"),
thumbnail_url=doc.get("thumbnail_url"),
+ video_type=doc.get("video_type", "regular"),
)
doc = self.videos_collection.find_one(
@@ -74,6 +78,7 @@ def get_video_by_id(self, video_id: str) -> Optional[VideoHighlight]:
"date": 1,
"video_url": 1,
"thumbnail_url": 1,
+ "video_type": 1,
},
)
if not doc:
@@ -85,6 +90,7 @@ def get_video_by_id(self, video_id: str) -> Optional[VideoHighlight]:
date=doc.get("date"),
video_url=doc.get("video_url"),
thumbnail_url=doc.get("thumbnail_url"),
+ video_type=doc.get("video_type", "regular"),
)
def create_video(self, video: VideoHighlight) -> VideoHighlight:
diff --git a/src/components/admin/VideoManagement.tsx b/src/components/admin/VideoManagement.tsx
index 64c55c2..c3d1ff9 100644
--- a/src/components/admin/VideoManagement.tsx
+++ b/src/components/admin/VideoManagement.tsx
@@ -5,6 +5,13 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Card } from "@/components/ui/card";
import { Plus, Trash, Pencil, VideoCamera } from "@phosphor-icons/react";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
import {
AlertDialog,
AlertDialogTrigger,
@@ -33,6 +40,7 @@ export default function VideoManagement() {
date: new Date().toISOString().split("T")[0],
videoUrl: "",
thumbnailUrl: "",
+ videoType: "regular" as "regular" | "short",
});
// Site links editable in admin. Initialize with stable defaults to avoid
@@ -51,6 +59,7 @@ export default function VideoManagement() {
date: new Date().toISOString().split("T")[0],
videoUrl: "",
thumbnailUrl: "",
+ videoType: "regular",
});
setEditingId(null);
};
@@ -95,6 +104,7 @@ export default function VideoManagement() {
date: video.date,
videoUrl: video.videoUrl,
thumbnailUrl: video.thumbnailUrl || "",
+ videoType: video.videoType || "regular",
});
setEditingId(video.id);
};
@@ -121,6 +131,7 @@ export default function VideoManagement() {
date: formData.date,
videoUrl: formData.videoUrl,
thumbnailUrl: formData.thumbnailUrl || undefined,
+ videoType: formData.videoType,
});
toast.success("Video updated successfully");
} else {
@@ -130,6 +141,7 @@ export default function VideoManagement() {
date: formData.date,
videoUrl: formData.videoUrl,
thumbnailUrl: formData.thumbnailUrl || undefined,
+ videoType: formData.videoType,
});
toast.success("Video added successfully");
}
@@ -271,20 +283,38 @@ export default function VideoManagement() {
/>
-
-
- setFormData({ ...formData, videoUrl: e.target.value })
+
+
+
+
+
+ setFormData({ ...formData, videoUrl: e.target.value })
+ }
+ placeholder="https://youtube.com/watch?v=..."
+ required
+ />
+
+
({});
+ // Pagination state for regular videos
+ const [currentPage, setCurrentPage] = useState(0);
+ const videosPerPage = 4;
+
+ // State for shorts section
+ const [currentShortIndex, setCurrentShortIndex] = useState(0);
+ const shortsContainerRef = useRef
(null);
+ const [touchStart, setTouchStart] = useState(0);
+
useEffect(() => {
const load = async () => {
try {
@@ -48,16 +64,24 @@ export default function VideosPage() {
load();
}, []);
- const getEmbedUrl = (url: string) => {
+ const getEmbedUrl = (url: string, autoplay = false) => {
if (url.includes("youtube.com") || url.includes("youtu.be")) {
const videoId = url.includes("youtu.be")
? url.split("youtu.be/")[1]?.split("?")[0]
: url.split("v=")[1]?.split("&")[0];
- return `https://www.youtube.com/embed/${videoId}`;
+ return `https://www.youtube.com/embed/${videoId}${autoplay ? "?autoplay=1&mute=1&loop=1&playlist=" + videoId : ""}`;
}
return url;
};
+ const getShortsEmbedUrl = (url: string) => {
+ if (url.includes("youtube.com/shorts/")) {
+ const videoId = url.split("shorts/")[1]?.split("?")[0];
+ return `https://www.youtube.com/embed/${videoId}?autoplay=1&mute=1&loop=1&playlist=${videoId}`;
+ }
+ return getEmbedUrl(url, true);
+ };
+
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("pt-PT", {
month: "long",
@@ -104,6 +128,53 @@ export default function VideosPage() {
);
}
+ // Separate videos by type and sort by date (most recent first)
+ const regularVideos = videos
+ .filter((v) => !v.videoType || v.videoType === "regular")
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+
+ const shorts = videos
+ .filter((v) => v.videoType === "short")
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+
+ // Pagination for regular videos
+ const totalPages = Math.ceil(regularVideos.length / videosPerPage);
+ const startIndex = currentPage * videosPerPage;
+ const endIndex = startIndex + videosPerPage;
+ const currentVideos = regularVideos.slice(startIndex, endIndex);
+
+ // Handlers for shorts navigation
+ const handlePrevShort = () => {
+ if (currentShortIndex > 0) {
+ setCurrentShortIndex(currentShortIndex - 1);
+ }
+ };
+
+ const handleNextShort = () => {
+ if (currentShortIndex < shorts.length - 1) {
+ setCurrentShortIndex(currentShortIndex + 1);
+ }
+ };
+
+ // Touch handlers for mobile swipe
+ const handleTouchStart = (e: React.TouchEvent) => {
+ setTouchStart(e.touches[0].clientY);
+ };
+
+ const handleTouchEnd = (e: React.TouchEvent) => {
+ const touchEnd = e.changedTouches[0].clientY;
+ const diff = touchStart - touchEnd;
+
+ // Swipe down (diff < 0) goes to previous, swipe up (diff > 0) goes to next
+ if (Math.abs(diff) > 50) {
+ if (diff > 0) {
+ handleNextShort();
+ } else {
+ handlePrevShort();
+ }
+ }
+ };
+
return (
@@ -116,50 +187,175 @@ export default function VideosPage() {
-
- {videos.map((video) => (
-
setSelectedVideo(video)}
- >
-
- {video.thumbnailUrl ? (
-

- ) : (
-
- )}
-
-
+
+ {/* Left side: Regular videos in 2x2 grid */}
+
+
+
Vídeos
+ {totalPages > 1 && (
+
+
+
+ {currentPage + 1} / {totalPages}
+
+
+ )}
+
+
+ {regularVideos.length === 0 ? (
+
+ Ainda sem vídeos regulares
+
+ ) : (
+
+ {currentVideos.map((video) => (
+
setSelectedVideo(video)}
+ >
+
+ {video.thumbnailUrl ? (
+

+ ) : (
+
+ )}
+
+
+
+
+ {video.title}
+
+
+
+ {video.description}
+
+
+
+ {formatDate(video.date)}
+
+
+
+ ))}
-
-
- {video.title}
-
-
-
- {video.description}
-
-
-
- {formatDate(video.date)}
-
+ )}
+
+
+ {/* Right side: Shorts section */}
+
+
+
+
Shorts
+ {shorts.length > 1 && (
+
+ {currentShortIndex + 1} / {shorts.length}
+
+ )}
-
- ))}
+
+ {shorts.length === 0 ? (
+
+ Ainda sem shorts
+
+ ) : (
+
+
+
+
+
+
+ {/* Navigation buttons for desktop - on the right side */}
+
+
+
+
+
+
+
+
+ {shorts[currentShortIndex].title}
+
+
+ {shorts[currentShortIndex].description}
+
+
+
+ {formatDate(shorts[currentShortIndex].date)}
+
+
+
+ {/* Swipe hint for mobile */}
+
+ Deslize para cima/baixo para navegar
+
+
+ )}
+
+