restyle interim commit

This commit is contained in:
Thomas Bishop 2025-11-04 19:06:51 +00:00
parent dbcc643222
commit 33d553fb99
30 changed files with 594 additions and 1365 deletions

10
package-lock.json generated
View file

@ -21,7 +21,6 @@
"clsx": "^2.1.1",
"gray-matter": "^4.0.3",
"i": "^0.3.7",
"lucide-react": "^0.509.0",
"marked": "^15.0.12",
"react": "^19.1.0",
"react-dom": "^19.1.0",
@ -5150,15 +5149,6 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.509.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.509.0.tgz",
"integrity": "sha512-xCJHn6Uh5qF6PGml25vveCTrHJZcqS1G1MVzWZK54ZQsOiCVJk4fwY3oyo5EXS2S+aqvTpWYIfJN+PesJ0quxg==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",

View file

@ -24,7 +24,6 @@
"clsx": "^2.1.1",
"gray-matter": "^4.0.3",
"i": "^0.3.7",
"lucide-react": "^0.509.0",
"marked": "^15.0.12",
"react": "^19.1.0",
"react-dom": "^19.1.0",

View file

@ -13,7 +13,8 @@ const renderer = {
}
const highlighter = await createHighlighter({
themes: ["dark-plus"],
themes: ["gruvbox-dark-hard"],
langs: [
"javascript",
"typescript",
@ -48,7 +49,8 @@ const posts = files.map((file) => {
(match, lang, code) => {
return highlighter.codeToHtml(code.trim(), {
lang: lang || "text",
theme: "dark-plus",
theme: "gruvbox-dark-hard",
transformers: [transformerColorizedBrackets()],
})
}

View file

@ -1,135 +0,0 @@
// @ts-nocheck
import { Button } from "@/components/ui/button"
import { useTheme } from "@/context/ThemeProvider"
import { MoonStar, MenuIcon } from "lucide-react"
import { Link } from "react-router"
import headerImage from "../images/radigue_gruvbox.png"
import controlPanel from "../images/control-panel.png"
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
NavigationMenuViewport,
} from "@/components/ui/navigation-menu"
import { Toggle } from "@/components/ui/toggle"
const Menu = () => {
return (
<NavigationMenu>
<NavigationMenuList>
{/* Desktop menu - hidden on mobile, visible on md+ */}
<div className="hidden md:flex md:space-x-3">
<NavigationMenuItem className="">
<NavigationMenuLink
asChild
className={`${navigationMenuTriggerStyle()} border-0 rounded bg-accent/20 backdrop-blur-md hover:bg-accent/40 active:bg-accent/40 focus-visible:outline-none font-semibold transition-colors`}
>
<Link to="/posts/page/1">Posts</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem className="">
<NavigationMenuLink
asChild
className={`${navigationMenuTriggerStyle()} border-0 rounded bg-accent/20 backdrop-blur-md hover:bg-accent/40 active:bg-accent/40 focus-visible:outline-none font-semibold transition-colors`}
>
<Link to="/about">About</Link>
</NavigationMenuLink>
</NavigationMenuItem>
</div>
{/* Mobile dropdown - visible only on small screens */}
<NavigationMenuItem className="md:hidden px-4">
<NavigationMenuTrigger className="border-0 rounded bg-accent/20 backdrop-blur-md hover:bg-accent/40 active:bg-accent/40 focus-visible:outline-none font-semibold border-none">
<MenuIcon />
</NavigationMenuTrigger>
<NavigationMenuContent className="bg-accent/20 border-none active:border-none">
<NavigationMenuLink asChild>
<Link to="/posts/page/1" className="block">
Posts
</Link>
</NavigationMenuLink>
<NavigationMenuLink asChild>
<Link to="/about" className="block">
About
</Link>
</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
<NavigationMenuViewport />
</NavigationMenu>
)
}
const Header = () => {
const { theme, setTheme } = useTheme()
return (
<header className="w-full h-15 flex items-center justify-center border-b border-border border-b-1 fixed top-0 z-20 bg-sidebar">
<div
className="absolute inset-0 opacity-70 dark:opacity-40"
style={{
backgroundImage: `url(${headerImage})`,
backgroundSize: "cover",
backgroundPosition: "center 30%",
backgroundRepeat: "no-repeat",
filter: "blur(0px) grayscale(10%)",
}}
/>
<div className="absolute inset-0 bg-gradient-to-b from-background/40 via-background/30 to-background/40 dark:from-background/60 dark:via-background/50 dark:to-background/60" />
<div className="w-full px-0 md:px-4 flex items-center justify-between">
<div className="flex items-center md:px-0 px-4">
<Button
variant="ghost"
asChild
className="border-0 rounded bg-accent/40 backdrop-blur-sm hover:bg-background/40 active:bg-background/40 focus-visible:outline-none transition-colors"
// className="border-0 rounded-none bg-background/20 backdrop-blur-sm hover:bg-background/40 transition-colors"
// className="border-0 rounded-none bg-background/40 backdrop-blur-md hover:bg-background transition-colors"
//
//className="border md:border-none rounded-none z-500"
>
<Link to="/">
<span className="text-xl tracking-normal font-semibold">
Systems Obscure
</span>
</Link>
</Button>
</div>
<div className="flex itemr justify-between md:gap-4 z-500">
{/*
<Toggle
className="border bg-background rounded-none"
pressed={theme === "dark"}
onPressedChange={() =>
setTheme(theme === "dark" ? "light" : "dark")
}
>
<MoonStar size={10} />
</Toggle>
*/}
<Menu />
{/*
<div className="hidden md:block">
<Button variant="ghost">
<GitMerge /> Forgejo
</Button>
</div>
*/}
</div>
</div>
</header>
)
}
export { Header }

View file

@ -1,46 +0,0 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View file

@ -1,59 +0,0 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View file

@ -1,92 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View file

@ -1,173 +0,0 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function NavigationMenu({
className,
children,
viewport = true,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean
}) {
return (
<NavigationMenuPrimitive.Root
data-slot="navigation-menu"
data-viewport={viewport}
className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
)
}
function NavigationMenuList({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
"group flex flex-1 list-none items-center justify-center gap-1",
className
)}
{...props}
/>
)
}
function NavigationMenuItem({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn("relative", className)}
{...props}
/>
)
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
)
function NavigationMenuTrigger({
className,
children,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
{/*
<ChevronDownIcon
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
*/}
</NavigationMenuPrimitive.Trigger>
)
}
function NavigationMenuContent({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className
)}
{...props}
/>
)
}
function NavigationMenuViewport({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center"
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
{...props}
/>
</div>
)
}
function NavigationMenuLink({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function NavigationMenuIndicator({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return (
<NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator"
className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className
)}
{...props}
>
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator>
)
}
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
}

View file

@ -1,127 +0,0 @@
import * as React from "react"
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
)
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />
}
type PaginationLinkProps = {
isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
)
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
)
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
)
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}

View file

@ -1,45 +0,0 @@
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Toggle({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Toggle, toggleVariants }

View file

@ -12,46 +12,59 @@ import { convertDate } from "@/utils/convertDate"
const PostListing = ({ posts, title, showAllButton }) => {
return (
<>
<div className="mb-5">
<h2 className="scroll-m-20 text-[1.5rem] font-bold ">{title}</h2>
<div className="container mx-auto p-4 grow">
<div className="space-my-8">
<section className="container">
<h2 className="text-2xl font-medium mb-4">{title}</h2>
{posts.map((post) => (
<ul>
<li className="mb-4">
<div className="flex justify-between items-center relative gap-2 hover:bg-[#504945]">
<span className="overflow-hidden whitespace-nowrap text-muted-foreground shrink-0">
{convertDate(post.date)}
</span>
<Link
to={`/posts/${post.slug}`}
key={post.slug}
className="text-right overflow-hidden text-ellipsis whitespace-nowrap min-w-0 flex-1"
>
{post.title}
</Link>
</div>
</li>
</ul>
))}
</section>
</div>
<div className="grid grid-cols-1 md:grid-cols-1 gap-3">
{posts.map((post) => (
<Link
to={`/posts/${post.slug}`}
key={post.slug}
className="block no-underline"
>
<Card
key={post.slug}
className="flex flex-col h-full hover:bg-primary/5 py-4 rounded border-none "
>
<CardHeader className="">
<CardTitle className="leading-snug font-bold ">
{post.title}
</CardTitle>
<CardDescription className="text-sm text-muted-foreground">
<div className="flex justify-between gap-2">
<span className="text-sm">{convertDate(post.date)}</span>
</div>
</CardDescription>
</CardHeader>
</Card>
</Link>
))}
</div>
{showAllButton && (
<Button
asChild
variant="secondary"
className="w-full mt-4 rounded bg-accent/20 hover:bg-accent/40"
>
<Link to="/posts/page/1">View all</Link>
</Button>
)}
</>
</div>
)
}
export default PostListing
{
/*
<Link
to={`/posts/${post.slug}`}
key={post.slug}
className="block no-underline"
>
<Card
key={post.slug}
className="flex flex-col h-full hover:bg-primary/5 py-4 rounded border-none "
>
<CardHeader className="">
<CardTitle className="leading-snug font-bold ">
{post.title}
</CardTitle>
<CardDescription className="text-sm text-muted-foreground">
<div className="flex justify-between gap-2">
<span className="text-sm">{convertDate(post.date)}</span>
</div>
</CardDescription>
</CardHeader>
</Card>
</Link>
*/
}

View file

@ -1,74 +0,0 @@
import * as React from "react"
import { createContext, useContext, useEffect, useState } from "react"
type Theme = "dark" | "light" | "system"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: "dark",
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = "dark",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider")
return context
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="284.27203mm"
height="284.27203mm"
viewBox="0 0 284.27203 284.27203"
version="1.1"
id="svg5"
xml:space="preserve"
sodipodi:docname="gruvbox-computer.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.33275303"
inkscape:cx="1158.5169"
inkscape:cy="575.50189"
inkscape:window-width="2038"
inkscape:window-height="1262"
inkscape:window-x="579"
inkscape:window-y="1459"
inkscape:window-maximized="0"
inkscape:current-layer="svg5" /><defs
id="defs2" /><rect
style="fill:#e78a4e;fill-opacity:1;stroke-width:0.440112;stroke-linecap:square;stroke-linejoin:bevel"
id="rect1"
width="284.27203"
height="284.27203"
x="2.1542969e-06"
y="4.1542967e-06" /><g
id="g4"><path
stroke="#848484"
d="M 79.951508,4.441751 H 239.85454 M 71.068008,13.325252 h 8.8835 m 151.019532,0 h 8.8835 M 62.184508,22.208754 h 8.8835 m 151.019522,0 h 17.76701 M 53.301008,31.092255 h 8.8835 m 159.903022,0 h 17.76701 M 53.301008,39.975756 h 8.8835 m 17.767,0 H 204.32053 m 17.767,0 h 17.76701 M 53.301008,48.859257 h 8.8835 m 159.903022,0 h 17.76701 M 53.301008,57.742759 h 8.8835 m 159.903022,0 h 17.76701 M 53.301008,66.62626 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.88351 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m 8.8835,0 h 17.767 m -213.204032,8.8835 H 230.97104 m 8.8835,0 h 17.767 m -26.6505,8.8835 h 17.767 m 8.8835,0 h 8.8835 M 35.534008,173.22828 H 239.85454 m 8.8835,0 h 17.767 m -239.854532,8.8835 h 8.8835 m 204.320532,0 h 26.6505 m -239.854532,8.8835 h 8.8835 m 204.320532,0 h 26.6505 m -239.854532,8.8835 h 8.8835 m 97.718512,0 h 88.83501 m 17.76701,0 h 26.6505 m -239.854532,8.8835 h 8.8835 m 186.553522,0 h 53.30101 m -248.738032,8.8835 h 8.8835 m 26.6505,0 h 17.767 m 124.369022,0 h 8.8835 m 35.53401,0 h 8.8835 m 17.767,0 h 8.8835 m -257.621532,8.8835 h 26.6505 m 17.767,0 h 8.8835 m 17.76701,0 h 8.883502 m 17.767,0 h 8.8835 m 17.767,0 h 8.88351 m 17.767,0 h 8.8835 m 8.8835,0 h 79.95151 m -257.621542,8.8835 h 8.88351 m 8.8835,0 h 8.8835 m 17.767,0 h 8.8835 m 17.767,0 h 8.88351 m 17.767002,0 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.8835 m 8.8835,0 h 8.8835 m 62.18451,0 h 17.767 M 8.883498,244.29629 h 8.8835 m 8.88351,0 h 8.8835 m 8.8835,0 h 17.767 m 8.8835,0 h 17.767 m 17.767012,0 h 8.8835 m 8.8835,0 h 17.767 m 8.8835,0 h 17.76701 m 79.95151,0 h 26.6505 M 0,253.17979 h 8.883498 m 168.786532,0 h 8.8835 m 53.30101,0 h 26.6505 M 0,262.06329 h 8.883498 m 168.786532,0 h 79.95151 m -26.6505,8.8835 h 8.8835"
id="path1051"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#7c6f64;stroke-opacity:1" /><path
stroke="#c6c6c6"
d="M 79.951508,13.325252 H 222.08753 M 71.068008,31.092255 H 222.08753 M 71.068008,39.975756 h 8.8835 m 133.252522,0 h 8.8835 M 71.068008,48.859257 h 8.8835 m 133.252522,0 h 8.8835 M 71.068008,57.742759 h 8.8835 m 133.252522,0 h 8.8835 M 71.068008,66.62626 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.88351 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 H 222.08753 m 35.53401,8.8835 h 8.8835 m -17.767,8.8835 h 8.8835 m -17.767,8.88351 h 8.8835 m -204.320532,17.767 H 239.85454 m -195.437032,8.8835 h 88.835012 m 88.83501,0 h 17.76701 m -195.437032,8.8835 h 17.767 m 17.767,0 h 53.301012 m -88.835012,8.8835 h 17.767 m 17.767,0 H 204.32053 m 8.8835,0 h 8.8835 m 17.76701,0 h 8.8835 m 17.767,0 h 8.8835 m -88.83501,35.53401 h 8.8835 m 35.53401,0 h 8.8835 M 8.883498,262.06329 H 177.67003 m 8.8835,8.8835 h 44.41751"
id="path1053"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#a89984;stroke-opacity:1" /><path
stroke="#ffffff"
d="m 222.08753,13.325252 h 8.88351 M 71.068008,22.208754 H 222.08753 M 62.184508,31.092255 h 8.8835 m -8.8835,8.883501 h 8.8835 m 133.252522,0 h 8.8835 M 62.184508,48.859257 h 8.8835 m 133.252522,0 h 8.8835 M 62.184508,57.742759 h 8.8835 m 133.252522,0 h 8.8835 M 62.184508,66.62626 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.88351 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 8.8835,0 H 213.20403 m -151.019522,8.8835 h 8.8835 m -35.534,35.53401 H 239.85454 m -204.320532,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m 177.670022,0 h 17.76701 m 17.767,0 h 8.8835 m -204.320532,8.8835 h 8.8835 m 17.767,0 h 8.88351 m 17.767002,0 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 17.767,0 h 8.8835 m 17.767,0 h 8.8835 m 17.767012,0 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.8835 m 26.6505,0 h 62.18451 m -239.854542,8.88351 h 8.88351 m 159.903022,0 h 62.18451 M 8.883498,253.17979 H 177.67003 m 17.767,0 h 35.53401"
id="path1055"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#d4be98;stroke-opacity:1" /><path
stroke="#000000"
d="m 239.85454,13.325252 h 8.8835 m -8.8835,8.883502 h 8.8835 m -8.8835,8.883501 h 8.8835 m -8.8835,8.883501 h 8.8835 M 79.951508,48.859257 H 195.43703 m 44.41751,0 h 8.8835 M 79.951508,57.742759 h 8.8835 m 151.019532,0 h 8.8835 M 79.951508,66.62626 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.8835 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.8835 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.8835 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.88351 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.8835 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.8835 h 8.8835 m 151.019532,0 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -17.767,8.8835 h 8.8835 m 26.6505,0 h 8.8835 m -222.087532,8.8835 H 230.97104 m 35.534,0 h 8.8835 m -8.8835,8.88351 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -142.13602,8.8835 h 88.83501 m -168.786522,17.767 h 8.8835 m 17.767,0 h 8.8835 m 17.767012,0 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.8835 m 17.767,0 h 8.8835 m 79.95151,0 h 8.8835 m -239.854532,8.8835 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.883502 m 17.767,0 h 8.8835 m 17.767,0 h 8.88351 m 17.767,0 h 8.8835 m 88.83501,0 h 8.8835 m -248.738032,8.88351 h 8.8835 m 17.767,0 h 8.8835 m 17.767,0 h 8.88351 m 17.767002,0 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.8835 m 97.71851,0 h 8.8835 m -17.767,8.8835 h 8.8835 m -17.767,8.8835 h 8.8835 M 8.883498,270.94679 H 177.67003 m 62.18451,0 h 17.767 m -71.06801,8.8835 h 53.30101"
id="path1057"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#282828;stroke-opacity:1" /><path
stroke="#000084"
d="m 195.43703,48.859257 h 8.8835 m -8.8835,8.883502 h 8.8835 m -8.8835,8.883501 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.88351 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -124.369022,8.8835 H 204.32053"
id="path1059"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#2e3b3b;stroke-opacity:1" /><path
stroke="#0000ff"
d="M 88.835008,57.742759 H 195.43703 M 88.835008,66.62626 h 8.88351 m 8.883502,0 h 88.83501 m -106.602022,8.8835 h 8.88351 m 8.883502,0 h 88.83501 M 88.835008,84.39326 H 195.43703 M 88.835008,93.27676 H 195.43703 M 88.835008,102.16027 H 195.43703 m -106.602022,8.8835 H 195.43703 m -106.602022,8.8835 H 195.43703"
id="path1061"
style="fill:none;fill-opacity:1;stroke-width:8.8835;shape-rendering:crispEdges;stroke:#7daea3;stroke-opacity:1" /><path
stroke="#01feff"
d="m 97.718518,66.62626 h 8.883502 m -8.883502,8.8835 h 8.883502"
id="path1063"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#87efcc;stroke-opacity:1" /><path
stroke="#008400"
d="m 62.184508,208.76228 h 17.767"
id="path1065"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#c4de3d;stroke-opacity:1" /><path
stroke="#838383"
d="m 97.718518,244.29629 h 8.883502 m 71.06801,0 h 8.8835 m -8.8835,26.6505 h 8.8835"
id="path1067"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#7c6f64;stroke-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

View file

@ -1,8 +1,93 @@
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap");
@import "./styles/_variables.css";
@import "./styles/shadcn-overrides.css";
@import "./styles/shadcn-theme.css";
@import "./styles/syntax-highlighting.css";
@import "tailwindcss";
@import "tw-animate-css";
@font-face {
font-family: "iA Writer Quattro";
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url(https://cdn.jsdelivr.net/fontsource/fonts/ia-writer-quattro@latest/latin-400-normal.woff2)
format("woff2"),
url(https://cdn.jsdelivr.net/fontsource/fonts/ia-writer-quattro@latest/latin-400-normal.woff)
format("woff");
}
@font-face {
font-family: "iA Writer Mono";
font-style: normal;
font-display: swap;
font-weight: 400;
src:
url(https://cdn.jsdelivr.net/fontsource/fonts/ia-writer-mono@latest/latin-400-normal.woff2)
format("woff2"),
url(https://cdn.jsdelivr.net/fontsource/fonts/ia-writer-mono@latest/latin-400-normal.woff)
format("woff");
}
* {
outline-color: color-mix(in srgb, var(--ring) 50%, transparent);
}
html {
font-family: var(--font-sansserif);
}
body {
background-color: var(--background);
color: var(--foreground);
}
h1 {
color: var(--color-orange-light);
}
h2 {
color: var(--color-green-light);
}
.monospaced-font {
font-family: "iA Writer Mono";
}
.scanlined {
position: relative; /* Add this */
}
.scanlined::after {
content: "";
position: absolute; /* Change from fixed */
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: linear-gradient(rgba(0, 0, 0, 0.4) 1px, transparent 1px);
background-size: 2px 2px;
background-repeat: repeat;
pointer-events: none;
z-index: 9999; /* Might want to lower this too */
}
code {
font-family: var(--font-monospaced);
}
p code {
color: var(--foreground);
background: #504945;
font-size: 14px;
padding: 0.2rem 0.3rem;
border-radius: var(--radius);
font-weight: 500;
}
.shiki {
padding: 1rem 1.2rem;
border-radius: 0;
overflow-x: auto;
margin: 1.5rem 0;
line-height: 1.3;
/* counter-reset: line; */
font-family: var(--font-monospaced) !important;
font-size: 14px !important;
}

View file

@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View file

@ -11,40 +11,40 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 15 * 60 * 1000, // 15 minutes
retry: 3,
refetchOnWindowFocus: false,
},
},
defaultOptions: {
queries: {
staleTime: 15 * 60 * 1000, // 15 minutes
retry: 3,
refetchOnWindowFocus: false,
},
},
})
export default function ScrollToTop() {
const { pathname } = useLocation()
const { pathname } = useLocation()
useEffect(() => {
window.scrollTo(1, 1)
}, [pathname])
useEffect(() => {
window.scrollTo(1, 1)
}, [pathname])
return null
return null
}
ReactDOM.createRoot(document.getElementById("root")!).render(
<StrictMode>
<BrowserRouter>
<ScrollToTop />
<QueryClientProvider client={queryClient}>
<Routes>
<Route index element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/posts/page/:page" element={<PostsPage />} />
<Route path="/posts/:slug" element={<BlogTemplate />} />
<Route path="/tags/:tag" element={<TagTemplate />} />
</Routes>
<StrictMode>
<BrowserRouter>
<ScrollToTop />
<QueryClientProvider client={queryClient}>
<Routes>
<Route index element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/posts" element={<PostsPage />} />
<Route path="/posts/:slug" element={<BlogTemplate />} />
<Route path="/tags/:tag" element={<TagTemplate />} />
</Routes>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</BrowserRouter>
</StrictMode>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</BrowserRouter>
</StrictMode>
)

View file

@ -4,105 +4,105 @@ import portrait from "../images/portrait-compressed.jpg"
const AboutPage = () => {
return (
<MainTemplate>
<div className="mb-5 ">
<h2 className="scroll-m-20 text-3xl font-bold lg:text-3xl pb-3">
About
</h2>
</div>
<div className="container mx-auto p-4">
<figure className="w-full flex flex-col items-center mb-6">
<div className="scanlined inline-block">
<img
alt="A portrait of the blog author"
src={portrait}
className="w-80 flex"
/>
</div>
<figcaption className="text-sm text-muted-foreground mt-3 text-center">
Pictured with the WITCH computer at the{" "}
<a
href="https://www.tnmoc.org/"
target="_blank"
className="text-primary hover:text-primary/80"
>
National Museum of Computing
</a>
, Bletchley Park.
</figcaption>
</figure>
<figure className="w-full flex flex-col items-center mb-6">
<img
alt="A portrait of the blog author"
src={portrait}
className="w-0 flex"
/>
<figcaption className="text-sm text-muted-foreground mt-3 text-center">
Pictured with the WITCH computer at the{" "}
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
I'm a self-taught software engineer based on the south coast of
England. This blog is my technical scrapbook. I document the details
of my technical life so I can have a record of progress when I look
back.{" "}
</p>
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
I completed a degree in Philosophy at the University of Warwick (2009)
and hold a Postgraduate Certificate of Education (2011).
</p>
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
Currently I work for{" "}
<a
href="https://www.tnmoc.org/"
href="https://en.wikipedia.org/wiki/ITV_(TV_network)"
target="_blank"
className="text-primary hover:text-primary/80"
className="underline decoration-1 hover:text-primary/80 underline-offset-4"
>
National Museum of Computing
</a>
, Bletchley Park.
</figcaption>
</figure>
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
I'm a self-taught software engineer based on the south coast of England.
This blog is my technical scrapbook. I document the details of my
technical life so I can have a record of progress when I look back.{" "}
</p>
ITV
</a>{" "}
{""}
as a backend software engineer. Before that, I worked as a full-stack
engineer at the{" "}
<a
href="https://en.wikipedia.org/wiki/BBC"
target="_blank"
className="underline decoration-1 hover:text-primary/80 underline-offset-4"
>
BBC
</a>{" "}
and as a frontend engineer at{" "}
<a
href="https://www.arria.com/"
target="_blank"
className="underline decoration-1 hover:text-primary/80 underline-offset-4"
>
Arria NLG
</a>{" "}
and in several web developer roles. Before software I was a
teacher.{" "}
</p>
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
I completed a degree in Philosophy at the University of Warwick (2009)
and hold a Postgraduate Certificate of Education (2011).
</p>
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
Currently I work for{" "}
<a
href="https://en.wikipedia.org/wiki/ITV_(TV_network)"
target="_blank"
className="underline decoration-1 hover:text-primary/80 underline-offset-4"
>
ITV
</a>{" "}
{""}
as a backend software engineer. Before that, I worked as a full-stack
engineer at the{" "}
<a
href="https://en.wikipedia.org/wiki/BBC"
target="_blank"
className="underline decoration-1 hover:text-primary/80 underline-offset-4"
>
BBC
</a>{" "}
and as a frontend engineer at{" "}
<a
href="https://www.arria.com/"
target="_blank"
className="underline decoration-1 hover:text-primary/80 underline-offset-4"
>
Arria NLG
</a>{" "}
and in several web developer roles. Before software I was a
teacher.{" "}
</p>
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
Some things I like:
<ul className="pt-2">
<li className="mb-1">🐶 Staffies and other bull-breeds</li>
<li className="mb-1">🎼 Classical music (Haydn, Mozart, JSB)</li>
<li className="mb-1">🛸 Science fiction </li>
</ul>
</p>
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
Some things I like:
<ul className="pt-2">
<li className="mb-1">🐶 Staffies and other bull-breeds</li>
<li className="mb-1">🎼 Classical music (Haydn, Mozart, JSB)</li>
<li className="mb-1">🛸 Science fiction </li>
</ul>
</p>
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
Some things I'm interested in:
<ul className="pt-2">
<li className="mb-1">🧑💻 Self-hosting and digital resiliance</li>
<li className="mb-1">🖳 The history of computing and networks</li>
<li className="mb-1">🇮🇪 Irish history and culture</li>
<li className="mb-1"> Buddhism</li>
{/*
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
Some things I'm interested in:
<ul className="pt-2">
<li className="mb-1">🧑💻 Self-hosting and digital resiliance</li>
<li className="mb-1">🖳 The history of computing and networks</li>
<li className="mb-1">🇮🇪 Irish history and culture</li>
<li className="mb-1"> Buddhism</li>
{/*
<li className="mb-1">📡 Civil communications infrastructure</li>
*/}
</ul>
</p>
</ul>
</p>
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
I self-host my own Git forge at{" "}
<a
href="https://forgejo.systemsobscure.net/thomasabishop"
className="underline decoration-1 hover:text-primary/80 underline-offset-4"
>
forgejo.systemsobscure.net
</a>{" "}
rather than use Microsoft GitHub. You can view my personal projects
there.
</p>
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
I self-host my own Git forge at{" "}
<a
href="https://forgejo.systemsobscure.net/thomasabishop"
className="underline decoration-1 hover:text-primary/80 underline-offset-4"
>
forgejo.systemsobscure.net
</a>{" "}
rather than use Microsoft GitHub. You can view my personal projects
there.
</p>
</div>
</MainTemplate>
)
}

View file

@ -1,42 +1,37 @@
import MainTemplate from "@/templates/MainTemplate"
import PostListing from "@/containers/PostListing"
import { usePosts } from "@/hooks/usePosts"
import roundedPortrait from "../images/round-portrait.png"
import gruvboxComputer from "../images/gruvbox-computer.svg"
import TodayILearned from "@/containers/TodayILearned"
const HomePage = () => {
const { posts } = usePosts()
return (
<MainTemplate>
<div className="mb-7 border-none border-accent/40 py-6 px-6 md:px-8 md:pt-6 bg-accent/15 opacity-90 rounded">
<div className="flex flex-col items-center md:flex-row md:items-start gap-4 md:gap-8 md:flex-row-reverse">
{/*
<div className="flex-shrink-0">
<img
src={roundedPortrait}
alt="Profile picture"
className="rounded-image w-36 h-36 md:w-38 md:h-38 object-contain border-2 rounded-full"
/>
</div>
*/}
<div className="text-center md:text-left flex-1">
<h1 className="scroll-m-20 text-3xl font-bold">
Another software engineer with a blog
</h1>
<p className="leading-[1.7] mt-5">
I'm a self-taught software engineer from London. This blog is a
technical scrapbook and digital garden.
</p>
</div>
<div className="container mx-auto p-4 grow">
<div className="space-my-8">
<section className="space-y-4">
<div className="gap-6 flex flex-col items-center sm:flex-row">
<div className="scanlined">
<img src={gruvboxComputer} className="md:w-80 w-50" />
</div>
<div>
<h1 className="text-4xl font-bold py-3 monospaced-font text-center sm:text-left md:text-left">
<div className="scanlined inline-block italic py-1 px-2">
systems obscure
</div>
</h1>
<p className="leading-relaxed text-center sm:text-left md:text-left">
software engineer at ITV, formerly BBC. this is my technical
scrapbook and digital garden.
</p>
</div>
</div>
</section>
</div>
</div>
<PostListing
title="Recent posts"
posts={posts.slice(0, 5)}
showAllButton
/>
<PostListing title="recent posts" posts={posts.slice(0, 5)} />
</MainTemplate>
)
}

View file

@ -1,132 +1,17 @@
// @ts-nocheck
import { useMemo, useEffect } from "react"
import MainTemplate from "@/templates/MainTemplate"
import { useParams, useNavigate } from "react-router"
import { convertDate } from "@/utils/convertDate"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Link } from "react-router"
import { usePosts } from "@/hooks/usePosts"
import PostListing from "@/containers/PostListing"
const PostsPage = () => {
const { page } = useParams()
const { posts } = usePosts()
const navigate = useNavigate()
const { posts } = usePosts()
const postsPerPage = 8
const currentPage = Number(page) || 1
const totalPages = Math.ceil(posts.length / postsPerPage)
useEffect(() => {
// Only redirect if we have posts and the page is definitively invalid
if (totalPages > 0 && (currentPage < 1 || currentPage > totalPages)) {
navigate(`/posts/page/1`, { replace: true })
}
}, [currentPage, totalPages, navigate])
const currentPosts = useMemo(() => {
const startIndex = (currentPage - 1) * postsPerPage
const endIndex = startIndex + postsPerPage
return posts.slice(startIndex, endIndex)
}, [posts, currentPage, postsPerPage])
const hasNextPage = currentPage < totalPages
const hasPrevPage = currentPage > 1
const goToNextPage = () => {
if (hasNextPage) {
navigate(`/posts/page/${currentPage + 1}`)
}
}
const goToPrevPage = () => {
if (hasPrevPage) {
navigate(`/posts/page/${currentPage - 1}`)
}
}
return (
<MainTemplate>
<div className="mb-5 ">
<h2 className="scroll-m-20 text-3xl font-bold lg:text-3xl">
All posts
</h2>
</div>
<div className="flex flex-col">
<div className="grid grid-cols-1 md:grid-cols-1 gap-3 flex-grow">
{currentPosts.map((post) => (
<Link
to={`/posts/${post.slug}`}
key={post.slug}
className="block no-underline"
>
<Card
key={post.slug}
className="flex flex-col h-full hover:bg-primary/5 py-4 rounded border-none"
>
<CardHeader>
<CardTitle className="leading-snug font-bold">
{post.title}
</CardTitle>
<CardDescription className="text-sm text-muted-foreground">
<div className="flex justify-between gap-2">
<span className="text-sm">{convertDate(post.date)}</span>
<div className="hidden md:block">
{post.tags.map((tag, i) => (
<Badge
className="ml-2 cursor-pointer"
key={i}
variant="secondary"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
navigate(`/tags/${tag}`)
}}
>
{tag}
</Badge>
))}
</div>
</div>
</CardDescription>
</CardHeader>
</Card>
</Link>
))}
</div>
<Pagination className="mt-4">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
className={`select-none ${hasPrevPage ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`}
onClick={goToPrevPage}
/>
</PaginationItem>
<PaginationItem>
<PaginationNext
className={`select-none ${hasNextPage ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`}
onClick={goToNextPage}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</MainTemplate>
)
return (
<MainTemplate>
<PostListing title={null} posts={posts} />
</MainTemplate>
)
}
export { PostsPage }

View file

@ -1,114 +1,79 @@
:root {
--radius: 0.3rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--font-monospaced: "Jetbrains Mono", monospace;
--font-sansserif: "Inter", sans-serif;
--radius: 0.3rem;
--background: #282828;
--foreground: #ebdbb2;
--sidebar: #3c3836;
--color-red-light: #fb4934;
--color-orange-light: #fe8019;
--color-green-light: #b8bb26;
--color-aqua-muted: #689d6a;
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: #8ec07c;
--primary-muted: #689d6a;
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: #928374;
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--font-monospaced: "iA Writer Mono";
--font-sansserif: "iA Writer Quattro", sans-serif;
}
/* .dark { */
/* --background: oklch(0.145 0 0); */
/* --foreground: oklch(0.985 0 0); */
/* --card: oklch(0.205 0 0); */
/* --card-foreground: oklch(0.985 0 0); */
/* --popover: oklch(0.205 0 0); */
/* --popover-foreground: oklch(0.985 0 0); */
/* --primary: oklch(0.922 0 0); */
/* --primary-foreground: oklch(0.205 0 0); */
/* --secondary: oklch(0.269 0 0); */
/* --secondary-foreground: oklch(0.985 0 0); */
/* --muted: oklch(0.269 0 0); */
/* --muted-foreground: oklch(0.708 0 0); */
/* --accent: oklch(0.269 0 0); */
/* --accent-foreground: oklch(0.985 0 0); */
/* --destructive: oklch(0.704 0.191 22.216); */
/* --border: oklch(1 0 0 / 10%); */
/* --input: oklch(1 0 0 / 15%); */
/* --ring: oklch(0.556 0 0); */
/* --chart-1: oklch(0.488 0.243 264.376); */
/* --chart-2: oklch(0.696 0.17 162.48); */
/* --chart-3: oklch(0.769 0.188 70.08); */
/* --chart-4: oklch(0.627 0.265 303.9); */
/* --chart-5: oklch(0.645 0.246 16.439); */
/* --sidebar: oklch(0.205 0 0); */
/* --sidebar-foreground: oklch(0.985 0 0); */
/* --sidebar-primary: oklch(0.488 0.243 264.376); */
/* --sidebar-primary-foreground: oklch(0.985 0 0); */
/* --sidebar-accent: oklch(0.269 0 0); */
/* --sidebar-accent-foreground: oklch(0.985 0 0); */
/* --sidebar-border: oklch(1 0 0 / 10%); */
/* --sidebar-ring: oklch(0.556 0 0); */
/* } */
.dark {
--background: oklch(0.2 0 0);
/* Softer dark gray instead of 0.145 */
--foreground: oklch(0.9 0 0);
/* Softer off-white instead of 0.985 */
--card: oklch(0.22 0 0);
/* Slightly lighter card */
--card-foreground: oklch(0.9 0 0);
/* Match foreground */
--popover: oklch(0.22 0 0);
--popover-foreground: oklch(0.9 0 0);
--primary: oklch(0.85 0 0);
/* Less harsh white */
--primary-foreground: oklch(0.22 0 0);
--secondary: oklch(0.3 0 0);
/* Lighter secondary */
--secondary-foreground: oklch(0.9 0 0);
--muted: oklch(0.3 0 0);
--muted-foreground: oklch(0.65 0 0);
/* Slightly softer */
--accent: #a89984;
--accent-foreground: oklch(0.9 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 15%);
/* Slightly more visible borders */
--input: oklch(1 0 0 / 18%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.22 0 0);
/* Match card */
--sidebar-foreground: oklch(0.9 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.9 0 0);
--sidebar-accent: oklch(0.3 0 0);
--sidebar-accent-foreground: oklch(0.9 0 0);
--sidebar-border: oklch(1 0 0 / 15%);
--sidebar-ring: oklch(0.556 0 0);
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius));
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}

View file

@ -1,51 +0,0 @@
* {
/* border-color: var(--border);*/
outline-color: color-mix(in srgb, var(--ring) 50%, transparent);
}
html {
font-family: var(--font-sansserif);
}
body {
background-color: var(--background);
color: var(--foreground);
}
button,
[type="button"],
[type="reset"],
[type="submit"],
.btn,
a[href]:not([aria-disabled="true"]),
[role="button"] {
cursor: pointer;
}
.shadow,
.shadow-sm,
.shadow-md,
.shadow-lg,
.shadow-xl,
.shadow-2xl,
[class*="shadow"] {
box-shadow: none !important;
}
.card,
.dialog,
.popover,
.dropdown-menu {
box-shadow: none !important;
}
figure > img {
max-width: 600px;
min-width: 300px;
}
img.rounded-image {
max-width: auto;
min-width: auto;
}

View file

@ -1,40 +0,0 @@
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius));
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}

View file

@ -10,64 +10,67 @@ const BlogTemplate = () => {
const { posts } = usePosts()
const post = posts?.find((x) => x.slug === slug)
return (
<MainTemplate>
{!post ? (
<div>Loading...</div>
) : (
<>
<div className="mb-5">
<h2 className="text-3xl font-bold lg:text-3xl">{post?.title}</h2>
</div>
<div className="flex md:flex-row md:justify-between flex-col mb-8">
<span className="text-muted-foreground">
{convertDate(post?.date)}
</span>
<div className="flex gap-1 mt-3 md:mt-0">
{post?.tags?.map((tag, i) => (
<Badge asChild variant="secondary" className="">
<Link key={i} to={`/tags/${tag}`}>
{tag}
</Link>
</Badge>
))}
</div>
</div>
<div
className="
[&>h2]:text-xl [&>h2]:font-semibold [&>h2]:first:mt-0 [&>h2:not(:first-child)]:mt-8
[&>h3]:text-xl [&>h3]:sm:text-1xl [&>h3]:font-semibold [&>h3:not(:first-child)]:mt-5
[&>h4]:text-lg [&>h4]:sm:text-xl [&>h4]:font-semibold [&>h4:not(:first-child)]:mt-4
return (
<MainTemplate>
<div className="container mx-auto p-4 grow">
{!post ? (
<div>Loading...</div>
) : (
<article className="prose prose-lg max-w-none">
<header className="mb-6 pb-4">
<h1 className="text-4xl font-semibold mb-4 leading-tight">
{post?.title}
</h1>
<div className="flex flex-wrap align-center gap-4 text-[#928374]">
<time datetime={convertDate(post?.date)} className="text-sm ">
{convertDate(post?.date)}
</time>
<div className="flex flex-wrap gap-3 align-center">
{post?.tags?.map((tag, i) => (
<Link
className="text-primary text-sm underline underline-offset-3 hover:text-[#689d6a]"
key={i}
to={`/tags/${tag}`}
>
{tag}
</Link>
))}
</div>
</div>
</header>
<div
className="
[&>h2]:text-2xl [&>h2]:font-medium [&>h2]:my-4 [&>h2]:text-[#fabd2f]!
[&>h3]:text-xl [&>h3]:font-medium [&>h3]:my-4 [&>h3]:text-[#fabd2f]
[&>h4]:text-lg [&>h4]:font-medium [&>h4]:my-4 [&>h4]:text-[#fabd2f]
[&>p]:leading-7 [&>p:not(:first-child)]:mt-4
[&>p+:is(h1,h2,h3,h4,h5,h6)]:mt-6
[&>blockquote]:mt-4 [&>blockquote]:border-l-2 [&>blockquote]:pl-6 [&>blockquote]:text-muted-foreground
[&>ul]:my-4 [&>ul]:ml-6 [&>ul]:list-disc [&>ul>li]:mt-2
[&>table]:w-full [&>table]:my-4
[&>table>thead>tr]:m-0 [&>table>thead>tr]:border-t [&>table>thead>tr]:p-0 [&>table>thead>tr:even]:bg-muted
[&>table>thead>tr>th]:border [&>table>thead>tr>th]:px-4 [&>table>thead>tr>th]:py-2 [&>table>thead>tr>th]:text-left [&>table>thead>tr>th]:font-bold [&>table>thead>tr>th[align=center]]:text-center [&>table>thead>tr>th[align=right]]:text-right
[&>table>tbody>tr]:m-0 [&>table>tbody>tr]:border-t [&>table>tbody>tr]:p-0 [&>table>tbody>tr:even]:bg-muted
[&>table>tbody>tr>td]:border [&>table>tbody>tr>td]:px-4 [&>table>tbody>tr>td]:py-2 [&>table>tbody>tr>td]:text-left [&>table>tbody>tr>td[align=center]]:text-center [&>table>tbody>tr>td[align=right]]:text-right
[&>code]:relative [&>code]:rounded [&>code]:bg-muted [&>code]:px-[0.3rem] [&>code]:py-[0.2rem] [&>code]:font-mono [&>code]:text-sm [&>code]:font-normal
[&>table>tbody>tr:nth-child(even)]:bg-muted
[&>table>thead>tr:nth-child(even)]:bg-muted
[&_table_code]:text-sm font-normal
[&>pre]:mt-6 [&>pre]:mb-6
[&>p+pre]:mt-6
[&>pre+p]:mt-6
[&>ul+pre]:mt-6
[&>li]:leading-[1.6]
[&_li_code]:relative [&_li_code]:rounded [&_li_code]:bg-muted [&_li_code]:px-[0.3rem] [&_li_code]:py-[0.2rem] [&_li_code]:font-mono [&_li_code]:text-sm [&_li_code]:font-normal
[&>p]:mt-6 [&>p]:leading-[1.6]
[&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-muted-foreground
[&_li_code]:relative [&_li_code]:rounded [&_li_code]:bg-[#504945] [&_li_code]:px-[0.3rem] [&_li_code]:py-[0.2rem] [&_li_code]:font-mono [&_li_code]:text-sm [&_li_code]:font-normal
[&>code]:relative [&>code]:rounded [&>code]:bg-[#504945] [&>code]:px-[0.3rem] [&>code]:py-[0.2rem] [&>code]:font-mono [&>code]:text-sm [&>code]:font-normal
[&>figure]:w-full [&>figure]:flex [&>figure]:flex-col [&>figure]:items-center [&>figure]:justify-center [&>figure]:mb-6 [&>figure]:mx-auto
[&>figure>img]:w-full
[&>figure>figcaption]:text-sm [&>figure>figcaption]:text-muted-foreground [&>figure>figcaption]:mt-3 [&>figure>figcaption]:text-center
[&>figure>figcaption]:text-sm [&>figure>figcaption]:text-[#bdae93] [&>figure>figcaption]:mt-3 [&>figure>figcaption]:text-center
[&>figure>figcaption>a]:text-primary [&>figure>figcaption>a:hover]:text-primary/80
"
dangerouslySetInnerHTML={{ __html: post?.html }}
/>
</>
)}
[&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-[#689d6a] [&_a]:text-primary
[&>table]:w-full [&>table]:my-4
[&>table>thead>tr]:m-0 [&>table>thead>tr]:border-t [&>table>thead>tr]:p-0 [&>table>thead>tr:even]:bg-muted
[&>table>thead>tr>th]:border [&>table>thead>tr>th]:px-4 [&>table>thead>tr>th]:py-2 [&>table>thead>tr>th]:text-left [&>table>thead>tr>th]:font-bold [&>table>thead>tr>th[align=center]]:text-center [&>table>thead>tr>th[align=right]]:text-right
[&>table>tbody>tr]:m-0 [&>table>tbody>tr]:border-t [&>table>tbody>tr]:p-0 [&>table>tbody>tr:even]:bg-muted
[&>table>tbody>tr>td]:border [&>table>tbody>tr>td]:px-4 [&>table>tbody>tr>td]:py-2 [&>table>tbody>tr>td]:text-left [&>table>tbody>tr>td[align=center]]:text-center [&>table>tbody>tr>td[align=right]]:text-right
"
dangerouslySetInnerHTML={{ __html: post?.html }}
/>
</article>
)}
</div>
</MainTemplate>
)
}

View file

@ -1,32 +1,84 @@
// @ts-nocheck
import { Header } from "@/components/Header"
import { ThemeProvider } from "@/context/ThemeProvider"
import { useTheme } from "@/context/ThemeProvider"
const MainTemplate = (props) => {
return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<MainContent>{props.children}</MainContent>
</ThemeProvider>
)
import gruvboxComputer from "../images/gruvbox-computer.svg"
import { Link } from "react-router"
const Header = () => {
return (
<header className="py-6">
<nav className="bg-sidebar container mx-auto justify-between flex gap-1">
<div className="scanlined">
<img src={gruvboxComputer} className="w-10" />
</div>
<ul class="flex space-x-4 px-4 py-2">
<li class="flex flex-col items-center justify-center">
<Link
class="text-primary underline underline-offset-3 hover:text-[#689d6a]"
to="/"
>
home
</Link>
</li>
<li class="flex flex-col items-center justify-center">
<Link
class="text-primary underline underline-offset-3 hover:text-[#689d6a]"
to="/posts"
>
posts
</Link>
</li>
<li class="flex flex-col items-center justify-center">
<Link
class="text-primary underline underline-offset-3 hover:text-[#689d6a]"
to="/about"
>
about
</Link>
</li>
</ul>
</nav>
</header>
)
}
const MainContent = ({ children }) => {
const { theme } = useTheme()
const classes =
theme === "light"
? "min-h-screen w-full flex flex-col overflow-x-hidden mb-15"
: "min-h-screen w-full flex flex-col overflow-x-hidden mb-15"
const Footer = () => {
return (
<footer className="bg-sidebar container mx-auto px-4 mt-10 mb-8">
<nav>
<ul className="flex flex-row justify-start gap-4">
<li className="flex flex-col items-center justify-center">
<a
className="text-primary underline underline-offset-3 hover:text-[#689d6a]"
href="https://forgejo.systemsobscure.net/thomasabishop"
target="blank"
>
forgejo
</a>
</li>
<li className="">
<a
className="text-primary underline underline-offset-3 hover:text-[#689d6a]"
href="https://fosstodon.org/@systemsobscure"
target="blank"
>
fosstodon
</a>
</li>
</ul>
</nav>
</footer>
)
}
return (
<div className={classes}>
<Header />
<main className="flex-1 w-full px-2 md:px-4 flex justify-center pt-22">
<div className="w-full max-w-3xl lg:max-w-3xl xl:max-w-3xl px-2 md:px-4 md:py-3 py-0">
{children}
</div>
</main>
</div>
)
const MainTemplate = ({ children }) => {
return (
<div className="antialiased max-w-3xl mt-3 mx-auto text-[#fbf1c7] bg-[#282828] no-scanlines wrapper">
<main className="flex-auto min-w-0 mt-0 flex flex-col px-2 md:px-0">
<Header />
<div>{children}</div>
<Footer />
</main>
</div>
)
}
export default MainTemplate

View file

@ -5,16 +5,19 @@ import PostListing from "@/containers/PostListing"
import { usePosts } from "@/hooks/usePosts"
const TagTemplate = () => {
const { tag } = useParams()
const { posts } = usePosts()
const { tag } = useParams()
const { posts } = usePosts()
const filteredPosts = posts.filter((post) => post.tags.includes(tag))
const filteredPosts = posts.filter((post) => post.tags.includes(tag))
return (
<MainTemplate>
<PostListing title={`Posts tagged: #${tag}`} posts={filteredPosts} />
</MainTemplate>
)
return (
<MainTemplate>
<div className="container mx-auto p-4">
<h1 className="h1 text-3xl text-[#b8bb26]!">{`Posts tagged: #${tag}`}</h1>
</div>
<PostListing title={null} posts={filteredPosts} />
</MainTemplate>
)
}
export default TagTemplate

View file

@ -24,12 +24,19 @@ const days = [
]
const convertDate = (isoStamp: string) => {
const unixSeconds = new Date(isoStamp)
// const weekday = days[unixSeconds.getDay()]
const day = unixSeconds.getDate()
const month = months[unixSeconds.getMonth()]
const year = unixSeconds.getFullYear()
return `${day} ${month} ${year}`
//const unixSeconds = new Date(isoStamp)
//// const weekday = days[unixSeconds.getDay()]
//const day = unixSeconds.getDate()
//const month = months[unixSeconds.getMonth()]
//const year = unixSeconds.getFullYear()
//return `${day} ${month} ${year}`
console.log(isoStamp)
const date = new Date(isoStamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, "0")
const day = String(date.getDate()).padStart(2, "0")
return `${year}-${month}-${day}`
}
export { convertDate }

View file