From 9e8cf4b147ad14477b3fad8d5fc671559128fb4e Mon Sep 17 00:00:00 2001 From: "Wyatt J. Miller" Date: Mon, 2 Dec 2024 18:29:45 -0500 Subject: [PATCH] code dump frontend --- frontend/components/AuthorCard.tsx | 42 +++++++ frontend/components/Footer.tsx | 62 +++++++++- frontend/components/Header.tsx | 47 ++++++-- frontend/components/PhotoCircle.tsx | 29 +++++ frontend/components/PostCard.tsx | 33 ++++++ frontend/components/PostCarousel.tsx | 19 +++ frontend/deno.json | 9 +- frontend/fresh.gen.ts | 10 +- frontend/islands/PostCard.tsx | 22 ---- frontend/islands/PostCarousel.tsx | 21 ---- frontend/islands/ProjectCard.tsx | 46 +++++++ frontend/lib/convertUtc.ts | 14 +++ frontend/lib/useFetch.tsx | 57 --------- frontend/routes/authors/[id].tsx | 73 ++++++++++++ frontend/routes/contact/index.tsx | 171 +++++++++++++++++++++++++++ frontend/routes/index.tsx | 24 ++-- frontend/routes/posts/[id].tsx | 22 +++- frontend/routes/posts/index.tsx | 97 +++++++-------- frontend/routes/projects/index.tsx | 67 +++++++++++ 19 files changed, 681 insertions(+), 184 deletions(-) create mode 100644 frontend/components/AuthorCard.tsx create mode 100644 frontend/components/PhotoCircle.tsx create mode 100644 frontend/components/PostCard.tsx create mode 100644 frontend/components/PostCarousel.tsx delete mode 100644 frontend/islands/PostCard.tsx delete mode 100644 frontend/islands/PostCarousel.tsx create mode 100644 frontend/islands/ProjectCard.tsx create mode 100644 frontend/lib/convertUtc.ts delete mode 100644 frontend/lib/useFetch.tsx create mode 100644 frontend/routes/contact/index.tsx create mode 100644 frontend/routes/projects/index.tsx diff --git a/frontend/components/AuthorCard.tsx b/frontend/components/AuthorCard.tsx new file mode 100644 index 0000000..da633a4 --- /dev/null +++ b/frontend/components/AuthorCard.tsx @@ -0,0 +1,42 @@ +import { PhotoCircle } from "./PhotoCircle.tsx"; + +export default function AuthorCard({ + author, + isIdentified, +}: { + author: Author; + isIdentified?: boolean; +}) { + return ( +
+
+
+ +
+

+ {author.first_name} {author.last_name} +

+

+ {author.bio} +

+
+
+
+
+ ); +} + +export type Author = { + author_id: number; + first_name: string; + last_name: string; + bio: string; + image?: string; +}; diff --git a/frontend/components/Footer.tsx b/frontend/components/Footer.tsx index c3ec6ea..a5eb28d 100644 --- a/frontend/components/Footer.tsx +++ b/frontend/components/Footer.tsx @@ -1,3 +1,63 @@ export default function Footer() { - return
THIS IS A FOOTER
; + return ( + + ); } diff --git a/frontend/components/Header.tsx b/frontend/components/Header.tsx index 70eb3a8..927c7c5 100644 --- a/frontend/components/Header.tsx +++ b/frontend/components/Header.tsx @@ -1,6 +1,10 @@ +import * as hi from "jsr:@preact-icons/hi2"; + interface HeaderLink { name: string; linkTo: string; + // deno-lint-ignore no-explicit-any + icon: any; newTab?: boolean; } @@ -8,34 +12,53 @@ const headerLinks: Array = [ { name: "Home", linkTo: "/", + icon: , }, { name: "Blog", linkTo: "posts/", + icon: , }, { name: "Projects", - linkTo: "projects", + linkTo: "projects/", + icon: , }, { name: "Contact", linkTo: "contact/", + icon: , }, ]; export default function Header() { return ( -
- {headerLinks.map((l) => { - const newTab = l.newTab ? "_blank" : "_self"; - return ( - + ); + })} +
+ ); } diff --git a/frontend/components/PhotoCircle.tsx b/frontend/components/PhotoCircle.tsx new file mode 100644 index 0000000..f2a5214 --- /dev/null +++ b/frontend/components/PhotoCircle.tsx @@ -0,0 +1,29 @@ +export const PhotoCircle = function PhotoCircle({ + src, + alt, + size = "w-48 h-48", + width = "256", + height = "256", +}: PhotoCircleOpts) { + return ( +
+ {alt +
+ ); +}; + +type PhotoCircleOpts = { + src: string; + alt?: string; + size?: string; + width?: string; + height?: string; +}; diff --git a/frontend/components/PostCard.tsx b/frontend/components/PostCard.tsx new file mode 100644 index 0000000..691226d --- /dev/null +++ b/frontend/components/PostCard.tsx @@ -0,0 +1,33 @@ +import { convertUtc } from "../lib/convertUtc.ts"; +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 {convertUtc(post.created_at)} +

+

{truncateString(post.body, 15)}

+ +
+ ); +}; + +export type Post = { + post_id: number; + author_id: number; + first_name: string; + last_name: string; + title: string; + body: string; + created_at: string; +}; diff --git a/frontend/components/PostCarousel.tsx b/frontend/components/PostCarousel.tsx new file mode 100644 index 0000000..c11dd99 --- /dev/null +++ b/frontend/components/PostCarousel.tsx @@ -0,0 +1,19 @@ +import { Post, PostCard } from "./PostCard.tsx"; + +interface PostOpts { + posts: Post[]; +} + +export const PostCarousel = function PostCarousel({ posts }: PostOpts) { + return ( +
+
+
+ {posts.map((post: Post) => { + return ; + })} +
+
+
+ ); +}; diff --git a/frontend/deno.json b/frontend/deno.json index 0a97205..a0e2d8c 100644 --- a/frontend/deno.json +++ b/frontend/deno.json @@ -23,11 +23,14 @@ "imports": { "$fresh/": "https://deno.land/x/fresh@1.6.8/", "$std/": "https://deno.land/std@0.216.0/", - "@preact-hooks/fetch": "jsr:@preact-hooks/fetch@^0.0.4", + "@preact-icons/common": "jsr:@preact-icons/common@^1.0.12", + "@preact-icons/hi2": "jsr:@preact-icons/hi2@^1.0.12", "@preact/signals": "https://esm.sh/*@preact/signals@1.2.2", "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1", - "preact": "https://esm.sh/preact@10.19.6", - "preact/": "https://esm.sh/preact@10.19.6/", + "preact": "npm:preact@10.22.1", + "preact/": "npm:/preact@10.22.1/", + "preact/hooks": "npm:preact@10.22.1/hooks", + "preact/jsx-runtime": "npm:preact@10.22.1/jsx-runtime", "tailwindcss": "npm:tailwindcss@3.4.1", "tailwindcss/": "npm:/tailwindcss@3.4.1/", "tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js" diff --git a/frontend/fresh.gen.ts b/frontend/fresh.gen.ts index b59e2e3..919fe11 100644 --- a/frontend/fresh.gen.ts +++ b/frontend/fresh.gen.ts @@ -7,12 +7,13 @@ 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 $contact_index from "./routes/contact/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 $projects_index from "./routes/projects/index.tsx"; import * as $Counter from "./islands/Counter.tsx"; -import * as $PostCard from "./islands/PostCard.tsx"; -import * as $PostCarousel from "./islands/PostCarousel.tsx"; +import * as $ProjectCard from "./islands/ProjectCard.tsx"; import { type Manifest } from "$fresh/server.ts"; const manifest = { @@ -22,14 +23,15 @@ const manifest = { "./routes/_layout.tsx": $_layout, "./routes/authors/[id].tsx": $authors_id_, "./routes/authors/index.tsx": $authors_index, + "./routes/contact/index.tsx": $contact_index, "./routes/index.tsx": $index, "./routes/posts/[id].tsx": $posts_id_, "./routes/posts/index.tsx": $posts_index, + "./routes/projects/index.tsx": $projects_index, }, islands: { "./islands/Counter.tsx": $Counter, - "./islands/PostCard.tsx": $PostCard, - "./islands/PostCarousel.tsx": $PostCarousel, + "./islands/ProjectCard.tsx": $ProjectCard, }, baseUrl: import.meta.url, } satisfies Manifest; diff --git a/frontend/islands/PostCard.tsx b/frontend/islands/PostCard.tsx deleted file mode 100644 index b347c55..0000000 --- a/frontend/islands/PostCard.tsx +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 2504c02..0000000 --- a/frontend/islands/PostCarousel.tsx +++ /dev/null @@ -1,21 +0,0 @@ -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/islands/ProjectCard.tsx b/frontend/islands/ProjectCard.tsx new file mode 100644 index 0000000..f5baa06 --- /dev/null +++ b/frontend/islands/ProjectCard.tsx @@ -0,0 +1,46 @@ +export const ProjectCard = function ProjectCard(props: ProjectProps) { + return ( +
props.repo && open(props.repo, "_blank")} + > +
+

+ + {props.title} + +

+
+ {props.repo && ( + e.stopPropagation()} + > + Active + + )} + {!props.repo && !props.wip && Dead} + {props.wip && WIP} +
+
+

+ {props.summary} +

+

+ {props.tech} +

+
+ ); +}; + +type ProjectProps = { + title: string; + repo?: string; + summary: string; + tech: string; + wip?: boolean; +}; diff --git a/frontend/lib/convertUtc.ts b/frontend/lib/convertUtc.ts new file mode 100644 index 0000000..4185fce --- /dev/null +++ b/frontend/lib/convertUtc.ts @@ -0,0 +1,14 @@ +export const convertUtc = (utcTimestamp: string): string => { + const utcTimeZone: string = Intl.DateTimeFormat().resolvedOptions().timeZone; + return new Date(utcTimestamp).toLocaleString("en-US", { + day: "numeric", + month: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + hourCycle: "h24", + timeZone: utcTimeZone, + }); +}; + +const validateTime = (parsedUtcTimestamp: any) => {}; diff --git a/frontend/lib/useFetch.tsx b/frontend/lib/useFetch.tsx deleted file mode 100644 index f018adf..0000000 --- a/frontend/lib/useFetch.tsx +++ /dev/null @@ -1,57 +0,0 @@ -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/authors/[id].tsx b/frontend/routes/authors/[id].tsx index e69de29..a847fe1 100644 --- a/frontend/routes/authors/[id].tsx +++ b/frontend/routes/authors/[id].tsx @@ -0,0 +1,73 @@ +import { FreshContext, Handlers, PageProps } from "$fresh/server.ts"; +import AuthorCard from "../../components/AuthorCard.tsx"; +import { Post } from "../../components/PostCard.tsx"; +import { PostCarousel } from "../../components/PostCarousel.tsx"; + +export const handler: Handlers = { + async GET(_req: Request, ctx: FreshContext) { + try { + const [authorResponse, authorPostResponse] = await Promise.all([ + fetch(`${Deno.env.get("BASE_URI_API")}/authors/${ctx.params.id}`), + fetch(`${Deno.env.get("BASE_URI_API")}/authors/${ctx.params.id}/posts`), + ]); + + const [authorData, authorPostData] = await Promise.all([ + authorResponse.json(), + authorPostResponse.json(), + ]); + + return ctx.render({ + authorData, + authorPostData, + }); + } catch (error) { + return ctx.render({ + error: error.message, + authorData: null, + authorPostData: [], + }); + } + }, +}; + +export default function AuthorIdentifier({ data }: PageProps) { + const { authorData, authorPostData, error } = data; + + if (error) { + return ( +
+

Error Loading Author Information

+

{error}

+
+ ); + } + + if (!authorData) { + return
No author found
; + } + + return ( + <> +
+ +
+
+ +
+ + ); +} + +interface PageData { + error?: string; + authorData: AuthorResponse; + authorPostData: Array; +} + +export type AuthorResponse = { + author_id: number; + first_name: string; + last_name: string; + bio: string; + image?: string; +}; diff --git a/frontend/routes/contact/index.tsx b/frontend/routes/contact/index.tsx new file mode 100644 index 0000000..970fab7 --- /dev/null +++ b/frontend/routes/contact/index.tsx @@ -0,0 +1,171 @@ +import { Handlers, PageProps } from "$fresh/server.ts"; +import { Head } from "$fresh/runtime.ts"; + +interface FormState { + name?: string; + email?: string; + message?: string; + errors?: { + name?: string; + email?: string; + message?: string; + }; + submitted?: boolean; +} + +export default function Contact({ data }: PageProps) { + return ( +
+ + Contact Us + + +

Contact Form

+ + {/* Check if form was successfully submitted */} + {data?.submitted && ( + + )} + +
+ {/* Name Input */} +
+ + + {data?.errors?.name && ( +

{data.errors.name}

+ )} +
+ + {/* Email Input */} +
+ + + {data?.errors?.email && ( +

{data.errors.email}

+ )} +
+ + {/* Message Textarea */} +
+ + + {data?.errors?.message && ( +

+ {data.errors.message} +

+ )} +
+ + {/* Submit Button */} +
+ +
+
+
+ ); +} + +// Server-side form handling +export const handler: Handlers = { + GET(_, ctx) { + return ctx.render({}); + }, + async POST(req, ctx) { + const formData = await req.formData(); + const state: FormState = { + name: formData.get("name")?.toString(), + email: formData.get("email")?.toString(), + message: formData.get("message")?.toString(), + }; + + // Validation logic + const errors: FormState["errors"] = {}; + + if (!state.name || state.name.trim() === "") { + errors.name = "Name is required"; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!state.email) { + errors.email = "Email is required"; + } else if (!emailRegex.test(state.email)) { + errors.email = "Invalid email format"; + } + + if (!state.message || state.message.trim() === "") { + errors.message = "Message is required"; + } + + // If there are errors, return the form with error messages + if (Object.keys(errors).length > 0) { + return ctx.render({ + ...state, + errors, + }); + } + + // TODO: Implement actual form submission logic here + // For example, send email, save to database, etc. + console.log("Form submitted:", state); + + // Return successful submission + return ctx.render({ + ...state, + submitted: true, + }); + }, +}; diff --git a/frontend/routes/index.tsx b/frontend/routes/index.tsx index de88a01..4938fbc 100644 --- a/frontend/routes/index.tsx +++ b/frontend/routes/index.tsx @@ -1,25 +1,27 @@ +import { PhotoCircle } from "../components/PhotoCircle.tsx"; + export default function Home() { return ( -
+
- the Fresh logo: a sliced lemon dripping with juice
-

- Heya! I'm Wyatt Miller +

+ Heya! I'm Wyatt

-

+

Thanks for checking out this corner of the Internet!

+

+ I design and develop software for developers, for end users, and + everyone in between +

- {/** about me stuff */}
); diff --git a/frontend/routes/posts/[id].tsx b/frontend/routes/posts/[id].tsx index 1e6b7aa..92e5e90 100644 --- a/frontend/routes/posts/[id].tsx +++ b/frontend/routes/posts/[id].tsx @@ -1,16 +1,28 @@ import { FreshContext, Handlers, PageProps } from "$fresh/server.ts"; -import { useFetch } from "../../lib/useFetch.tsx"; -interface PostResponse { +interface PageData { post_id: number; first_nane: string; last_name: string; title: string; body: string; created_at: string; + is_featured: boolean; } -export default function PostIdentifier(props: PageProps) { - console.log(props.data); - return
BLOG POST #{props.params.id}
; +export const handler: Handlers = { + async GET(_req: Request, ctx: FreshContext) { + const postResult = await fetch( + `${Deno.env.get("BASE_URI_API")}/posts/${ctx.params.id}`, + ); + + const postData = await postResult.json(); + return ctx.render({ + postData, + }); + }, +}; + +export default function PostIdentifier({ data }: PageProps) { + return
; } diff --git a/frontend/routes/posts/index.tsx b/frontend/routes/posts/index.tsx index c8f950f..a9a3e83 100644 --- a/frontend/routes/posts/index.tsx +++ b/frontend/routes/posts/index.tsx @@ -1,61 +1,37 @@ -// 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 { PostCarousel } from "../../components/PostCarousel.tsx"; import { Handlers, PageProps } from "$fresh/server.ts"; -import { Post } from "../../islands/PostCard.tsx"; +import { Post } from "../../components/PostCard.tsx"; interface PageData { featuredPosts: Post[]; + recentPosts: 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`), - ]); + const [featuredResult, recentResult, hotResult, popularResult] = + await Promise.all([ + fetch(`${Deno.env.get("BASE_URI_API")}/posts/featured`), + fetch(`${Deno.env.get("BASE_URI_API")}/posts/recent`), + fetch(`${Deno.env.get("BASE_URI_API")}/posts/hot`), + fetch(`${Deno.env.get("BASE_URI_API")}/posts/popular`), + ]); - // Parse all JSON responses concurrently - const [featuredPosts, hotPosts, popularPosts] = await Promise.all([ - featuredResult.json(), - hotResult.json(), - popularResult.json(), - ]); + // parse all JSON responses concurrently + const [featuredPosts, recentPosts, hotPosts, popularPosts] = await Promise + .all([ + featuredResult.json(), + recentResult.json(), + hotResult.json(), + popularResult.json(), + ]); return ctx.render({ featuredPosts, + recentPosts, hotPosts, popularPosts, }); @@ -63,20 +39,45 @@ export const handler: Handlers = { }; export default function PostPage({ data }: PageProps) { - const { featuredPosts, hotPosts, popularPosts } = data; + const { featuredPosts, recentPosts, hotPosts, popularPosts } = data; return ( -
+
+

Blog

-

Featured Posts

+

+ Featured Posts +

+
+ Ignite the impossible +
-

Recent Posts

+

+ Recent Posts +

+
+ Now with 100% fresh perspective +
+ +
+
+

+ Hot Posts +

+
+ Making chaos look cool since forever +
-

Popular Posts

+

+ Popular Posts +

+
+ Content may cause uncontrollable reading +
diff --git a/frontend/routes/projects/index.tsx b/frontend/routes/projects/index.tsx new file mode 100644 index 0000000..3ab1790 --- /dev/null +++ b/frontend/routes/projects/index.tsx @@ -0,0 +1,67 @@ +import { ProjectCard } from "../../islands/ProjectCard.tsx"; + +export default function Projects() { + return ( +
+
+

+ Projects +

+
+ + + + + + + + +
+
+
+ ); +}