diff --git a/.gitignore b/.gitignore index 24bbfa9..db4a4d0 100755 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,10 @@ node_modules/ .github/instructions/ .github/prompts/ .github/chatmodes/ +.github/implementation_processings/ + +# Playwright MCP folder +.playwright-mcp/ # Logs npm-debug.log* diff --git a/frontend/.env b/frontend/.env deleted file mode 100755 index 99d0012..0000000 --- a/frontend/.env +++ /dev/null @@ -1,2 +0,0 @@ -VITE_SUPABASE_URL=https://cvujbyajfhxsdtonlurw.supabase.co -VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImN2dWpieWFqZmh4c2R0b25sdXJ3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTM2MzUzNjEsImV4cCI6MjA2OTIxMTM2MX0.ZPoNBW15TRnEZe-MrRbKlRuDqaTH9Ch3R7VLqASnFZ8 diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf3..3b0b403 100755 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index ed2290a..88c24f8 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "date-fns": "^4.1.0", "input-otp": "^1.4.2", "lucide-react": "^0.540.0", "next-themes": "^0.4.6", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f4be4cf..5947057 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,9 @@ import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; import Dashboard from "@/pages/Dashboard"; +import Library from "@/pages/Library"; +import Collections from "@/pages/Collections"; +import CollectionDetail from "@/pages/CollectionDetail"; +import Settings from "@/pages/Settings"; import ResourceDetail from "@/pages/ResourceDetail"; import SignUp from "@/pages/SignUp"; import SignIn from "@/pages/SignIn"; @@ -43,6 +47,46 @@ export default function App() { } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - - - - Add New Resource - - - Paste a YouTube video or article link to add it to your collection - - - -
-
-
- - setUrl(e.target.value)} - placeholder="Resource Link" - className="pl-10 bg-background focus:bg-background placeholder:text-card-foreground/70" - disabled={isLoading} - /> +
+ + + + + Add New Resource + + + Paste a YouTube video or article link to add it to your collection + + + + +
+
+ + setUrl(e.target.value)} + placeholder="Resource Link" + className="pl-10 bg-background focus:bg-background placeholder:text-card-foreground/70" + disabled={isLoading} + /> +
+ {url && !isValidUrl(url) && ( +

+ Please enter a valid URL +

+ )}
- {url && !isValidUrl(url) && ( -

- Please enter a valid URL -

- )} -
-
- +
+ - -
- - - + +
+ + + +
); } diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx index b620c39..b8be91d 100755 --- a/frontend/src/components/app-sidebar.tsx +++ b/frontend/src/components/app-sidebar.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { LayoutDashboard } from "lucide-react"; +import { LayoutDashboard, Library, FolderOpen } from "lucide-react"; import { NavMain } from "@/components/nav-main"; import { NavUser } from "@/components/nav-user"; @@ -22,32 +22,33 @@ const data = { items: [ { title: "Overview", - url: "#", + url: "/dashboard", }, + ], + }, + { + title: "Library", + url: "#", + icon: Library, + items: [ + { + title: "My Resources", + url: "/library", + }, + ], + }, + { + title: "Collections", + url: "#", + icon: FolderOpen, + items: [ { - title: "Add Resource", - url: "#", + title: "My Collections", + url: "/collections", }, ], }, ], - // projects: [ - // { - // name: "Knowledge Base", - // url: "#", - // icon: Frame, - // }, - // { - // name: "Analytics", - // url: "#", - // icon: PieChart, - // }, - // { - // name: "Resources", - // url: "#", - // icon: Map, - // }, - // ], }; export function AppSidebar({ ...props }: React.ComponentProps) { diff --git a/frontend/src/components/collection-color-dot.tsx b/frontend/src/components/collection-color-dot.tsx new file mode 100644 index 0000000..1fb3335 --- /dev/null +++ b/frontend/src/components/collection-color-dot.tsx @@ -0,0 +1,64 @@ +import { cn } from "@/lib/utils"; + +interface CollectionColorDotProps { + color: string; + size?: "sm" | "md" | "lg"; + selected?: boolean; + className?: string; + onClick?: () => void; + title?: string; +} + +const colorOptions = [ + { value: "#3B82F6", name: "Blue", border: "#2563EB" }, + { value: "#10B981", name: "Green", border: "#059669" }, + { value: "#8B5CF6", name: "Purple", border: "#7C3AED" }, + { value: "#F59E0B", name: "Orange", border: "#D97706" }, + { value: "#EF4444", name: "Red", border: "#DC2626" }, + { value: "#6B7280", name: "Gray", border: "#4B5563" }, +]; + +const getBorderColor = (mainColor: string): string => { + const colorOption = colorOptions.find((option) => option.value === mainColor); + return colorOption ? colorOption.border : mainColor; +}; + +const sizeClasses = { + sm: "w-4 h-4", + md: "w-6 h-6", + lg: "w-8 h-8", +}; + +export function CollectionColorDot({ + color, + size = "md", + selected = false, + className, + onClick, + title, +}: CollectionColorDotProps) { + const baseClasses = + "rounded-full border-2 max-md:border-[1px] transition-all duration-200"; + const sizeClass = sizeClasses[size]; + const selectedClasses = selected ? "border-foreground shadow-md" : ""; + + return ( +
+ ); +} + +export { colorOptions, getBorderColor }; diff --git a/frontend/src/components/compact-resource-card.tsx b/frontend/src/components/compact-resource-card.tsx new file mode 100755 index 0000000..1bce3cf --- /dev/null +++ b/frontend/src/components/compact-resource-card.tsx @@ -0,0 +1,114 @@ +import { FileText } from "lucide-react"; +import YouTubeIcon from "@/components/icons/youtube"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import type { Resource } from "@/types/resource"; + +interface CompactResourceCardProps { + resource: Resource; +} + +export function CompactResourceCard({ resource }: CompactResourceCardProps) { + const getContentTypeIcon = () => { + return resource.content_type === "youtube" ? ( + + ) : ( + + ); + }; + + const getTagColor = (tag: string) => { + // Simple hash function to get consistent colors for tags + const hash = tag.split("").reduce((a, b) => { + a = (a << 5) - a + b.charCodeAt(0); + return a & a; + }, 0); + + const colors = [ + "bg-blue-100 text-blue-800", + "bg-green-100 text-green-800", + "bg-yellow-100 text-yellow-800", + "bg-red-100 text-red-800", + "bg-purple-100 text-purple-800", + "bg-pink-100 text-pink-800", + "bg-indigo-100 text-indigo-800", + ]; + + return colors[Math.abs(hash) % colors.length]; + }; + + const truncateSummary = (summary: string, maxLength: number = 80) => { + if (summary.length <= maxLength) return summary; + return summary.substring(0, maxLength) + "..."; + }; + + const imageAlt = resource.title + ? `${resource.title} thumbnail` + : "Image unavailable"; + + return ( + +
+ {resource.thumbnail_link ? ( + {imageAlt} + ) : ( + + {imageAlt} + + )} +
+ + + {/* min-w-0 here allows the title to actually truncate within a flex row */} +
+
{getContentTypeIcon()}
+ + {resource.title} + +
+
+ + +
+

+ {truncateSummary(resource.summary, 200)} +

+
+ +
+ {/* min-w-0 avoids tag row forcing a wider min-content size */} +
+ {resource.tags.slice(0, 2).map((tag, index) => ( + + {tag} + + ))} + + {resource.tags.length > 2 && ( + + +{resource.tags.length - 2} + + )} + + {/* Gradient fade effect for overflowing tags */} +
+
+
+ + + ); +} diff --git a/frontend/src/components/compact-resource-filters.tsx b/frontend/src/components/compact-resource-filters.tsx index 1544af1..b0180f9 100755 --- a/frontend/src/components/compact-resource-filters.tsx +++ b/frontend/src/components/compact-resource-filters.tsx @@ -49,7 +49,7 @@ export function CompactResourceFilters({
-

Your Resources

-

+

My Resources

+

{resultCount}{" "} {resultCount === 1 ? "resource found" : "resources found"}

@@ -85,7 +85,7 @@ export function CompactResourceFilters({ {/* Content Type */} { + onValueChange={(value: string) => { const [sortBy, sortOrder] = value.split("-"); - onFiltersChange({ ...filters, sortBy, sortOrder }); + onFiltersChange({ + ...filters, + sortBy: sortBy as FilterOptions["sortBy"], + sortOrder: sortOrder as FilterOptions["sortOrder"], + }); }} > diff --git a/frontend/src/components/mindly-logo.tsx b/frontend/src/components/mindly-logo.tsx deleted file mode 100755 index 4562cb0..0000000 --- a/frontend/src/components/mindly-logo.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import mindlyIcon from "@/assets/mindly-icon-white.svg"; -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/components/ui/sidebar"; - -export function MindleyLogo() { - return ( - - - -
- Mindley -
-
- Mindley - AI-Powered Platform -
-
-
-
- ); -} diff --git a/frontend/src/components/nav-main.tsx b/frontend/src/components/nav-main.tsx index aaf6549..730d661 100755 --- a/frontend/src/components/nav-main.tsx +++ b/frontend/src/components/nav-main.tsx @@ -1,4 +1,6 @@ import { ChevronRight, type LucideIcon } from "lucide-react"; +import { useState } from "react"; +import { useLocation } from "react-router-dom"; import { Collapsible, @@ -32,47 +34,63 @@ export function NavMain({ }[]; }) { const { isMobile, setOpenMobile } = useSidebar(); + const location = useLocation(); + + const [openKey, setOpenKey] = useState( + () => items.find((i) => i.isActive)?.title ?? null + ); + return ( Platform - {items.map((item) => ( - - - - - {item.icon && } - {item.title} - - - - - - {item.items?.map((subItem) => ( - - - { - // on mobile, close the sidebar when navigating - if (isMobile) setOpenMobile(false); - }} - > - {subItem.title} - - - - ))} - - - - - ))} + {items.map((item) => { + const isOpen = openKey === item.title; + return ( + setOpenKey(open ? item.title : null)} + className="group/collapsible" + > + + + + {item.icon && } + {item.title} + + + + + + {item.items?.map((subItem) => { + const isCurrentPage = location.pathname === subItem.url; + return ( + + + { + // on mobile, close the sidebar when navigating + if (isMobile) setOpenMobile(false); + }} + > + {subItem.title} + + + + ); + })} + + + + + ); + })} ); diff --git a/frontend/src/components/nav-user.tsx b/frontend/src/components/nav-user.tsx index ab08eae..526da4e 100755 --- a/frontend/src/components/nav-user.tsx +++ b/frontend/src/components/nav-user.tsx @@ -1,4 +1,4 @@ -import { BadgeCheck, Bell, ChevronsUpDown, LogOut } from "lucide-react"; +import { BadgeCheck, Settings, ChevronsUpDown, LogOut } from "lucide-react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { @@ -18,11 +18,13 @@ import { } from "@/components/ui/sidebar"; import { useAuth } from "@/hooks/use-auth"; import { useToast } from "@/hooks/use-toast"; +import { useNavigate } from "react-router-dom"; export function NavUser() { const { isMobile } = useSidebar(); const { user, signOut } = useAuth(); const { toast } = useToast(); + const navigate = useNavigate(); const handleSignOut = async () => { try { @@ -40,6 +42,15 @@ export function NavUser() { } }; + const handleAccountClick = () => { + // Navigate to account settings or show account modal + navigate("/settings/account"); + }; + + const handleSettingsClick = () => { + navigate("/settings"); + }; + if (!user) { return null; } @@ -121,17 +132,13 @@ export function NavUser() { - + Account - {/* - - Billing - */} - - - Notifications + + + Settings diff --git a/frontend/src/components/recent-resources.tsx b/frontend/src/components/recent-resources.tsx new file mode 100755 index 0000000..f816c68 --- /dev/null +++ b/frontend/src/components/recent-resources.tsx @@ -0,0 +1,68 @@ +import { Link } from "react-router-dom"; +import { ResourceCard } from "@/components/resource-card"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import type { Resource } from "@/types/resource"; + +interface RecentResourcesProps { + recentResources: Resource[]; + onViewDetails: (id: string) => void; +} + +export function RecentResources({ + recentResources, + onViewDetails, +}: RecentResourcesProps) { + return ( +
+
+
+

Recent Resources

+

+ Your latest additions to the library +

+
+ +
+ + {recentResources.length === 0 ? ( + + +
+ + + +
+

No resources yet

+

+ Start by adding your first resource using the form above. + Resources will appear here once processed. +

+
+
+ ) : ( +
+ {recentResources.map((resource) => ( + + ))} +
+ )} +
+ ); +} diff --git a/frontend/src/components/resource-card.tsx b/frontend/src/components/resource-card.tsx index 5d73421..13c5fef 100755 --- a/frontend/src/components/resource-card.tsx +++ b/frontend/src/components/resource-card.tsx @@ -1,4 +1,4 @@ -import { Calendar, ExternalLink, FileText, User } from "lucide-react"; +import { Calendar, ExternalLink, FileText, User, Trash2 } from "lucide-react"; import YouTubeIcon from "@/components/icons/youtube"; import { useEffect, useRef, useState } from "react"; import { Badge } from "@/components/ui/badge"; @@ -16,9 +16,16 @@ import type { Resource } from "@/types/resource"; interface ResourceCardProps { resource: Resource; onViewDetails: (id: string) => void; + showRemoveButton?: boolean; + onRemove?: () => void; } -export function ResourceCard({ resource, onViewDetails }: ResourceCardProps) { +export function ResourceCard({ + resource, + onViewDetails, + showRemoveButton, + onRemove, +}: ResourceCardProps) { const tagsContainerRef = useRef(null); const tagRefs = useRef>([]); const [forceNewLine, setForceNewLine] = useState(false); @@ -98,7 +105,7 @@ export function ResourceCard({ resource, onViewDetails }: ResourceCardProps) { : "Image unavailable"; return ( - +
{resource.thumbnail_link ? (
- +
+ + {showRemoveButton && onRemove && ( + + )} +
diff --git a/frontend/src/components/resource-filters.tsx b/frontend/src/components/resource-filters.tsx index 6d915b2..cd5961d 100755 --- a/frontend/src/components/resource-filters.tsx +++ b/frontend/src/components/resource-filters.tsx @@ -86,7 +86,7 @@ export function ResourceFilters({ + onValueChange={(value: FilterOptions["sortBy"]) => onFiltersChange({ ...filters, sortBy: value }) } > @@ -124,7 +124,7 @@ export function ResourceFilters({ + ); + } +); +FloatingInput.displayName = "FloatingInput"; + +const FloatingLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( +