code dump frontend
This commit is contained in:
parent
44f1f35caa
commit
9e8cf4b147
42
frontend/components/AuthorCard.tsx
Normal file
42
frontend/components/AuthorCard.tsx
Normal 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;
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
<div class="bg-[#313244] flex justify-center space-x-6 p-4">
|
||||||
{headerLinks.map((l) => {
|
{headerLinks.map((l) => {
|
||||||
const newTab = l.newTab ? "_blank" : "_self";
|
const newTab = l.newTab ? "_blank" : "_self";
|
||||||
return (
|
return (
|
||||||
<div class="">
|
<a
|
||||||
<a href={l.linkTo} target={newTab} class="">
|
href={l.linkTo}
|
||||||
{l.name}
|
target={newTab}
|
||||||
</a>
|
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>
|
</div>
|
||||||
|
</a>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
29
frontend/components/PhotoCircle.tsx
Normal file
29
frontend/components/PhotoCircle.tsx
Normal 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;
|
||||||
|
};
|
33
frontend/components/PostCard.tsx
Normal file
33
frontend/components/PostCard.tsx
Normal 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;
|
||||||
|
};
|
19
frontend/components/PostCarousel.tsx
Normal file
19
frontend/components/PostCarousel.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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"
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
46
frontend/islands/ProjectCard.tsx
Normal file
46
frontend/islands/ProjectCard.tsx
Normal 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;
|
||||||
|
};
|
14
frontend/lib/convertUtc.ts
Normal file
14
frontend/lib/convertUtc.ts
Normal 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) => {};
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
||||||
|
};
|
171
frontend/routes/contact/index.tsx
Normal file
171
frontend/routes/contact/index.tsx
Normal 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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
.all([
|
||||||
featuredResult.json(),
|
featuredResult.json(),
|
||||||
|
recentResult.json(),
|
||||||
hotResult.json(),
|
hotResult.json(),
|
||||||
popularResult.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>
|
||||||
|
67
frontend/routes/projects/index.tsx
Normal file
67
frontend/routes/projects/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user