Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ yarn-error.log*
.env*.local
.env.production
.env.development
.env
.env.example
.env.test
.env.template

# vercel
.vercel
Expand Down
4 changes: 2 additions & 2 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
15 changes: 15 additions & 0 deletions app/api/cron/update-alumni/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
);
}
}
185 changes: 185 additions & 0 deletions app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Post>;
}

const PortableTextComponents = {
types: {
image: ({ value }: PortableTextImageType) => {
if (!value?.asset?._ref) {
return null;
}
return (
<figure className="my-8">
<div className="relative w-full h-[500px] rounded-lg overflow-hidden">
<Image
src={urlFor(value).url()}
alt={value.alt || "Blog post image"}
fill
className="object-contain bg-gray-50"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
{value.caption && (
<figcaption className="mt-2 text-center text-sm text-gray-500 italic">
{value.caption}
</figcaption>
)}
</figure>
);
},
},
block: {
h1: ({ children }: PortableTextComponentProps<PortableTextBlock>) => (
<h1 className="text-4xl font-bold my-8 bg-gradient-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
{children}
</h1>
),
h2: ({ children }: PortableTextComponentProps<PortableTextBlock>) => (
<h2 className="text-3xl font-semibold my-6 bg-gradient-to-r from-green-400 to-green-600 bg-clip-text text-transparent">
{children}
</h2>
),
normal: ({ children }: PortableTextComponentProps<PortableTextBlock>) => (
<p className="text-gray-700 leading-relaxed mb-6">{children}</p>
),
blockquote: ({ children }: PortableTextComponentProps<PortableTextBlock>) => (
<blockquote className="border-l-4 border-green-500 pl-4 my-6 italic text-gray-700">
{children}
</blockquote>
),
},
marks: {
highlight: ({ children }: PortableTextProps) => (
<span className="bg-gradient-to-r from-green-200 to-green-300 px-1 rounded">
{children}
</span>
),
link: ({ children, value }: PortableTextProps) => {
const rel = value?.href?.startsWith('/') ? undefined : 'noreferrer noopener';
return (
<a
href={value?.href}
rel={rel}
className="text-green-500 hover:text-green-600 underline transition-colors"
>
{children}
</a>
);
},
},
};

export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);

if (!post) {
return <div>Post not found</div>;
}

return (
<div className="min-h-screen bg-white">
{/* Navbar */}
<nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200">
<div className="container mx-auto px-4">
<div className="h-16 flex items-center justify-between">
<div className="flex items-center gap-8">
<Link
href="/blog"
className="flex items-center gap-2 text-gray-600 hover:text-green-500 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
<span>Back to Blog</span>
</Link>
<div className="h-6 w-px bg-gray-200"></div>
<Link
href="/"
className="text-xl font-bold bg-gradient-to-r from-green-400 to-green-600 bg-clip-text text-transparent"
>
<span className="flex items-center gap-2">
<Image
src="/coc_vjti.jpeg"
alt="Logo"
width={32}
height={32}
className="w-8 h-8"
/>
CoC
</span>
</Link>
</div>
<div className="flex items-center gap-4">
{post.categories?.map((category: string) => (
<span
key={category}
className="px-3 py-1 text-sm text-green-600 bg-green-50 rounded-full"
>
{category}
</span>
))}
</div>
</div>
</div>
</nav>

<article className="max-w-4xl mx-auto px-4 py-12">
<header className="mb-12">
{post.mainImage && (
<div className="relative w-full h-[60vh] mb-8 rounded-xl overflow-hidden">
<Image
src={urlFor(post.mainImage).url()}
alt={post.title}
fill
className="object-cover"
priority
/>
</div>
)}
<h1 className="text-5xl font-bold mb-4 bg-gradient-to-r from-green-600 to-green-400 bg-clip-text text-transparent">
{post.title}
</h1>
<div className="flex items-center gap-4 text-gray-600 mb-6">
<span className="font-medium">{post.author}</span>
<span>•</span>
<time dateTime={post.publishedAt}>
{format(new Date(post.publishedAt), "MMMM d, yyyy")}
</time>
</div>
</header>

<div className="prose prose-lg max-w-none prose-headings:text-green-600 prose-a:text-green-500 prose-blockquote:border-green-500">
{/* <PortableText value={post.content} components={PortableTextComponents} /> */}
{post.body && (
<PortableText value={post.body} components={PortableTextComponents} />
)}
</div>
</article>
</div>
);
}
65 changes: 65 additions & 0 deletions app/blog/components/BlogList.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{posts.map((post) => (
<Link
href={`/blog/${post.slug.current}`}
key={post._id}
className="group relative h-[450px] block transform perspective-1000 hover:z-10"
>
<article className="relative h-full w-full transition-all duration-500 [transform-style:preserve-3d] group-hover:[transform:rotateY(10deg)]">
<div className="absolute inset-0">
<div className="relative h-full w-full overflow-hidden rounded-2xl bg-gray-900/90 border border-gray-800">
{post.mainImage && (
<div className="absolute inset-0 transition-transform duration-500 group-hover:scale-110">
<Image
src={urlFor(post.mainImage).url()}
alt={post.title}
fill
className="object-cover opacity-50"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
)}
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-gray-900/60 to-gray-900/90" />
<div className="relative h-full p-6 flex flex-col justify-end">
<div className="flex flex-wrap gap-2 mb-3">
{post.categories.map((category) => (
<Badge
key={category}
variant="outline"
className="border-green-500 text-green-400"
>
{category}
</Badge>
))}
</div>
<h2 className="text-2xl font-semibold text-white mb-2">
{post.title}
</h2>
<p className="text-gray-400 line-clamp-3 mb-4">
{post.excerpt}
</p>
<div className="flex items-center text-gray-400 text-sm">
<span>{post.author}</span>
<span className="mx-2">•</span>
<time dateTime={post.publishedAt}>
{format(new Date(post.publishedAt), "MMMM d, yyyy")}
</time>
</div>
</div>
</div>
</div>
</article>
</Link>
))}
</div>
);
}
Loading
Loading