diff --git a/.gitignore b/.gitignore index 933241d..3157398 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,10 @@ yarn-error.log* .env*.local .env.production .env.development +.env +.env.example +.env.test +.env.template # vercel .vercel diff --git a/.husky/commit-msg b/.husky/commit-msg index d582379..7af31c7 100644 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,5 +1,5 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" +# #!/usr/bin/env sh +# . "$(dirname -- "$0")/_/husky.sh" # Validate commit message format npx --no -- commitlint --edit "$1" diff --git a/app/api/cron/update-alumni/route.ts b/app/api/cron/update-alumni/route.ts new file mode 100644 index 0000000..8f8f1e5 --- /dev/null +++ b/app/api/cron/update-alumni/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server'; +import { checkAndUpdateAlumniStatus } from '@/lib/supabase'; + +export async function GET() { + try { + await checkAndUpdateAlumniStatus(); + return NextResponse.json({ success: true, message: 'Alumni status updated successfully' }); + } catch (error) { + console.error('Error updating alumni status:', error); + return NextResponse.json( + { success: false, message: 'Failed to update alumni status' }, + { status: 500 } + ); + } +} diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..123746a --- /dev/null +++ b/app/blog/[slug]/page.tsx @@ -0,0 +1,185 @@ +import { client } from "@/sanity/lib/client"; +import { urlFor } from "@/sanity/lib/image"; +import { PortableText } from "@portabletext/react"; +import { PortableTextComponentProps } from "@portabletext/react"; +import { PortableTextBlock } from "@portabletext/types"; +import { format } from "date-fns"; +import Image from "next/image"; +import Link from "next/link"; +import { ArrowLeft } from "lucide-react"; +import { Post, PortableTextImageType, PortableTextProps } from "@/types/blog"; + +async function getPost(slug: string) { + const query = `*[_type == "post" && slug.current == $slug][0] { + _id, + title, + slug, + publishedAt, + mainImage, + content, + body, + "author": author->name, + "categories": categories[]->title + }`; + + return client.fetch(query, { slug }) as Promise; +} + +const PortableTextComponents = { + types: { + image: ({ value }: PortableTextImageType) => { + if (!value?.asset?._ref) { + return null; + } + return ( +
+
+ {value.alt +
+ {value.caption && ( +
+ {value.caption} +
+ )} +
+ ); + }, + }, + block: { + h1: ({ children }: PortableTextComponentProps) => ( +

+ {children} +

+ ), + h2: ({ children }: PortableTextComponentProps) => ( +

+ {children} +

+ ), + normal: ({ children }: PortableTextComponentProps) => ( +

{children}

+ ), + blockquote: ({ children }: PortableTextComponentProps) => ( +
+ {children} +
+ ), + }, + marks: { + highlight: ({ children }: PortableTextProps) => ( + + {children} + + ), + link: ({ children, value }: PortableTextProps) => { + const rel = value?.href?.startsWith('/') ? undefined : 'noreferrer noopener'; + return ( + + {children} + + ); + }, + }, +}; + +export default async function BlogPost({ + params, +}: { + params: { slug: string }; +}) { + const post = await getPost(params.slug); + + if (!post) { + return
Post not found
; + } + + return ( +
+ {/* Navbar */} + + +
+
+ {post.mainImage && ( +
+ {post.title} +
+ )} +

+ {post.title} +

+
+ {post.author} + + +
+
+ +
+ {/* */} + {post.body && ( + + )} +
+
+
+ ); +} diff --git a/app/blog/components/BlogList.tsx b/app/blog/components/BlogList.tsx new file mode 100644 index 0000000..2290f4f --- /dev/null +++ b/app/blog/components/BlogList.tsx @@ -0,0 +1,65 @@ +import Image from "next/image"; +import Link from "next/link"; +import { urlFor } from "@/sanity/lib/image"; +import { format } from "date-fns"; +import { Badge } from "@/components/ui/badge"; +import { Post } from "@/types/blog"; + +export default function BlogList({ posts }: { posts: Post[] }) { + return ( +
+ {posts.map((post) => ( + +
+
+
+ {post.mainImage && ( +
+ {post.title} +
+ )} +
+
+
+ {post.categories.map((category) => ( + + {category} + + ))} +
+

+ {post.title} +

+

+ {post.excerpt} +

+
+ {post.author} + + +
+
+
+
+
+ + ))} +
+ ); +} diff --git a/app/blog/page.tsx b/app/blog/page.tsx new file mode 100644 index 0000000..c368218 --- /dev/null +++ b/app/blog/page.tsx @@ -0,0 +1,126 @@ +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; +import { client } from "@/sanity/lib/client"; +import BlogList from "./components/BlogList"; +import { Meteors } from "@/components/ui/meteors"; +import { Badge } from "@/components/ui/badge"; +import Navbar from "@/components/Navbar"; + +async function getPosts() { + const query = `*[_type == "post"] | order(publishedAt desc) { + _id, + title, + slug, + publishedAt, + excerpt, + mainImage, + "author": author->name, + "categories": categories[]->title + }`; + + return client.fetch(query); +} + +export default async function BlogPage() { + const session = await getServerSession(); + + if (!session) { + redirect("/auth/signin?callbackUrl=/blog"); + } + + const posts = await getPosts(); + + return ( +
+ +
+ +
+ +
+
+
+
+ + Student Resources + + + Tech Events + + + Community + +
+ +

+ + Knowledge Hub + + + + Knowledge Hub + + + + +

+ +

+ Discover events, resources, and insights to fuel your tech journey +

+ +
+
+ + Latest Events +
+
+ + Student Resources +
+
+
+
+
+ +
+
+
+
+
+ Featured Content +
+
+ +
+
+
+
+

+ Latest Articles +

+

+ Stay updated with the latest tech trends, events, and student resources +

+
+
+
+ {[1, 2, 3].map((i) => ( +
+
+
+ ))} +
+ Join 1000+ readers +
+
+ + +
+
+
+ ); +} diff --git a/app/dashboard/[domain]/page.tsx b/app/dashboard/[domain]/page.tsx index b56fe5c..f6cccae 100644 --- a/app/dashboard/[domain]/page.tsx +++ b/app/dashboard/[domain]/page.tsx @@ -1,30 +1,61 @@ "use client"; -// import { useSession } from "next-auth/react"; import { useParams } from "next/navigation"; -import { domains } from "@/config/navigation"; import { motion } from "framer-motion"; import { Card } from "@/components/ui/card"; import { ScrollArea } from "@/components/ui/scroll-area"; import ResourceTable from "@/components/ResourceTable"; +import { useEffect, useState } from "react"; +import { getDomainBySlug, getSubjectsByDomain, type Domain, type Subject } from "@/lib/supabase-resources"; +import { Code, Database, FileText, Video, LucideIcon } from "lucide-react"; -interface Domain { - name: string; - resources: string; - icon: React.ComponentType; - gradient: string; - description: string; - categories: string[]; - } +const domainIcons: { [key: string]: LucideIcon } = { + 'web-development': Code, + 'machine-learning': Database, + 'mobile-development': FileText, + 'cloud-computing': Video, +}; export default function DomainPage() { -// const { data: session } = useSession(); const params = useParams(); - const currentDomain = domains.find( - (d: Domain) => d.resources === params.domain - ); + const [domain, setDomain] = useState(null); + const [subjects, setSubjects] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchDomainData = async () => { + try { + setLoading(true); + const domainSlug = params.domain as string; + const [domainData, subjectsData] = await Promise.all([ + getDomainBySlug(domainSlug), + getSubjectsByDomain(domainSlug) + ]); + setDomain(domainData); + setSubjects(subjectsData); + } catch (error) { + console.error('Error fetching domain data:', error); + } finally { + setLoading(false); + } + }; + + if (params.domain) { + fetchDomainData(); + } + }, [params.domain]); + + if (loading) { + return ( +
+
+
+ ); + } + + if (!domain) return null; - if (!currentDomain) return null; + const DomainIcon = domainIcons[domain.slug] || Code; return (
@@ -36,10 +67,10 @@ export default function DomainPage() { {/* Welcome Section */}

- {currentDomain.name} + {domain.name}

- Browse through {currentDomain.name.toLowerCase()} resources curated for VJTI students. + Browse through {domain.name.toLowerCase()} resources curated for VJTI students.

@@ -47,15 +78,15 @@ export default function DomainPage() {
-
- +
+

Resources

- {currentDomain.description} + {domain.description}

@@ -63,18 +94,17 @@ export default function DomainPage() {
- {currentDomain.categories.map((category: string) => ( -
+ {subjects.map((subject) => ( +

- {category} + {subject.name}

@@ -85,4 +115,4 @@ export default function DomainPage() {
); -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index dab6750..5e0d61c 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,21 +1,94 @@ "use client"; import { useSession } from "next-auth/react"; -import { usePathname } from "next/navigation"; +// import { usePathname } from "next/navigation"; import { motion } from "framer-motion"; import { Card } from "@/components/ui/card"; -// import { ScrollArea } from "@/components/ui/scroll-area"; -// import ResourceTable from "@/components/ResourceTable"; -import { domains } from "@/config/navigation"; -import Link from "next/link"; +import { useEffect, useState } from "react"; +import { getDomains, getSubjectsByDomain, getResourcesBySubject, type Domain, type Subject, type Resource } from "@/lib/supabase-resources"; +// import Link from "next/link"; import { cn } from "@/lib/utils"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Badge } from "@/components/ui/badge"; +import { BookOpen, Code, FileText, Video } from "lucide-react"; export default function Dashboard() { const { data: session } = useSession(); - const pathname = usePathname(); - const currentDomain = domains.find( - domain => `/dashboard/${domain.resources}` === pathname - ); + // const pathname = usePathname(); + const [domains, setDomains] = useState([]); + const [subjects, setSubjects] = useState([]); + const [resources, setResources] = useState([]); + const [selectedDomain, setSelectedDomain] = useState(""); + const [selectedSubject, setSelectedSubject] = useState(""); + const [loading, setLoading] = useState(true); + + const fetchData = async () => { + try { + const domainsData = await getDomains(); + setDomains(domainsData); + if (domainsData.length > 0) { + setSelectedDomain(domainsData[0].slug); + } + } catch (error) { + console.error("Error fetching domains:", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, []); + + const fetchSubjects = async () => { + try { + const subjectsData = await getSubjectsByDomain(selectedDomain); + setSubjects(subjectsData); + if (subjectsData.length > 0) { + setSelectedSubject(subjectsData[0].id); + } + } catch (error) { + console.error("Error fetching subjects:", error); + } + }; + + useEffect(() => { + if (selectedDomain) { + fetchSubjects(); + } + }, [selectedDomain]); + + const fetchResources = async () => { + try { + const resourcesData = await getResourcesBySubject(selectedSubject); + setResources(resourcesData); + } catch (error) { + console.error("Error fetching resources:", error); + } + }; + + useEffect(() => { + if (selectedSubject) { + fetchResources(); + } + }, [selectedSubject]); + + const getResourceIcon = (type: string) => { + switch (type) { + case 'video': + return