feat: final changes before first deploy

This commit is contained in:
Thomas Bishop 2025-11-07 15:55:32 +00:00
parent 00240d9978
commit 06be988b4d
16 changed files with 132 additions and 108 deletions

View file

@ -1 +1,33 @@
TBC # eolas-app
A React web app that serves as the frontend for my Zettelkasten, Eolas.
eolas-app is a constituent part of my knowledge management system comprising [eolas](https://forgejo.systemsobscure.net/thomasabishop/eolas),
[eolas-db](https://forgejo.systemsobscure.net/thomasabishop/eolas-db), and [eolas-api](https://forgejo.systemsobscure.net/thomasabishop/eolas-api).
It sources its data from
[eolas-api](https://forgejo.systemsobscure.net/thomasabishop/eolas-api) also
running on my VPS.
## Local development
```sh
npm install
npm run dev
```
This will use Vite to start the application at `http://localhost:5173`. The
application requires a local instance of `eolas-api` to be running, specified
via the environment variable `VITE_EOLAS_API_ENDPOINT` in a `.env`.
Alternatively use the production API URL.
## Deployment
The application is deployed to my remote VPS, residing at `/var/www/eolas-app`.
It is publicly accessible at
[eolas.systemsobscure.net](https://eolas.systemsobscure.net).
Deployment is automated via a [Forgejo action](https://forgejo.systemsobscure.net/thomasabishop/eolas-app/src/branch/main/.forgejo/workflows/deploy.yaml) that builds the Webpack bundle
and transfers it to the VPS. Deployment actions are always executed by the `deploy` user on the VPS.

View file

@ -1,20 +1,18 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" <title>Eólas</title>
rel="stylesheet"
/>
<title>Vite + React + TS</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
public/favicon-96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

3
public/favicon.svg Normal file
View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-library-icon lucide-square-library"><rect width="18" height="18" x="3" y="3" rx="2"></rect><path d="M7 7v10"></path><path d="M11 7v10"></path><path d="m15 7 2 10"></path></svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
@media (prefers-color-scheme: dark) { :root { filter: none; } }
</style></svg>

After

Width:  |  Height:  |  Size: 655 B

21
public/site.webmanifest Normal file
View file

@ -0,0 +1,21 @@
{
"name": "MyWebSite",
"short_name": "MySite",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -22,24 +22,6 @@ export default function EntryReferences({ entryTitle }) {
return ( return (
<div className="w-full flex flex-row justify-between gap-3"> <div className="w-full flex flex-row justify-between gap-3">
<div className="w-full"> <div className="w-full">
<div className="flex flex row justify-between bg-sidebar">
<h3 className="font-medium text-sm p-1 ml-1">Tags</h3>
<Badge variant="secondary" className="rounded-none">
{tags?.count}
</Badge>
</div>
<div className="mt-2 mb-3 pl-1">
{tags?.data.map((item, i) => (
<Link
key={i}
to={`/tags/${item}`}
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2"
>
{item}
</Link>
))}
</div>
<div className="flex flex row justify-between bg-sidebar"> <div className="flex flex row justify-between bg-sidebar">
<h3 className="font-medium text-sm p-1 ml-1">Incoming links</h3> <h3 className="font-medium text-sm p-1 ml-1">Incoming links</h3>
<Badge variant="secondary" className="rounded-none"> <Badge variant="secondary" className="rounded-none">
@ -59,9 +41,7 @@ export default function EntryReferences({ entryTitle }) {
</Link> </Link>
))} ))}
</div> </div>
</div>
<div className="w-full">
<div className="flex flex row justify-between bg-sidebar"> <div className="flex flex row justify-between bg-sidebar">
<h3 className="font-medium text-sm p-1 ml-1">Outgoing links</h3> <h3 className="font-medium text-sm p-1 ml-1">Outgoing links</h3>
<Badge variant="secondary" className="rounded-none"> <Badge variant="secondary" className="rounded-none">
@ -82,6 +62,26 @@ export default function EntryReferences({ entryTitle }) {
))} ))}
</div> </div>
</div> </div>
<div className="w-full">
<div className="flex flex row justify-between bg-sidebar">
<h3 className="font-medium text-sm p-1 ml-1">Tags</h3>
<Badge variant="secondary" className="rounded-none">
{tags?.count}
</Badge>
</div>
<div className="mt-2 mb-3 pl-1">
{tags?.data.map((item, i) => (
<Link
key={i}
to={`/tags/${item}`}
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2"
>
{item}
</Link>
))}
</div>
</div>
</div> </div>
) )
} }

View file

@ -2,7 +2,7 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/componen
import EntryBody from "@/components/EntryBody" import EntryBody from "@/components/EntryBody"
import EntryMetadata from "@/containers/EntryMetadata" import EntryMetadata from "@/containers/EntryMetadata"
export default function Entry({ entryTitle, entryBody, isLoading }) { export default function Entry({ entryTitle, entryBody, history, metadata, isLoading }) {
return ( return (
<ResizablePanelGroup direction="vertical" className="w-full h-full"> <ResizablePanelGroup direction="vertical" className="w-full h-full">
<ResizablePanel defaultSize={75}> <ResizablePanel defaultSize={75}>
@ -13,7 +13,7 @@ export default function Entry({ entryTitle, entryBody, isLoading }) {
<ResizableHandle withHandle /> <ResizableHandle withHandle />
<ResizablePanel defaultSize={25}> <ResizablePanel defaultSize={25}>
<div className="h-full overflow-auto"> <div className="h-full overflow-auto">
<EntryMetadata entryTitle={entryTitle} /> <EntryMetadata entryTitle={entryTitle} history={history} metadata={metadata} />
</div> </div>
</ResizablePanel> </ResizablePanel>
</ResizablePanelGroup> </ResizablePanelGroup>

View file

@ -2,8 +2,10 @@ import EntryReferences from "@/components/EntryReferences"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Bookmark } from "lucide-react" import { Bookmark } from "lucide-react"
import { History } from "lucide-react" import { History } from "lucide-react"
import { Braces } from "lucide-react"
export default function EntryMetadata({ entryTitle }) { export default function EntryMetadata({ entryTitle, history, metadata }) {
console.log(history)
return ( return (
<Tabs defaultValue="references" className="w-full h-full flex flex-col"> <Tabs defaultValue="references" className="w-full h-full flex flex-col">
<TabsList className="rounded-none shadow-none drop-shadow-none w-full bg-sidebar sticky top-0 z-10 flex-shrink-0"> <TabsList className="rounded-none shadow-none drop-shadow-none w-full bg-sidebar sticky top-0 z-10 flex-shrink-0">
@ -22,13 +24,30 @@ export default function EntryMetadata({ entryTitle }) {
<History /> <History />
History History
</TabsTrigger> </TabsTrigger>
<TabsTrigger
className="pl-2 justify-start text-sm font-semibold rounded-none shadow-none data-[state=active]:border-1 data-[state=active]:border-border py-4"
value="metadata"
>
<Braces />
Metadata
</TabsTrigger>
</TabsList> </TabsList>
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<TabsContent value="references" className="px-2 lg:px-2 m-0"> <TabsContent value="references" className="px-2 lg:px-2 m-0">
<EntryReferences entryTitle={entryTitle} /> <EntryReferences entryTitle={entryTitle} />
</TabsContent> </TabsContent>
<TabsContent className="pl-4 lg:pl-6 m-0" value="history"></TabsContent> <TabsContent className="pl-4 lg:pl-6 m-0" value="history">
<div className="w-full">
<p>Last modified: {history.lastModified}</p>
</div>
</TabsContent>
<TabsContent className="pl-4 lg:pl-6 m-0" value="metadata">
<div className="w-full">
<p>Size on disk: {metadata.fileSize} B</p>
</div>
</TabsContent>
</div> </div>
</Tabs> </Tabs>
) )

View file

@ -22,7 +22,7 @@ const columns = [
}, },
cell: ({ cell, row }) => { cell: ({ cell, row }) => {
return ( return (
<Link to={`entries/${row.original.title}`}> <Link to={`entries/${row.original.link}`}>
<span className="text-foreground underline-offset-3 underline hover:text-gray-700 dark:hover:text-green-300"> <span className="text-foreground underline-offset-3 underline hover:text-gray-700 dark:hover:text-green-300">
{row.original.title} {row.original.title}
</span> </span>
@ -46,7 +46,6 @@ export default function RecentEdits() {
queryFn: () => api.get("/entries?limit=20&sort=date").then((res) => res.data), queryFn: () => api.get("/entries?limit=20&sort=date").then((res) => res.data),
}) })
console.log(data)
const parsed = data?.data?.map((entry) => { const parsed = data?.data?.map((entry) => {
const [date, time] = entry?.last_modified?.split(" ") const [date, time] = entry?.last_modified?.split(" ")
return { return {
@ -57,6 +56,7 @@ export default function RecentEdits() {
year: "numeric", year: "numeric",
}), }),
time: time, time: time,
link: entry.title,
} }
}) })

View file

@ -1,8 +1,5 @@
import MainTemplate from "@/templates/MainTemplate" import MainTemplate from "@/templates/MainTemplate"
import RecentEdits from "@/containers/RecentEdits" import RecentEdits from "@/containers/RecentEdits"
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"
import { Button } from "@/components/ui/button"
export default function Home() { export default function Home() {
return ( return (
<> <>
@ -16,75 +13,22 @@ export default function Home() {
</div> </div>
<div className="p-4 lg:p-6"> <div className="p-4 lg:p-6">
<p className="leading-7 [&:not(:first-child)]:mt-6 font-normal"> <p className="leading-7 [&:not(:first-child)]:mt-6 font-normal">
{/* Hi,{" "}
<HoverCard>
<HoverCardTrigger asChild>
<Button
variant="outline"
size="sm"
className="rounded-none mr-1 font-normal text-base"
>
Eólas
</Button>
</HoverCardTrigger>
<HoverCardContent className="w-70 rounded-none">
<div className="flex justify-between gap-4">
<div className="space-y-1">
<h4 className="text-sm font-semibold">🇮🇪</h4>
<p className="text-sm">
Irish for "knowledge", especially knowledge gained
through practical experience.
</p>
</div>
</div>
</HoverCardContent>
</HoverCard>
*/}
Eólas is{" "}
<a <a
className="text-foreground underline-offset-3 font-medium underline hover:text-gray-700 dark:hover:text-green-300" className="text-foreground underline-offset-3 font-medium underline hover:text-gray-700 dark:hover:text-green-300"
href="#" href="https://systemsobscure.blog/about"
target="_blank"
> >
my I'm Thomas
</a>{" "}
technical knowledge management system, or "second-brain", comprising
notes from the study of software engineering and computer
science.{" "}
</p>
<p className="leading-7 [&:not(:first-child)]:mt-6 font-normal">
🇮🇪 The word <i>eólas</i> (pronounced "aw-lus") is Irish for
"knowledge", especially knowledge gained through practical experience.
🇮🇪
</p>
</div>
</div>
</div>
{/*
<div className="bg-muted rounded-none p-3 font-medium text-sm text-muted-foreground my-4 mb-6">
<p className="">
{" "}
<span className="mr-2">🇮🇪</span> "Eólas" is Irish for "knowledge",
especially knowledge gained through practical experience.
</p>
</div>
I'm
<a
className="text-black underline-offset-3 font-medium underline hover:text-gray-700"
href="#"
>
Thomas
</a> </a>
. , Eólas is my technical knowledge management system, or
"second-brain", comprising notes from the study of software
engineering and computer science.{" "}
</p>
</div>
</div>
</div>
*/}
<div className="px-4 lg:px-6 flex-1 flex"> <div className="px-4 lg:px-6 flex-1 flex">
<RecentEdits /> <RecentEdits />
</div> </div>

View file

@ -16,7 +16,15 @@ export default function EntryTemplate() {
return ( return (
<PageTemplate <PageTemplate
titleComponent={<span>{entry?.replace(/_/g, " ")}</span>} titleComponent={<span>{entry?.replace(/_/g, " ")}</span>}
pageBody={<Entry entryTitle={entry} entryBody={data?.body} isLoading={isLoading} />} pageBody={
<Entry
entryTitle={entry}
entryBody={data?.body}
isLoading={isLoading}
history={{ lastModified: data?.last_modified }}
metadata={{ fileSize: data?.size }}
/>
}
/> />
) )
} }