diff --git a/frontend/components/Header.tsx b/frontend/components/Header.tsx index 2721883..70eb3a8 100644 --- a/frontend/components/Header.tsx +++ b/frontend/components/Header.tsx @@ -1,3 +1,41 @@ -export default function Header() { - return
THIS IS THE HEADER
; +interface HeaderLink { + name: string; + linkTo: string; + newTab?: boolean; +} + +const headerLinks: Array = [ + { + name: "Home", + linkTo: "/", + }, + { + name: "Blog", + linkTo: "posts/", + }, + { + name: "Projects", + linkTo: "projects", + }, + { + name: "Contact", + linkTo: "contact/", + }, +]; + +export default function Header() { + return ( +
+ {headerLinks.map((l) => { + const newTab = l.newTab ? "_blank" : "_self"; + return ( +
+ + {l.name} + +
+ ); + })} +
+ ); } diff --git a/frontend/fresh.gen.ts b/frontend/fresh.gen.ts index d73b3e5..b59e2e3 100644 --- a/frontend/fresh.gen.ts +++ b/frontend/fresh.gen.ts @@ -5,10 +5,14 @@ import * as $_404 from "./routes/_404.tsx"; import * as $_app from "./routes/_app.tsx"; import * as $_layout from "./routes/_layout.tsx"; +import * as $authors_id_ from "./routes/authors/[id].tsx"; +import * as $authors_index from "./routes/authors/index.tsx"; import * as $index from "./routes/index.tsx"; import * as $posts_id_ from "./routes/posts/[id].tsx"; import * as $posts_index from "./routes/posts/index.tsx"; import * as $Counter from "./islands/Counter.tsx"; +import * as $PostCard from "./islands/PostCard.tsx"; +import * as $PostCarousel from "./islands/PostCarousel.tsx"; import { type Manifest } from "$fresh/server.ts"; const manifest = { @@ -16,12 +20,16 @@ const manifest = { "./routes/_404.tsx": $_404, "./routes/_app.tsx": $_app, "./routes/_layout.tsx": $_layout, + "./routes/authors/[id].tsx": $authors_id_, + "./routes/authors/index.tsx": $authors_index, "./routes/index.tsx": $index, "./routes/posts/[id].tsx": $posts_id_, "./routes/posts/index.tsx": $posts_index, }, islands: { "./islands/Counter.tsx": $Counter, + "./islands/PostCard.tsx": $PostCard, + "./islands/PostCarousel.tsx": $PostCarousel, }, baseUrl: import.meta.url, } satisfies Manifest; diff --git a/frontend/islands/PostCard.tsx b/frontend/islands/PostCard.tsx new file mode 100644 index 0000000..b347c55 --- /dev/null +++ b/frontend/islands/PostCard.tsx @@ -0,0 +1,22 @@ +import { truncateString } from "../lib/truncate.ts"; + +export const PostCard = function PostCard({ post }: { post: Post }) { + return ( +
+

{post.title}

+

+ Written by {post.first_name} {post.last_name} at {post.created_at} +

+

{truncateString(post.body, 15)}

+
+ ); +}; + +export interface Post { + post_id: number; + first_name: string; + last_name: string; + title: string; + body: string; + created_at: string; +} diff --git a/frontend/islands/PostCarousel.tsx b/frontend/islands/PostCarousel.tsx new file mode 100644 index 0000000..2504c02 --- /dev/null +++ b/frontend/islands/PostCarousel.tsx @@ -0,0 +1,21 @@ +import { Post, PostCard } from "./PostCard.tsx"; + +interface PostOpts { + posts: Post[]; +} + +export const PostCarousel = function PostCarousel({ posts }: PostOpts) { + return ( +
+
+
+ {posts.map((post: Post, idx: number) => { + if (idx < 3) { + return ; + } + })} +
+
+
+ ); +}; diff --git a/frontend/lib/truncate.ts b/frontend/lib/truncate.ts new file mode 100644 index 0000000..98aa086 --- /dev/null +++ b/frontend/lib/truncate.ts @@ -0,0 +1,3 @@ +export const truncateString = (str: string, maxLength: number) => { + return str.length > maxLength ? `${str.slice(0, maxLength)}...` : str; +}; diff --git a/frontend/lib/useFetch.tsx b/frontend/lib/useFetch.tsx new file mode 100644 index 0000000..f018adf --- /dev/null +++ b/frontend/lib/useFetch.tsx @@ -0,0 +1,57 @@ +import { useEffect } from "preact/hooks"; +import { signal } from "@preact/signals"; + +export function useFetch( + url: string, + options?: FetchOptions, +): { + data: T | null; + loading: boolean; + error: Error | null; + refetch: (newURL?: string, newOptions?: FetchOptions) => void; +} { + const data = signal(null); + const loading = signal(true); + const error = signal(null); + + useEffect(() => { + fetch(url, options) + .then((response) => { + if (!response.ok) { + throw new Error("Failed to fetch data"); + } + return response.json(); + }) + .then((resp) => (data.value = resp)) + .catch((err) => (error.value = err)) + .finally(() => (loading.value = false)); + }, [url, options]); + + const refetch = (newURL?: string, newOptions?: FetchOptions) => { + loading.value = true; + error.value = null; + fetch(newURL || url, newOptions || options) + .then((response) => { + if (!response.ok) { + throw new Error("Failed to fetch data"); + } + return response.json(); + }) + .then((resp) => (data.value = resp)) + .catch((err) => (error.value = err)) + .finally(() => (loading.value = false)); + }; + + return { + data: data.value, + loading: loading.value, + error: error.value, + refetch, + }; +} + +export interface FetchOptions { + method: "GET" | "POST" | "PUT" | "DELETE"; + headers: Record; + body: string; +} diff --git a/frontend/routes/_404.tsx b/frontend/routes/_404.tsx index c63ae2e..6bc7c53 100644 --- a/frontend/routes/_404.tsx +++ b/frontend/routes/_404.tsx @@ -8,18 +8,11 @@ export default function Error404() {
- the Fresh logo: a sliced lemon dripping with juice

404 - Page not found

-

- The page you were looking for doesn't exist. -

- Go back home +

The page you were looking for doesn't exist.

+ + Go back home +
diff --git a/frontend/routes/authors/[id].tsx b/frontend/routes/authors/[id].tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/routes/authors/index.tsx b/frontend/routes/authors/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/routes/posts/[id].tsx b/frontend/routes/posts/[id].tsx index 2d282ad..1e6b7aa 100644 --- a/frontend/routes/posts/[id].tsx +++ b/frontend/routes/posts/[id].tsx @@ -1,3 +1,16 @@ -export default function PostIdentifier() { - return null; +import { FreshContext, Handlers, PageProps } from "$fresh/server.ts"; +import { useFetch } from "../../lib/useFetch.tsx"; + +interface PostResponse { + post_id: number; + first_nane: string; + last_name: string; + title: string; + body: string; + created_at: string; +} + +export default function PostIdentifier(props: PageProps) { + console.log(props.data); + return
BLOG POST #{props.params.id}
; } diff --git a/frontend/routes/posts/index.tsx b/frontend/routes/posts/index.tsx index e718174..c8f950f 100644 --- a/frontend/routes/posts/index.tsx +++ b/frontend/routes/posts/index.tsx @@ -1,52 +1,84 @@ -export default function Posts() { +// import { FreshContext, Handlers, PageProps } from "$fresh/server.ts"; +import { PostCarousel } from "../../islands/PostCarousel.tsx"; + +// export interface PostItems { +// title: string; +// author: string; +// publish_date: string; +// truncated_content: string; +// } +// +// export interface PostReponse { +// new_posts: Array; +// popular_posts: Array; +// hot_posts: Array; +// } +// +// export interface PostResponseOne { +// post_id: number; +// first_name: string; +// last_name: string; +// title: string; +// body: string; +// created_at: string; +// } +// +// export const handler: Handlers> = { +// GET(_req: Request, ctx: FreshContext) { +// const results = fetch(`${Deno.env.get("BASE_URI")}/posts/all`); +// return ctx.render(results.then((resp) => resp.json())); +// }, +// }; + +import { Handlers, PageProps } from "$fresh/server.ts"; +import { Post } from "../../islands/PostCard.tsx"; + +interface PageData { + featuredPosts: Post[]; + hotPosts: Post[]; + popularPosts: Post[]; +} + +export const handler: Handlers = { + async GET(_: any, ctx: any) { + const [featuredResult, hotResult, popularResult] = await Promise.all([ + fetch(`${Deno.env.get("BASE_URI")}/posts/all`), + fetch(`${Deno.env.get("BASE_URI")}/posts/hot`), + fetch(`${Deno.env.get("BASE_URI")}/posts/popular`), + ]); + + // Parse all JSON responses concurrently + const [featuredPosts, hotPosts, popularPosts] = await Promise.all([ + featuredResult.json(), + hotResult.json(), + popularResult.json(), + ]); + + return ctx.render({ + featuredPosts, + hotPosts, + popularPosts, + }); + }, +}; + +export default function PostPage({ data }: PageProps) { + const { featuredPosts, hotPosts, popularPosts } = data; + return ( -
-
-

Blog posts

-

- A lot of them! -

-
-
-
-
-

Blog Post 1

-

Written by Wyatt Miller

-

- Lorem ipsum odor amet, consectetuer adipiscing elit. Senectus - aliquet fusce habitant sem integer lectus curae quisque. Tincidunt - vitae adipiscing justo et nulla. Faucibus imperdiet turpis maximus - nam natoque suscipit platea. Feugiat imperdiet malesuada enim diam - primis. Vitae sollicitudin molestie cubilia tempus nunc dignissim - adipiscing. -

-
-
-

Blog Post 2

-

Written by Wyatt Miller

-

- Lorem ipsum odor amet, consectetuer adipiscing elit. Senectus - aliquet fusce habitant sem integer lectus curae quisque. Tincidunt - vitae adipiscing justo et nulla. Faucibus imperdiet turpis maximus - nam natoque suscipit platea. Feugiat imperdiet malesuada enim diam - primis. Vitae sollicitudin molestie cubilia tempus nunc dignissim - adipiscing. -

-
-
-

Blog Post 3

-

Written by Wyatt Miller

-

- Lorem ipsum odor amet, consectetuer adipiscing elit. Senectus - aliquet fusce habitant sem integer lectus curae quisque. Tincidunt - vitae adipiscing justo et nulla. Faucibus imperdiet turpis maximus - nam natoque suscipit platea. Feugiat imperdiet malesuada enim diam - primis. Vitae sollicitudin molestie cubilia tempus nunc dignissim - adipiscing. -

-
-
-
+
+
+

Featured Posts

+ +
+
+

Recent Posts

+ +
+
+

Popular Posts

+ +
); }