code dump frontend

This commit is contained in:
Wyatt J. Miller 2024-12-02 18:29:45 -05:00
parent 44f1f35caa
commit 9e8cf4b147
19 changed files with 681 additions and 184 deletions

View File

@ -0,0 +1,42 @@
import { PhotoCircle } from "./PhotoCircle.tsx";
export default function AuthorCard({
author,
isIdentified,
}: {
author: Author;
isIdentified?: boolean;
}) {
return (
<div
class={isIdentified
? "p-6 bg-[#45475a] rounded-lg shadow-md"
: "p-6 bg-[#45475a] rounded-lg shadow-md transition-all duration-300 ease-in-out hover:shadow-xl hover:scale-105"}
>
<div class="min-w-screen flex flex-col items-center justify-between bg-[#313244] sm:min-h-screen">
<div class="sm:mt-14 sm:mb-14 mt-12 mb-4 flex flex-col items-center gap-y-5 gap-x-10 md:flex-row">
<PhotoCircle
src={author.image ?? "/logo.svg"}
alt="Author's profile photo"
/>
<div class="space-y-2 text-center md:text-left">
<p class="text-2xl text-[#f5e0dc] font-bold sm:text-4xl">
{author.first_name} {author.last_name}
</p>
<p class="text-md font-medium text-[#f2cdcd] sm:text-xl">
{author.bio}
</p>
</div>
</div>
</div>
</div>
);
}
export type Author = {
author_id: number;
first_name: string;
last_name: string;
bio: string;
image?: string;
};

View File

@ -1,3 +1,63 @@
export default function Footer() { export default function Footer() {
return <div>THIS IS A FOOTER</div>; return (
<footer class="bg-[#313244] text-[#cdd6f4] py-8">
<div class="container mx-auto px-4">
{/* Grid layout that switches from 2 to 1 column on small screens */}
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="space-y-2">
<a
class="mb-8 text-[#cdd6f4] transition-all duration-300 ease-in-out hover:text-[#cba6f7] hover:drop-shadow-[0_0_20px_rgba(96,165,250,0.7)] hover:scale-110 cursor-pointer visited:text-[#bac2de]"
href="/Resume_Wyatt_miller.png"
>
RSS
</a>
<br />
<a
class="mb-8 text-[#cdd6f4] transition-all duration-300 ease-in-out hover:text-[#cba6f7] hover:drop-shadow-[0_0_20px_rgba(96,165,250,0.7)] hover:scale-110 cursor-pointer visited:text-[#bac2de]"
href="/Resume_Wyatt_miller.png"
>
Sitemap
</a>
</div>
<div>
<a
class="mb-8 text-[#cdd6f4] transition-all duration-300 ease-in-out hover:text-[#cba6f7] hover:drop-shadow-[0_0_20px_rgba(96,165,250,0.7)] hover:scale-110 cursor-pointer visited:text-[#bac2de]"
href="/Resume_Wyatt_miller.png"
>
Resume
</a>
<br />
<a
class="mb-8 text-[#cdd6f4] transition-all duration-300 ease-in-out hover:text-[#cba6f7] hover:drop-shadow-[0_0_20px_rgba(96,165,250,0.7)] hover:scale-110 cursor-pointer visited:text-[#bac2de]"
href="mailto:wjmiller2016@gmail.com"
>
Email me
</a>
<br />
<a
class="mb-8 text-[#cdd6f4] transition-all duration-300 ease-in-out hover:text-[#cba6f7] hover:drop-shadow-[0_0_20px_rgba(96,165,250,0.7)] hover:scale-110 cursor-pointer visited:text-[#bac2de]"
href="https://x.com/wymillerlinux"
>
X
</a>
<br />
<a
class="mb-8 text-[#cdd6f4] transition-all duration-300 ease-in-out hover:text-[#cba6f7] hover:drop-shadow-[0_0_20px_rgba(96,165,250,0.7)] hover:scale-110 cursor-pointer visited:text-[#bac2de]"
href="https://github.com/wymillerlinux"
>
GitHub
</a>
</div>
</div>
<div class="border-t border-gray-700 mt-8 pt-4 text-center">
<p class="text-sm text-gray-400">
© {new Date().getFullYear()}{" "}
Miller Web Soultions. All Rights Reserved.
</p>
</div>
</div>
</footer>
);
} }

View File

@ -1,6 +1,10 @@
import * as hi from "jsr:@preact-icons/hi2";
interface HeaderLink { interface HeaderLink {
name: string; name: string;
linkTo: string; linkTo: string;
// deno-lint-ignore no-explicit-any
icon: any;
newTab?: boolean; newTab?: boolean;
} }
@ -8,34 +12,53 @@ const headerLinks: Array<HeaderLink> = [
{ {
name: "Home", name: "Home",
linkTo: "/", linkTo: "/",
icon: <hi.HiOutlineHome />,
}, },
{ {
name: "Blog", name: "Blog",
linkTo: "posts/", linkTo: "posts/",
icon: <hi.HiOutlineBookmarkSquare />,
}, },
{ {
name: "Projects", name: "Projects",
linkTo: "projects", linkTo: "projects/",
icon: <hi.HiOutlineWrenchScrewdriver />,
}, },
{ {
name: "Contact", name: "Contact",
linkTo: "contact/", linkTo: "contact/",
icon: <hi.HiOutlinePencilSquare />,
}, },
]; ];
export default function Header() { export default function Header() {
return ( return (
<div> <nav>
{headerLinks.map((l) => { <div class="bg-[#313244] flex justify-center space-x-6 p-4">
const newTab = l.newTab ? "_blank" : "_self"; {headerLinks.map((l) => {
return ( const newTab = l.newTab ? "_blank" : "_self";
<div class=""> return (
<a href={l.linkTo} target={newTab} class=""> <a
{l.name} href={l.linkTo}
target={newTab}
class="text-[#cdd6f4]
text-lg
font-medium
transition-all
duration-300
ease-in-out
hover:text-[#cba6f7]
hover:drop-shadow-[0_0_20px_rgba(96,165,250,0.7)]
hover:scale-110
cursor-pointer"
>
<div class="flex items-center gap-2">
{l.icon} {l.name}
</div>
</a> </a>
</div> );
); })}
})} </div>
</div> </nav>
); );
} }

View File

@ -0,0 +1,29 @@
export const PhotoCircle = function PhotoCircle({
src,
alt,
size = "w-48 h-48",
width = "256",
height = "256",
}: PhotoCircleOpts) {
return (
<div
className={`${size} rounded-full border-4 border-white shadow-md overflow-hidden`}
>
<img
src={src}
alt={alt ?? "A photo in circle"}
width={width}
height={height}
className="w-full h-full object-cover"
/>
</div>
);
};
type PhotoCircleOpts = {
src: string;
alt?: string;
size?: string;
width?: string;
height?: string;
};

View File

@ -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 (
<div class="p-6 bg-[#45475a] rounded-lg shadow-md transition-all duration-300 ease-in-out hover:shadow-xl hover:scale-105">
<a href={`${Deno.env.get("BASE_URI_WEB")}/posts/${post.post_id}`}>
<h2 class="text-white text-lg font-bold mb-2">{post.title}</h2>
<p class="text-white">
Written by{" "}
<a
class="text-white transition-all duration-300 ease-in-out hover:text-[#74c7ec] hover:drop-shadow-[0_0_10px_rgba(96,165,250,0.7)] hover:scale-110 cursor-pointer"
href={`${Deno.env.get("BASE_URI_WEB")}/authors/${post.author_id}`}
>
{post.first_name} {post.last_name}
</a>{" "}
at {convertUtc(post.created_at)}
</p>
<p class="text-gray-400">{truncateString(post.body, 15)}</p>
</a>
</div>
);
};
export type Post = {
post_id: number;
author_id: number;
first_name: string;
last_name: string;
title: string;
body: string;
created_at: string;
};

View File

@ -0,0 +1,19 @@
import { Post, PostCard } from "./PostCard.tsx";
interface PostOpts {
posts: Post[];
}
export const PostCarousel = function PostCarousel({ posts }: PostOpts) {
return (
<div class="post-carousel flex flex-col items-center justify-between bg-[#313244]">
<div class="flex items-center justify-center">
<div class="flex space-x-4">
{posts.map((post: Post) => {
return <PostCard post={post} />;
})}
</div>
</div>
</div>
);
};

View File

@ -23,11 +23,14 @@
"imports": { "imports": {
"$fresh/": "https://deno.land/x/fresh@1.6.8/", "$fresh/": "https://deno.land/x/fresh@1.6.8/",
"$std/": "https://deno.land/std@0.216.0/", "$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": "https://esm.sh/*@preact/signals@1.2.2",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1", "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
"preact": "https://esm.sh/preact@10.19.6", "preact": "npm:preact@10.22.1",
"preact/": "https://esm.sh/preact@10.19.6/", "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/": "npm:/tailwindcss@3.4.1/", "tailwindcss/": "npm:/tailwindcss@3.4.1/",
"tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js" "tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js"

View File

@ -7,12 +7,13 @@ import * as $_app from "./routes/_app.tsx";
import * as $_layout from "./routes/_layout.tsx"; import * as $_layout from "./routes/_layout.tsx";
import * as $authors_id_ from "./routes/authors/[id].tsx"; import * as $authors_id_ from "./routes/authors/[id].tsx";
import * as $authors_index from "./routes/authors/index.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 $index from "./routes/index.tsx";
import * as $posts_id_ from "./routes/posts/[id].tsx"; import * as $posts_id_ from "./routes/posts/[id].tsx";
import * as $posts_index from "./routes/posts/index.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 $Counter from "./islands/Counter.tsx";
import * as $PostCard from "./islands/PostCard.tsx"; import * as $ProjectCard from "./islands/ProjectCard.tsx";
import * as $PostCarousel from "./islands/PostCarousel.tsx";
import { type Manifest } from "$fresh/server.ts"; import { type Manifest } from "$fresh/server.ts";
const manifest = { const manifest = {
@ -22,14 +23,15 @@ const manifest = {
"./routes/_layout.tsx": $_layout, "./routes/_layout.tsx": $_layout,
"./routes/authors/[id].tsx": $authors_id_, "./routes/authors/[id].tsx": $authors_id_,
"./routes/authors/index.tsx": $authors_index, "./routes/authors/index.tsx": $authors_index,
"./routes/contact/index.tsx": $contact_index,
"./routes/index.tsx": $index, "./routes/index.tsx": $index,
"./routes/posts/[id].tsx": $posts_id_, "./routes/posts/[id].tsx": $posts_id_,
"./routes/posts/index.tsx": $posts_index, "./routes/posts/index.tsx": $posts_index,
"./routes/projects/index.tsx": $projects_index,
}, },
islands: { islands: {
"./islands/Counter.tsx": $Counter, "./islands/Counter.tsx": $Counter,
"./islands/PostCard.tsx": $PostCard, "./islands/ProjectCard.tsx": $ProjectCard,
"./islands/PostCarousel.tsx": $PostCarousel,
}, },
baseUrl: import.meta.url, baseUrl: import.meta.url,
} satisfies Manifest; } satisfies Manifest;

View File

@ -1,22 +0,0 @@
import { truncateString } from "../lib/truncate.ts";
export const PostCard = function PostCard({ post }: { post: Post }) {
return (
<div class="p-6 bg-gray-700 rounded-lg shadow-md">
<h2 class="text-grey-900 text-lg font-bold mb-2">{post.title}</h2>
<p>
Written by {post.first_name} {post.last_name} at {post.created_at}
</p>
<p>{truncateString(post.body, 15)}</p>
</div>
);
};
export interface Post {
post_id: number;
first_name: string;
last_name: string;
title: string;
body: string;
created_at: string;
}

View File

@ -1,21 +0,0 @@
import { Post, PostCard } from "./PostCard.tsx";
interface PostOpts {
posts: Post[];
}
export const PostCarousel = function PostCarousel({ posts }: PostOpts) {
return (
<div class="post-carousel">
<div class="h-screen bg-gray-700 flex items-center justify-center">
<div class="flex bg-gray-700 space-x-4">
{posts.map((post: Post, idx: number) => {
if (idx < 3) {
return <PostCard post={post} />;
}
})}
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,46 @@
export const ProjectCard = function ProjectCard(props: ProjectProps) {
return (
<div
class={`group space-y-1 rounded-md ${
props.wip ? "border-2 border-dashed" : "cursor-pointer"
} bg-[#45475a] px-3 py-2 m-10 shadow-md transition-all duration-300 ease-in-out hover:shadow-xl hover:scale-105`}
onClick={() => props.repo && open(props.repo, "_blank")}
>
<div class="flex items-center justify-between">
<h2 class="text-lg text-white font-black uppercase">
<a href={props.repo} target="_blank">
{props.title}
</a>
</h2>
<div class="bg-[#585b70] text-[#a6adc8] text-xs font-bold uppercase px-2.5 py-0.5 rounded-full">
{props.repo && (
<a
class="hover:underline"
href={props.repo}
target="_blank"
onClick={(e) => e.stopPropagation()}
>
Active
</a>
)}
{!props.repo && !props.wip && <span>Dead</span>}
{props.wip && <span>WIP</span>}
</div>
</div>
<p class="whitespace-pre-wrap italic font-light text-lg text-[#a6adc8]">
{props.summary}
</p>
<p class="whitespace-pre-wrap text-sm font-semibold text-[#a6adc8]">
{props.tech}
</p>
</div>
);
};
type ProjectProps = {
title: string;
repo?: string;
summary: string;
tech: string;
wip?: boolean;
};

View File

@ -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) => {};

View File

@ -1,57 +0,0 @@
import { useEffect } from "preact/hooks";
import { signal } from "@preact/signals";
export function useFetch<T>(
url: string,
options?: FetchOptions,
): {
data: T | null;
loading: boolean;
error: Error | null;
refetch: (newURL?: string, newOptions?: FetchOptions) => void;
} {
const data = signal<T | null>(null);
const loading = signal<boolean>(true);
const error = signal<Error | null>(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<string, string>;
body: string;
}

View File

@ -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<PageData> = {
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<PageData>) {
const { authorData, authorPostData, error } = data;
if (error) {
return (
<div>
<h1>Error Loading Author Information</h1>
<p>{error}</p>
</div>
);
}
if (!authorData) {
return <div>No author found</div>;
}
return (
<>
<div>
<AuthorCard author={authorData} isIdentified={true} />
</div>
<div>
<PostCarousel posts={authorPostData} />
</div>
</>
);
}
interface PageData {
error?: string;
authorData: AuthorResponse;
authorPostData: Array<Post>;
}
export type AuthorResponse = {
author_id: number;
first_name: string;
last_name: string;
bio: string;
image?: string;
};

View File

@ -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<FormState>) {
return (
<div class="max-w-md mx-auto p-6 bg-[#313244] rounded-lg shadow-md">
<Head>
<title>Contact Us</title>
</Head>
<h1 class="text-2xl font-bold mb-6 text-center">Contact Form</h1>
{/* Check if form was successfully submitted */}
{data?.submitted && (
<div
class="bg-[#a6e3a1] text-[#cdd6f4] px-4 py-3 rounded relative"
role="alert"
>
Your message has been sent successfully!
</div>
)}
<form method="POST" class="space-y-4">
{/* Name Input */}
<div>
<label
htmlFor="name"
class="block text-[#cdd6f4] text-sm font-bold mb-2"
>
Name
</label>
<input
type="text"
id="name"
name="name"
required
placeholder="Your Name"
value={data?.name || ""}
class={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500
${data?.errors?.name ? "border-red-500" : "border-gray-300"}`}
/>
{data?.errors?.name && (
<p class="text-red-500 text-xs italic mt-1">{data.errors.name}</p>
)}
</div>
{/* Email Input */}
<div>
<label
htmlFor="email"
class="block text-[#cdd6f4] text-sm font-bold mb-2"
>
Email
</label>
<input
type="email"
id="email"
name="email"
required
placeholder="your@email.com"
value={data?.email || ""}
class={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500
${data?.errors?.email ? "border-red-500" : "border-gray-300"}`}
/>
{data?.errors?.email && (
<p class="text-red-500 text-xs italic mt-1">{data.errors.email}</p>
)}
</div>
{/* Message Textarea */}
<div>
<label
htmlFor="message"
class="block text-[#cdd6f4] text-sm font-bold mb-2"
>
Message
</label>
<textarea
id="message"
name="message"
required
placeholder="Write your message here..."
rows={4}
class={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500
${data?.errors?.message ? "border-red-500" : "border-gray-300"}`}
>
{data?.message || ""}
</textarea>
{data?.errors?.message && (
<p class="text-red-500 text-xs italic mt-1">
{data.errors.message}
</p>
)}
</div>
{/* Submit Button */}
<div>
<button
type="submit"
class="w-full bg-[#89b4fa] text-[#cdd6f4] py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Send Message
</button>
</div>
</form>
</div>
);
}
// 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,
});
},
};

View File

@ -1,25 +1,27 @@
import { PhotoCircle } from "../components/PhotoCircle.tsx";
export default function Home() { export default function Home() {
return ( return (
<body> <body>
<div class="min-w-screen flex flex-col items-center justify-between bg-gray-100 dark:bg-gray-700 sm:min-h-screen"> <div class="min-w-screen flex flex-col items-center justify-between bg-[#313244] sm:min-h-screen">
<div class="sm:mt-14 sm:mb-14 mt-12 mb-4 flex flex-col items-center gap-y-5 gap-x-10 md:flex-row"> <div class="sm:mt-14 sm:mb-14 mt-12 mb-4 flex flex-col items-center gap-y-5 gap-x-10 md:flex-row">
<img <PhotoCircle
class="my-6" src="https://websites.us-east-1.linodeobjects.com/IMG_1480-min.png"
src="/logo.svg" alt="Wyatt's profile photo"
width="128"
height="128"
alt="the Fresh logo: a sliced lemon dripping with juice"
/> />
<div class="space-y-2 text-center md:text-left"> <div class="space-y-2 text-center md:text-left">
<h1 class="text-2xl text-white font-bold sm:text-4xl"> <h1 class="text-2xl text-[#f5e0dc] font-bold sm:text-4xl">
Heya! I'm Wyatt Miller Heya! I'm Wyatt
</h1> </h1>
<h2 class="text-md font-medium text-cyan-700 dark:text-cyan-200 sm:text-xl"> <h2 class="text-md font-medium text-[#f2cdcd] sm:text-xl">
Thanks for checking out this corner of the Internet! Thanks for checking out this corner of the Internet!
</h2> </h2>
<h3 class="text-sm font-light italic text-[#f5e0dc] sm:text-md">
I design and develop software for developers, for end users, and
everyone in between
</h3>
</div> </div>
</div> </div>
{/** about me stuff */}
</div> </div>
</body> </body>
); );

View File

@ -1,16 +1,28 @@
import { FreshContext, Handlers, PageProps } from "$fresh/server.ts"; import { FreshContext, Handlers, PageProps } from "$fresh/server.ts";
import { useFetch } from "../../lib/useFetch.tsx";
interface PostResponse { interface PageData {
post_id: number; post_id: number;
first_nane: string; first_nane: string;
last_name: string; last_name: string;
title: string; title: string;
body: string; body: string;
created_at: string; created_at: string;
is_featured: boolean;
} }
export default function PostIdentifier(props: PageProps) { export const handler: Handlers<PageData> = {
console.log(props.data); async GET(_req: Request, ctx: FreshContext) {
return <div>BLOG POST #{props.params.id}</div>; 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<PageData>) {
return <div className=""></div>;
} }

View File

@ -1,61 +1,37 @@
// import { FreshContext, Handlers, PageProps } from "$fresh/server.ts"; import { PostCarousel } from "../../components/PostCarousel.tsx";
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<PostItems>;
// popular_posts: Array<PostItems>;
// hot_posts: Array<PostItems>;
// }
//
// export interface PostResponseOne {
// post_id: number;
// first_name: string;
// last_name: string;
// title: string;
// body: string;
// created_at: string;
// }
//
// export const handler: Handlers<Array<PostCard>> = {
// 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 { Handlers, PageProps } from "$fresh/server.ts";
import { Post } from "../../islands/PostCard.tsx"; import { Post } from "../../components/PostCard.tsx";
interface PageData { interface PageData {
featuredPosts: Post[]; featuredPosts: Post[];
recentPosts: Post[];
hotPosts: Post[]; hotPosts: Post[];
popularPosts: Post[]; popularPosts: Post[];
} }
export const handler: Handlers<PageData> = { export const handler: Handlers<PageData> = {
async GET(_: any, ctx: any) { async GET(_: any, ctx: any) {
const [featuredResult, hotResult, popularResult] = await Promise.all([ const [featuredResult, recentResult, hotResult, popularResult] =
fetch(`${Deno.env.get("BASE_URI")}/posts/all`), await Promise.all([
fetch(`${Deno.env.get("BASE_URI")}/posts/hot`), fetch(`${Deno.env.get("BASE_URI_API")}/posts/featured`),
fetch(`${Deno.env.get("BASE_URI")}/posts/popular`), 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 // parse all JSON responses concurrently
const [featuredPosts, hotPosts, popularPosts] = await Promise.all([ const [featuredPosts, recentPosts, hotPosts, popularPosts] = await Promise
featuredResult.json(), .all([
hotResult.json(), featuredResult.json(),
popularResult.json(), recentResult.json(),
]); hotResult.json(),
popularResult.json(),
]);
return ctx.render({ return ctx.render({
featuredPosts, featuredPosts,
recentPosts,
hotPosts, hotPosts,
popularPosts, popularPosts,
}); });
@ -63,20 +39,45 @@ export const handler: Handlers<PageData> = {
}; };
export default function PostPage({ data }: PageProps<PageData>) { export default function PostPage({ data }: PageProps<PageData>) {
const { featuredPosts, hotPosts, popularPosts } = data; const { featuredPosts, recentPosts, hotPosts, popularPosts } = data;
return ( return (
<div class="space-y-12 py-8"> <div class="space-y-12 px-10 py-8 bg-[#313244]">
<h1 class="text-3xl text-white font-bold uppercase text-center">Blog</h1>
<section> <section>
<h2 class="text-2xl font-bold mb-4">Featured Posts</h2> <h2 class="text-2xl font-bold mb-2 text-white text-center lg:text-left">
Featured Posts
</h2>
<div className="text-lg font-thin italic mb-4 text-white text-center lg:text-left">
Ignite the impossible
</div>
<PostCarousel posts={featuredPosts} /> <PostCarousel posts={featuredPosts} />
</section> </section>
<section> <section>
<h2 class="text-2xl font-bold mb-4">Recent Posts</h2> <h2 class="text-2xl font-bold mb-2 text-white text-center lg:text-left">
Recent Posts
</h2>
<div className="text-lg font-thin italic mb-4 text-white text-center lg:text-left">
Now with 100% fresh perspective
</div>
<PostCarousel posts={recentPosts} />
</section>
<section>
<h2 class="text-2xl font-bold mb-2 text-white text-center lg:text-left">
Hot Posts
</h2>
<div className="text-lg font-thin italic mb-4 text-white text-center lg:text-left">
Making chaos look cool since forever
</div>
<PostCarousel posts={hotPosts} /> <PostCarousel posts={hotPosts} />
</section> </section>
<section> <section>
<h2 class="text-2xl font-bold mb-4">Popular Posts</h2> <h2 class="text-2xl font-bold mb-2 text-white text-center lg:text-left">
Popular Posts
</h2>
<div className="text-lg font-thin italic mb-4 text-white text-center lg:text-left">
Content may cause uncontrollable reading
</div>
<PostCarousel posts={popularPosts} /> <PostCarousel posts={popularPosts} />
</section> </section>
</div> </div>

View File

@ -0,0 +1,67 @@
import { ProjectCard } from "../../islands/ProjectCard.tsx";
export default function Projects() {
return (
<div class="space-y-12 px-10 py-8 sm:min-h-screen bg-[#313244]">
<section
id="projects"
class="lg:grid-cols-desktop grid scroll-mt-16 grid-cols-1 gap-x-10 gap-y-4 bg-[#313244] "
>
<h1 class="text-3xl text-white font-bold uppercase text-center">
Projects
</h1>
<div class="grid grid-cols-1 sm:grid-cols-2 ">
<ProjectCard
wip
title="Website v2"
summary="This website was built by yours truly!"
// repo="https://scm.wyattjmiller.com/wymiller/my-website-v2"
tech="Typescript, Deno, Fresh, Tailwind, Rust, PostgreSQL, Docker"
/>
<ProjectCard
title="BallBot"
repo="https://scm.wyattjmiller.com/wymiller/ballbot"
summary="A Discord bot that tells me NFL games, teams, and more!"
tech="Rust, Discord SDK, Docker"
/>
<ProjectCard
title="Nix configurations"
repo="https://scm.wyattjmiller.com/wymiller/nix-config-v2"
summary="My 'master' declarative system configuration for multiple computers"
tech="Nix"
/>
<ProjectCard
wip
title="omega"
summary="Work-in-progress music bot for Discord that plays music from different music sources"
tech="Rust, Discord SDK, SurrealDB, yt-dlp"
/>
<ProjectCard
title="gt"
repo="https://scm.wyattjmiller.com/wymiller/gt"
summary="Command line application to interact with Gitea"
tech="Rust"
/>
<ProjectCard
title="The Boyos Bot"
repo="https://github.com/NoahFlowa/BoyosBot"
summary="All-in-one Discord bot, built with my friend, NoahFlowa"
tech="Javascript, Node, Discord SDK, Docker"
/>
<ProjectCard
title="drillsergeant"
repo="https://scm.wyattjmiller.com/wymiller/drillsergeant"
summary="Git commit counter, to scratch an itch I had"
tech="C#, .NET"
/>
<ProjectCard
title="bleak"
repo="https://scm.wyattjmiller.com/wymiller/bleak"
summary="Turns your Raspberry Pi into a lighting controller"
tech="Rust"
/>
</div>
</section>
</div>
);
}