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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules
.DS_Store
next-env.d.ts
dist/
gcp-keys.json
15 changes: 15 additions & 0 deletions app/api/blog/[slug]/description/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getTextFile } from '@/lib/gcp';
import { NextResponse } from 'next/server';

export async function GET(
req: Request,
{ params }: { params: Promise<{ slug: string }> },
) {
await params;
const file = new URL(req.url).searchParams.get('file');
if (!file) return new NextResponse('Missing file param', { status: 400 });
const text = await getTextFile(file);
return new NextResponse(text, {
headers: { 'Content-Type': 'text/plain' },
});
}
17 changes: 17 additions & 0 deletions app/api/blog/[slug]/image-url/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getSignedUrl } from '@/lib/gcp';
import { NextResponse } from 'next/server';

export async function GET(
req: Request,
{ params }: { params: Promise<{ slug: string }> },
) {
await params;
const file = new URL(req.url).searchParams.get('file');
if (!file)
return NextResponse.json(
{ error: 'Missing file param' },
{ status: 400 },
);
const imageUrl = await getSignedUrl(file);
return NextResponse.json({ imageUrl });
}
7 changes: 7 additions & 0 deletions app/api/blog/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { getBlogList } from '@/lib/gcp';
import { NextResponse } from 'next/server';

export async function GET() {
const data = await getBlogList();
return NextResponse.json(data.posts);
}
205 changes: 140 additions & 65 deletions app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,83 @@
"use client";
import { useState } from "react";
'use client';
import { useState, useEffect } from 'react';
import Updates from './Updates';
import Announcement from './Announcement';
import Media from './Media';

const updateContents = [
"Update 1",
"Update 2",
"Update 3",
"Update 4",
"Update 5",
];
type BlogPageEntry = {
slug: string;
title: string;
date: string;
description: string;
imageUrl: string;
};

async function fetchBlogPages(): Promise<BlogPageEntry[]> {
const indexRes = await fetch('/api/blog');
if (!indexRes.ok) throw new Error('Failed to fetch blog index');
const index: {
slug: string;
title: string;
date: string;
image: string;
descriptionFile: string;
}[] = await indexRes.json();

const pages = await Promise.all(
index.map(async (entry) => {
const [descRes, imgRes] = await Promise.all([
fetch(
`/api/blog/${entry.slug}/description?file=${encodeURIComponent(entry.descriptionFile)}`,
),
fetch(
`/api/blog/${entry.slug}/image-url?file=${encodeURIComponent(entry.image)}`,
),
]);
const description = await descRes.text();
const { imageUrl } = await imgRes.json();
return { ...entry, description, imageUrl };
}),
);

return pages;
}

export default function BlogPage() {
const [updates, setUpdates] = useState<BlogPageEntry[]>([]);
const [index, setIndex] = useState(0);
const total = updateContents.length;
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

const prev = () =>
setIndex((i) => (i === 0 ? total - 1 : i - 1));
useEffect(() => {
fetchBlogPages()
.then((pages) => {
setUpdates(pages);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, []);

const next = () =>
setIndex((i) => (i === total - 1 ? 0 : i + 1));

const get = (offset : number) =>
updateContents[(index + offset + total) % total];
const total = updates.length;
const prev = () => setIndex((i) => (i === 0 ? total - 1 : i - 1));
const next = () => setIndex((i) => (i === total - 1 ? 0 : i + 1));
const get = (offset: number) => updates[(index + offset + total) % total];

return (
<main className="min-h-screen bg-[#FDFCF7] text-base pt-36">
<div className="px-4 md:px-16 lg:px-32 py-12 space-y-12">
<img
src="image.png"
alt="image"
className="w-full max-w-[1160px] h-[306px]"
src="image.png"
alt="image"
className="w-full max-w-[1160px] h-[306px]"
/>

<section className="space-y-10">
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-serif tracking-wide">ANNOUNCEMENTS</h1>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-serif tracking-wide">
ANNOUNCEMENTS
</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-[16px]">
<Announcement />
<Announcement />
Expand All @@ -45,50 +87,84 @@ export default function BlogPage() {
</section>

<section className="space-y-10">
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-serif tracking-wide">UPDATES</h1>
<div className="flex items-center justify-center gap-4 sm:gap-8">

{/* Previous slide */}
<Updates
className="h-[150px] sm:h-[226px] w-full sm:w-[234px] flex items-center justify-center text-center p-2 sm:p-4 text-xs sm:text-sm"
>
{get(-1)}
</Updates>

{/* Left arrow */}
<button
onClick={prev}
className="text-2xl sm:text-4xl font-light hover:opacity-60"
>
&#8249;
</button>

{/* Active slide */}
<Updates
className="h-[250px] sm:h-[419px] w-full sm:w-[416px] flex items-center justify-center text-center p-4 sm:p-6 text-sm sm:text-lg"
>
{get(0)}
</Updates>

{/* Right arrow */}
<button
onClick={next}
className="text-2xl sm:text-4xl font-light hover:opacity-60"
>
&#8250;
</button>

{/* Next slide */}
<Updates
className="h-[150px] sm:h-[226px] w-full sm:w-[234px] flex items-center justify-center text-center p-2 sm:p-4 text-xs sm:text-sm"
>
{get(1)}
</Updates>
</div>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-serif tracking-wide">
UPDATES
</h1>

{loading && (
<p className="text-center text-gray-400">
Loading updates...
</p>
)}

{error && (
<p className="text-center text-red-400">
Error: {error}
</p>
)}

{!loading && !error && total > 0 && (
<div className="flex items-center justify-center gap-4 sm:gap-8">
<Updates className="h-[150px] sm:h-[226px] w-full sm:w-[234px] flex items-center justify-center text-center p-2 sm:p-4 text-xs sm:text-sm">
<div>
<img
src={get(-1).imageUrl}
alt={get(-1).title}
className="w-full h-24 object-cover mb-2"
/>
<p>{get(-1).title}</p>
</div>
</Updates>

<button
onClick={prev}
className="text-2xl sm:text-4xl font-light hover:opacity-60"
>
&#8249;
</button>

<Updates className="h-[250px] sm:h-[419px] w-full sm:w-[416px] flex items-center justify-center text-center p-4 sm:p-6 text-sm sm:text-lg">
<div className="space-y-3">
<img
src={get(0).imageUrl}
alt={get(0).title}
className="w-full h-40 object-cover"
/>
<h2 className="font-semibold">
{get(0).title}
</h2>
<p className="text-sm text-gray-600">
{get(0).date}
</p>
<p>{get(0).description}</p>
</div>
</Updates>

<button
onClick={next}
className="text-2xl sm:text-4xl font-light hover:opacity-60"
>
&#8250;
</button>

<Updates className="h-[150px] sm:h-[226px] w-full sm:w-[234px] flex items-center justify-center text-center p-2 sm:p-4 text-xs sm:text-sm">
<div>
<img
src={get(1).imageUrl}
alt={get(1).title}
className="w-full h-24 object-cover mb-2"
/>
<p>{get(1).title}</p>
</div>
</Updates>
</div>
)}
</section>

<section className="space-y-10">
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-serif tracking-wide">MEDIA</h1>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-serif tracking-wide">
MEDIA
</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-[16px]">
<Media />
<Media />
Expand All @@ -98,6 +174,5 @@ export default function BlogPage() {
</section>
</div>
</main>

);
}
}
95 changes: 93 additions & 2 deletions lib/gcp.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,96 @@
// Example
import { Storage } from '@google-cloud/storage';

if (!process.env.GCS_KEY_FILEPATH) {
throw new Error("Missing GCS_KEY_FILEPATH");
}

if (!process.env.GCS_BUCKET) {
throw new Error("Missing GCS_BUCKET");
}

const storage = new Storage({
keyFilename: process.env.GCS_KEY_FILEPATH,
});

const bucket = storage.bucket(process.env.GCS_BUCKET);

/**
* Fetches blog index JSON file (ex: blog/index.json)
* Returns parsed JSON.
*/
export async function getBlogList() {
const file = bucket.file("blog/index.json");

const [exists] = await file.exists();
if (!exists) {
throw new Error("Blog index not found");
}

const [contents] = await file.download();
return JSON.parse(contents.toString("utf-8"));
}

/**
* Fetches a single blog post by slug.
* Ex: blog/my-post.json
*/
export async function getBlogPost(slug: string) {
const file = bucket.file(`blog/${slug}.json`);

const [exists] = await file.exists();
if (!exists) {
throw new Error(`Blog post not found: ${slug}`);
}

const [contents] = await file.download();
return JSON.parse(contents.toString("utf-8"));
}

/**
* Generates a temporary signed URL for private files.
* Default: 15 minute expiration.
*/
export async function getSignedUrl(
fileName: string,
expiresInMinutes = 15
) {
const file = bucket.file(fileName);

const [url] = await file.getSignedUrl({
version: "v4",
action: "read",
expires: Date.now() + expiresInMinutes * 60 * 1000,
});

return url;
}

/**
* Fetches a text file from GCS (e.g. description.txt)
*/
export async function getTextFile(filePath: string): Promise<string> {
const file = bucket.file(filePath);
const [exists] = await file.exists();
if (!exists) throw new Error(`File not found: ${filePath}`);
const [contents] = await file.download();
return contents.toString("utf-8");
}

/**
* Do ur init to the gcp here
* Fetches all blog pages: metadata from index.json,
* then description + signed image URL per page.
*/
export async function getBlogPages() {
const index = await getBlogList();
// index is assumed to be: Array<{ slug: string, title: string, date: string, ... }>

const pages = await Promise.all(
index.map(async (entry: { slug: string; [key: string]: any }) => {
const description = await getTextFile(`blog/${entry.slug}/description.txt`);
const imageUrl = await getSignedUrl(`blog/${entry.slug}/image.png`);
return { ...entry, description, imageUrl };
})
);

return pages;
}
3 changes: 3 additions & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function middleware(req: any, res: any, next: () => void) {
// Your middleware logic here
}
Loading