feat: final changes before first deploy
34
README.md
|
|
@ -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.
|
||||||
|
|
|
||||||
28
index.html
|
|
@ -1,20 +1,18 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<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.gstatic.com" crossorigin />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<title>Vite + React + TS</title>
|
<head>
|
||||||
</head>
|
<meta charset="UTF-8" />
|
||||||
|
<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.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||||
|
<title>Eólas</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
3
public/favicon.svg
Normal 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
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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 |
BIN
public/web-app-manifest-192x192.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
public/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>{" "}
|
</a>
|
||||||
technical knowledge management system, or "second-brain", comprising
|
, Eólas is my technical knowledge management system, or
|
||||||
notes from the study of software engineering and computer
|
"second-brain", comprising notes from the study of software
|
||||||
science.{" "}
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
|
||||||
.
|
|
||||||
|
|
||||||
|
|
||||||
*/}
|
|
||||||
<div className="px-4 lg:px-6 flex-1 flex">
|
<div className="px-4 lg:px-6 flex-1 flex">
|
||||||
<RecentEdits />
|
<RecentEdits />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||