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>
|
||||
<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>
|
||||
|
|
|
|||
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 (
|
||||
<div className="w-full flex flex-row justify-between gap-3">
|
||||
<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">
|
||||
<h3 className="font-medium text-sm p-1 ml-1">Incoming links</h3>
|
||||
<Badge variant="secondary" className="rounded-none">
|
||||
|
|
@ -59,9 +41,7 @@ export default function EntryReferences({ entryTitle }) {
|
|||
</Link>
|
||||
))}
|
||||
</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">Outgoing links</h3>
|
||||
<Badge variant="secondary" className="rounded-none">
|
||||
|
|
@ -82,6 +62,26 @@ export default function EntryReferences({ entryTitle }) {
|
|||
))}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/componen
|
|||
import EntryBody from "@/components/EntryBody"
|
||||
import EntryMetadata from "@/containers/EntryMetadata"
|
||||
|
||||
export default function Entry({ entryTitle, entryBody, isLoading }) {
|
||||
export default function Entry({ entryTitle, entryBody, history, metadata, isLoading }) {
|
||||
return (
|
||||
<ResizablePanelGroup direction="vertical" className="w-full h-full">
|
||||
<ResizablePanel defaultSize={75}>
|
||||
|
|
@ -13,7 +13,7 @@ export default function Entry({ entryTitle, entryBody, isLoading }) {
|
|||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={25}>
|
||||
<div className="h-full overflow-auto">
|
||||
<EntryMetadata entryTitle={entryTitle} />
|
||||
<EntryMetadata entryTitle={entryTitle} history={history} metadata={metadata} />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ import EntryReferences from "@/components/EntryReferences"
|
|||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Bookmark } 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 (
|
||||
<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">
|
||||
|
|
@ -22,13 +24,30 @@ export default function EntryMetadata({ entryTitle }) {
|
|||
<History />
|
||||
History
|
||||
</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>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<TabsContent value="references" className="px-2 lg:px-2 m-0">
|
||||
<EntryReferences entryTitle={entryTitle} />
|
||||
</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>
|
||||
</Tabs>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const columns = [
|
|||
},
|
||||
cell: ({ cell, row }) => {
|
||||
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">
|
||||
{row.original.title}
|
||||
</span>
|
||||
|
|
@ -46,7 +46,6 @@ export default function RecentEdits() {
|
|||
queryFn: () => api.get("/entries?limit=20&sort=date").then((res) => res.data),
|
||||
})
|
||||
|
||||
console.log(data)
|
||||
const parsed = data?.data?.map((entry) => {
|
||||
const [date, time] = entry?.last_modified?.split(" ")
|
||||
return {
|
||||
|
|
@ -57,6 +56,7 @@ export default function RecentEdits() {
|
|||
year: "numeric",
|
||||
}),
|
||||
time: time,
|
||||
link: entry.title,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import MainTemplate from "@/templates/MainTemplate"
|
||||
import RecentEdits from "@/containers/RecentEdits"
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -16,75 +13,22 @@ export default function Home() {
|
|||
</div>
|
||||
<div className="p-4 lg:p-6">
|
||||
<p className="leading-7 [&:not(:first-child)]:mt-6 font-normal">
|
||||
{/*
|
||||
|
||||
<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{" "}
|
||||
Hi,{" "}
|
||||
<a
|
||||
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
|
||||
</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.
|
||||
🇮🇪
|
||||
I'm Thomas
|
||||
</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="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">
|
||||
<RecentEdits />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,15 @@ export default function EntryTemplate() {
|
|||
return (
|
||||
<PageTemplate
|
||||
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 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||