feat: better layout in a lot of places
This commit is contained in:
parent
ff3e7f6667
commit
3cfa2274a8
@ -11,7 +11,7 @@ export function Card(
|
|||||||
}}
|
}}
|
||||||
class="text-white rounded-3xl shadow-md p-4 relative overflow-hidden
|
class="text-white rounded-3xl shadow-md p-4 relative overflow-hidden
|
||||||
lg:w-56 lg:h-56
|
lg:w-56 lg:h-56
|
||||||
sm:w-40 sm:h-40
|
sm:w-48 sm:h-48
|
||||||
w-32 h-32"
|
w-32 h-32"
|
||||||
>
|
>
|
||||||
<div class="h-full flex flex-col justify-between relative z-10">
|
<div class="h-full flex flex-col justify-between relative z-10">
|
||||||
|
12
components/Grid.tsx
Normal file
12
components/Grid.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ComponentChildren } from "preact";
|
||||||
|
|
||||||
|
export const Grid = ({ children }: { children: ComponentChildren }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="grid gap-4 py-6"
|
||||||
|
style={{ gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))" }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
15
components/HashTags.tsx
Normal file
15
components/HashTags.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export const HashTags = ({ tags }: { tags: string[] }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={`flex gap-2 px-8`}
|
||||||
|
>
|
||||||
|
{tags.map((t) => {
|
||||||
|
return (
|
||||||
|
<span class="bg-gray-700 text-white p-2 rounded-xl text-sm">
|
||||||
|
#{t}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,9 +1,13 @@
|
|||||||
import IconExternalLink from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/external-link.tsx";
|
import IconExternalLink from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/external-link.tsx";
|
||||||
import { Star } from "@components/Stars.tsx";
|
import { Star } from "@components/Stars.tsx";
|
||||||
|
import IconArrowNarrowLeft from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-narrow-left.tsx";
|
||||||
|
import IconEdit from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/edit.tsx";
|
||||||
|
|
||||||
export function RecipeHero(
|
export function RecipeHero(
|
||||||
{ data, subline, backlink }: {
|
{ data, subline, backlink, editLink }: {
|
||||||
backlink: string;
|
backlink: string;
|
||||||
subline?: string[];
|
subline?: string[];
|
||||||
|
editLink?: string;
|
||||||
data: { meta?: { image?: string; link?: string }; name: string };
|
data: { meta?: { image?: string; link?: string }; name: string };
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@ -31,15 +35,26 @@ export function RecipeHero(
|
|||||||
|
|
||||||
<div class="flex justify-between mx-8 mt-8">
|
<div class="flex justify-between mx-8 mt-8">
|
||||||
<a
|
<a
|
||||||
class="px-4 py-2 bg-gray-300 text-gray-800 rounded-lg"
|
class="px-4 py-2 bg-gray-300 text-gray-800 rounded-lg flex gap-1 items-center"
|
||||||
href={backlink}
|
href={backlink}
|
||||||
>
|
>
|
||||||
Back
|
<IconArrowNarrowLeft class="w-5 h-5" /> Back
|
||||||
</a>
|
</a>
|
||||||
|
{editLink &&
|
||||||
|
(
|
||||||
|
<a
|
||||||
|
class={`px-4 py-2 ${
|
||||||
|
imageUrl ? "bg-gray-300 text-gray-800" : "text-gray-200"
|
||||||
|
} rounded-lg flex gap-1 items-center`}
|
||||||
|
href={editLink}
|
||||||
|
>
|
||||||
|
<IconEdit class="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={`inset-x-0 py-4 px-8 ${imageUrl ? "py-8" : ""} `}
|
class={`relative inset-x-0 py-4 px-8 ${imageUrl ? "py-8" : ""} `}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class={`${imageUrl ? "noisy-gradient" : ""} flex gap-4 items-center ${
|
class={`${imageUrl ? "noisy-gradient" : ""} flex gap-4 items-center ${
|
||||||
@ -69,7 +84,7 @@ export function RecipeHero(
|
|||||||
{subline?.length &&
|
{subline?.length &&
|
||||||
(
|
(
|
||||||
<div
|
<div
|
||||||
class="relative z-50 flex gap-5 font-sm text-light mt-3"
|
class={`relative z-50 flex gap-5 font-sm text-light mt-3`}
|
||||||
style={{ color: imageUrl ? "#1F1F1F" : "white" }}
|
style={{ color: imageUrl ? "#1F1F1F" : "white" }}
|
||||||
>
|
>
|
||||||
{subline.filter((s) => s && s?.length > 1).map((s) => {
|
{subline.filter((s) => s && s?.length > 1).map((s) => {
|
||||||
|
37
components/Youtube.tsx
Normal file
37
components/Youtube.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export const isYoutubeLink = (link: string) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(link);
|
||||||
|
return ["youtu.be", "youtube.com"].includes(url.hostname);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function extractYoutubeId(link: string) {
|
||||||
|
const url = new URL(link);
|
||||||
|
if (url.searchParams.has("v")) {
|
||||||
|
const id = url.searchParams.get("v");
|
||||||
|
|
||||||
|
if (id?.length && id.length > 4) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.pathname.replace(/^\//, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const YoutubePlayer = ({ link }: { link: string }) => {
|
||||||
|
const id = extractYoutubeId(link);
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
width="100%"
|
||||||
|
height="400px"
|
||||||
|
src={`https://www.youtube-nocookie.com/embed/${id}`}
|
||||||
|
frameBorder="0"
|
||||||
|
allow="autoplay; encrypted-media"
|
||||||
|
allowFullScreen
|
||||||
|
>
|
||||||
|
</iframe>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
|
import { menu } from "@lib/menus.ts";
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
@ -9,25 +10,6 @@ export type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MainLayout = ({ children, url }: Props) => {
|
export const MainLayout = ({ children, url }: Props) => {
|
||||||
const menu = [
|
|
||||||
{
|
|
||||||
name: "🏡 Home",
|
|
||||||
link: "/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "🍽️ Recipes",
|
|
||||||
link: "/recipes",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "🍿 Movies",
|
|
||||||
link: "/movies",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "📝 Articles",
|
|
||||||
link: "/articles",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="md:grid mx-auto"
|
class="md:grid mx-auto"
|
||||||
|
@ -30,7 +30,9 @@ export const KMenu = (
|
|||||||
) => {
|
) => {
|
||||||
const activeMenuType = useSignal(type);
|
const activeMenuType = useSignal(type);
|
||||||
const activeMenu = menus[activeMenuType.value || "main"];
|
const activeMenu = menus[activeMenuType.value || "main"];
|
||||||
const activeState = useSignal<"normal" | "loading" | "error">("normal");
|
const activeState = useSignal<"normal" | "loading" | "error" | "input">(
|
||||||
|
"normal",
|
||||||
|
);
|
||||||
const activeIndex = useSignal(-1);
|
const activeIndex = useSignal(-1);
|
||||||
|
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
@ -127,14 +129,14 @@ export const KMenu = (
|
|||||||
style={{ background: "#1f1f1f88" }}
|
style={{ background: "#1f1f1f88" }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class={`relative w-1/2 max-h-64 rounded-2xl shadow-2xl nnoisy-gradient overflow-hidden after:opacity-10 border border-gray-500`}
|
class={`relative w-1/2 max-h-64 max-w-[400px] rounded-2xl shadow-2xl nnoisy-gradient overflow-hidden after:opacity-10`}
|
||||||
style={{ background: "#1f1f1f" }}
|
style={{ background: "#1f1f1f" }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="grid h-12 text-gray-400 border-b border-gray-500 "
|
class="grid h-12 text-gray-400 border-b border-gray-500 "
|
||||||
style={{ gridTemplateColumns: "4em 1fr" }}
|
style={{ gridTemplateColumns: "4em 1fr" }}
|
||||||
>
|
>
|
||||||
{activeState.value === "normal" &&
|
{(activeState.value === "normal" || activeState.value === "input") &&
|
||||||
(
|
(
|
||||||
<>
|
<>
|
||||||
<div class="grid place-items-center border-r border-gray-500">
|
<div class="grid place-items-center border-r border-gray-500">
|
||||||
@ -161,6 +163,11 @@ export const KMenu = (
|
|||||||
{activeState.value === "normal" &&
|
{activeState.value === "normal" &&
|
||||||
(
|
(
|
||||||
<div class="" style={{ maxHeight: "12rem", overflowY: "auto" }}>
|
<div class="" style={{ maxHeight: "12rem", overflowY: "auto" }}>
|
||||||
|
{entries?.length === 0 && (
|
||||||
|
<div class="text-gray-400 px-4 py-2">
|
||||||
|
No Entries
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{entries.map(
|
{entries.map(
|
||||||
(k, index) => {
|
(k, index) => {
|
||||||
return (
|
return (
|
||||||
|
@ -23,7 +23,9 @@ export const menus: Record<string, Menu> = {
|
|||||||
title: "Link:",
|
title: "Link:",
|
||||||
entries: [],
|
entries: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
state.activeMenu.value = "input_link";
|
state.activeMenu.value = "input_link";
|
||||||
|
state.activeState.value = "input";
|
||||||
|
|
||||||
const unsub = state.commandInput.subscribe(async (value) => {
|
const unsub = state.commandInput.subscribe(async (value) => {
|
||||||
if (isValidUrl(value)) {
|
if (isValidUrl(value)) {
|
||||||
|
@ -2,7 +2,7 @@ import { Signal } from "@preact/signals";
|
|||||||
|
|
||||||
export type MenuState = {
|
export type MenuState = {
|
||||||
activeMenu: Signal<string>;
|
activeMenu: Signal<string>;
|
||||||
activeState: Signal<"error" | "normal" | "loading">;
|
activeState: Signal<"input" | "error" | "normal" | "loading">;
|
||||||
commandInput: Signal<string>;
|
commandInput: Signal<string>;
|
||||||
visible: Signal<boolean>;
|
visible: Signal<boolean>;
|
||||||
menus: Record<string, Menu>;
|
menus: Record<string, Menu>;
|
||||||
|
16
lib/cache/cache.ts
vendored
16
lib/cache/cache.ts
vendored
@ -54,7 +54,19 @@ export function expire(id: string, seconds: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function set<T extends RedisValue>(id: string, content: T) {
|
type RedisOptions = {
|
||||||
|
expires?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function set<T extends RedisValue>(
|
||||||
|
id: string,
|
||||||
|
content: T,
|
||||||
|
options?: RedisOptions,
|
||||||
|
) {
|
||||||
console.log("[cache] storing ", { id });
|
console.log("[cache] storing ", { id });
|
||||||
return await cache.set(id, content);
|
const res = await cache.set(id, content);
|
||||||
|
if (options?.expires) {
|
||||||
|
await expire(id, options.expires);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
46
lib/cache/documents.ts
vendored
46
lib/cache/documents.ts
vendored
@ -1,57 +1,31 @@
|
|||||||
import { Document } from "@lib/documents.ts";
|
import { Document } from "@lib/documents.ts";
|
||||||
import * as cache from "@lib/cache/cache.ts";
|
import * as cache from "@lib/cache/cache.ts";
|
||||||
|
|
||||||
type DocumentsCache = {
|
const CACHE_INTERVAL = 20; // 5 seconds;
|
||||||
lastUpdated: number;
|
|
||||||
documents: Document[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const CACHE_INTERVAL = 5000; // 5 seconds;
|
|
||||||
const CACHE_KEY = "documents";
|
const CACHE_KEY = "documents";
|
||||||
|
|
||||||
export async function getDocuments() {
|
export async function getDocuments() {
|
||||||
const docs = await cache.get<DocumentsCache>(CACHE_KEY);
|
const res = await cache.get<string>(CACHE_KEY);
|
||||||
if (!docs) return;
|
if (res) return JSON.parse(res);
|
||||||
|
return;
|
||||||
if (Date.now() > docs.lastUpdated + CACHE_INTERVAL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return docs.documents;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setDocuments(documents: Document[]) {
|
export function setDocuments(documents: Document[]) {
|
||||||
return cache.set(
|
return cache.set(
|
||||||
CACHE_KEY,
|
CACHE_KEY,
|
||||||
JSON.stringify({
|
JSON.stringify(documents),
|
||||||
lastUpdated: Date.now(),
|
{ expires: CACHE_INTERVAL },
|
||||||
documents,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type DocumentCache = {
|
export function getDocument(id: string) {
|
||||||
lastUpdated: number;
|
return cache.get<string>(CACHE_KEY + "/" + id);
|
||||||
content: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getDocument(id: string) {
|
|
||||||
const doc = await cache.get<DocumentCache>(CACHE_KEY + "/" + id);
|
|
||||||
if (!doc) return;
|
|
||||||
|
|
||||||
if (Date.now() > doc.lastUpdated + CACHE_INTERVAL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return doc.content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setDocument(id: string, content: string) {
|
export async function setDocument(id: string, content: string) {
|
||||||
await cache.set(
|
await cache.set(
|
||||||
CACHE_KEY + "/" + id,
|
CACHE_KEY + "/" + id,
|
||||||
JSON.stringify({
|
content,
|
||||||
lastUpdated: Date.now(),
|
{ expires: CACHE_INTERVAL },
|
||||||
content,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
1
lib/cache/image.ts
vendored
1
lib/cache/image.ts
vendored
@ -12,7 +12,6 @@ const CACHE_KEY = "images";
|
|||||||
|
|
||||||
function getCacheKey({ url: _url, width, height }: ImageCacheOptions) {
|
function getCacheKey({ url: _url, width, height }: ImageCacheOptions) {
|
||||||
const url = new URL(_url);
|
const url = new URL(_url);
|
||||||
|
|
||||||
return `${CACHE_KEY}/${url.hostname}/${url.pathname}/${width}/${height}`
|
return `${CACHE_KEY}/${url.hostname}/${url.pathname}/${width}/${height}`
|
||||||
.replace(
|
.replace(
|
||||||
"//",
|
"//",
|
||||||
|
@ -25,7 +25,7 @@ export async function getDocuments(): Promise<Document[]> {
|
|||||||
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.append("Accept", "application/json");
|
headers.append("Accept", "application/json");
|
||||||
|
console.log("[documents] fetching all documents");
|
||||||
const response = await fetch(`${SILVERBULLET_SERVER}/index.json`, {
|
const response = await fetch(`${SILVERBULLET_SERVER}/index.json`, {
|
||||||
headers: headers,
|
headers: headers,
|
||||||
});
|
});
|
||||||
@ -47,6 +47,8 @@ export function createDocument(
|
|||||||
headers.append("Content-Type", mediaType);
|
headers.append("Content-Type", mediaType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("[documents] creating document", { name });
|
||||||
|
|
||||||
return fetch(SILVERBULLET_SERVER + "/" + name, {
|
return fetch(SILVERBULLET_SERVER + "/" + name, {
|
||||||
body: content,
|
body: content,
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
@ -58,6 +60,7 @@ export async function getDocument(name: string): Promise<string> {
|
|||||||
const cachedDocument = await cache.getDocument(name);
|
const cachedDocument = await cache.getDocument(name);
|
||||||
if (cachedDocument) return cachedDocument;
|
if (cachedDocument) return cachedDocument;
|
||||||
|
|
||||||
|
console.log("[documents] fetching document", { name });
|
||||||
const response = await fetch(SILVERBULLET_SERVER + "/" + name);
|
const response = await fetch(SILVERBULLET_SERVER + "/" + name);
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
|
|
||||||
|
18
lib/menus.ts
Normal file
18
lib/menus.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export const menu = [
|
||||||
|
{
|
||||||
|
name: "🏡 Home",
|
||||||
|
link: "/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "🍽️ Recipes",
|
||||||
|
link: "/recipes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "🍿 Movies",
|
||||||
|
link: "/movies",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "📝 Articles",
|
||||||
|
link: "/articles",
|
||||||
|
},
|
||||||
|
];
|
@ -2,7 +2,7 @@ import { parseDocument, renderMarkdown } from "@lib/documents.ts";
|
|||||||
import { parse } from "yaml";
|
import { parse } from "yaml";
|
||||||
import { createCrud } from "@lib/crud.ts";
|
import { createCrud } from "@lib/crud.ts";
|
||||||
import { stringify } from "https://deno.land/std@0.194.0/yaml/stringify.ts";
|
import { stringify } from "https://deno.land/std@0.194.0/yaml/stringify.ts";
|
||||||
import { formatDate } from "@lib/string.ts";
|
import { extractHashTags, formatDate } from "@lib/string.ts";
|
||||||
import { fixRenderedMarkdown } from "@lib/helpers.ts";
|
import { fixRenderedMarkdown } from "@lib/helpers.ts";
|
||||||
|
|
||||||
export type Article = {
|
export type Article = {
|
||||||
@ -80,10 +80,9 @@ function parseArticle(original: string, id: string): Article {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let content = original.slice(range[0], range[1]);
|
let content = original.slice(range[0], range[1]);
|
||||||
const tags = [];
|
const tags = extractHashTags(content);
|
||||||
for (const [hashtag] of original.matchAll(/\B(\#[a-zA-Z\-]+\b)(?!;)/g)) {
|
for (const tag of tags) {
|
||||||
tags.push(hashtag.replace(/\#/g, ""));
|
content = content.replace("#" + tag, "");
|
||||||
content = content.replace(hashtag, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { parseDocument, renderMarkdown } from "@lib/documents.ts";
|
import { parseDocument, renderMarkdown } from "@lib/documents.ts";
|
||||||
import { parse } from "yaml";
|
import { parse } from "yaml";
|
||||||
import { createCrud } from "@lib/crud.ts";
|
import { createCrud } from "@lib/crud.ts";
|
||||||
|
import { extractHashTags } from "@lib/string.ts";
|
||||||
|
|
||||||
export type Movie = {
|
export type Movie = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
hashtags: string[];
|
tags: string[];
|
||||||
meta: {
|
meta: {
|
||||||
date: Date;
|
date: Date;
|
||||||
image: string;
|
image: string;
|
||||||
@ -52,16 +53,15 @@ export function parseMovie(original: string, id: string): Movie {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let description = original.slice(range[0], range[1]);
|
let description = original.slice(range[0], range[1]);
|
||||||
const hashtags = [];
|
const tags = extractHashTags(description);
|
||||||
for (const [hashtag] of original.matchAll(/\B(\#[a-zA-Z]+\b)(?!;)/g)) {
|
for (const tag of tags) {
|
||||||
hashtags.push(hashtag.replace(/\#/g, ""));
|
description = description.replace("#" + tag, "");
|
||||||
description = description.replace(hashtag, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
hashtags,
|
tags,
|
||||||
description: renderMarkdown(description),
|
description: renderMarkdown(description),
|
||||||
meta,
|
meta,
|
||||||
};
|
};
|
||||||
|
@ -15,3 +15,15 @@ export function safeFileName(inputString: string): string {
|
|||||||
|
|
||||||
return fileName;
|
return fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractHashTags(inputString: string) {
|
||||||
|
const hashtags = [];
|
||||||
|
|
||||||
|
for (
|
||||||
|
const [hashtag] of inputString.matchAll(/(?<!\()\B(\#[a-zA-Z\-]+\b)(?!;)/g)
|
||||||
|
) {
|
||||||
|
hashtags.push(hashtag.replace(/\#/g, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashtags;
|
||||||
|
}
|
||||||
|
@ -3,6 +3,8 @@ import { MainLayout } from "@components/layouts/main.tsx";
|
|||||||
import { Article, getArticle } from "@lib/resource/articles.ts";
|
import { Article, getArticle } from "@lib/resource/articles.ts";
|
||||||
import { RecipeHero } from "@components/RecipeHero.tsx";
|
import { RecipeHero } from "@components/RecipeHero.tsx";
|
||||||
import { KMenu } from "@islands/KMenu.tsx";
|
import { KMenu } from "@islands/KMenu.tsx";
|
||||||
|
import { isYoutubeLink, YoutubePlayer } from "@components/Youtube.tsx";
|
||||||
|
import { HashTags } from "@components/HashTags.tsx";
|
||||||
|
|
||||||
export const handler: Handlers<Article | null> = {
|
export const handler: Handlers<Article | null> = {
|
||||||
async GET(_, ctx) {
|
async GET(_, ctx) {
|
||||||
@ -23,27 +25,26 @@ export default function Greet(props: PageProps<Article>) {
|
|||||||
<RecipeHero
|
<RecipeHero
|
||||||
data={article}
|
data={article}
|
||||||
subline={[author, date.toString()]}
|
subline={[author, date.toString()]}
|
||||||
|
editLink={`https://notes.max-richter.dev/Media/articles/${article.id}`}
|
||||||
backlink="/articles"
|
backlink="/articles"
|
||||||
/>
|
/>
|
||||||
<KMenu type="main" context={article} />
|
<KMenu type="main" context={article} />
|
||||||
{article.tags.length &&
|
{article.tags.length > 0 && (
|
||||||
(
|
<>
|
||||||
<div class="flex gap-2 px-8">
|
<br />
|
||||||
{article.tags.map((t) => {
|
<HashTags tags={article.tags} />
|
||||||
return (
|
</>
|
||||||
<span class="bg-gray-700 text-white p-2 rounded-xl text-sm">
|
)}
|
||||||
#{t}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div class="px-8 text-white mt-10">
|
<div class="px-8 text-white mt-10">
|
||||||
|
{isYoutubeLink(article.meta.link) && (
|
||||||
|
<YoutubePlayer link={article.meta.link} />
|
||||||
|
)}
|
||||||
<pre
|
<pre
|
||||||
class="whitespace-break-spaces"
|
class="whitespace-break-spaces"
|
||||||
dangerouslySetInnerHTML={{ __html: article.content || "" }}
|
dangerouslySetInnerHTML={{ __html: article.content || "" }}
|
||||||
>
|
>
|
||||||
{article.content}
|
{article.content||""}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
@ -4,6 +4,7 @@ import IconArrowLeft from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-
|
|||||||
import { Article, getAllArticles } from "@lib/resource/articles.ts";
|
import { Article, getAllArticles } from "@lib/resource/articles.ts";
|
||||||
import { Card } from "@components/Card.tsx";
|
import { Card } from "@components/Card.tsx";
|
||||||
import { KMenu } from "@islands/KMenu.tsx";
|
import { KMenu } from "@islands/KMenu.tsx";
|
||||||
|
import { Grid } from "@components/Grid.tsx";
|
||||||
|
|
||||||
export const handler: Handlers<Article[] | null> = {
|
export const handler: Handlers<Article[] | null> = {
|
||||||
async GET(_, ctx) {
|
async GET(_, ctx) {
|
||||||
@ -28,11 +29,11 @@ export default function Greet(props: PageProps<Article[] | null>) {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<KMenu type="main" context={false} />
|
<KMenu type="main" context={false} />
|
||||||
<div class="flex flex-wrap items-center gap-4 px-4">
|
<Grid>
|
||||||
{props.data?.map((doc) => {
|
{props.data?.map((doc) => {
|
||||||
return <Card link={`/articles/${doc.id}`} title={doc.name} />;
|
return <Card link={`/articles/${doc.id}`} title={doc.name} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</Grid>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { Head } from "$fresh/runtime.ts";
|
|||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
import { Card } from "@components/Card.tsx";
|
import { Card } from "@components/Card.tsx";
|
||||||
import { PageProps } from "$fresh/server.ts";
|
import { PageProps } from "$fresh/server.ts";
|
||||||
|
import { menu } from "@lib/menus.ts";
|
||||||
|
|
||||||
export default function Home(props: PageProps) {
|
export default function Home(props: PageProps) {
|
||||||
return (
|
return (
|
||||||
@ -10,17 +11,16 @@ export default function Home(props: PageProps) {
|
|||||||
<title>app</title>
|
<title>app</title>
|
||||||
</Head>
|
</Head>
|
||||||
<MainLayout url={props.url}>
|
<MainLayout url={props.url}>
|
||||||
<div class="flex flex-wrap justify-center items-center gap-4 px-4">
|
<div class="flex flex-wrap items-center gap-4 px-4">
|
||||||
<Card
|
{menu.map((m) => {
|
||||||
title="🍽️ Recipes"
|
return (
|
||||||
image="/placeholder.svg"
|
<Card
|
||||||
link="/recipes"
|
title={m.name}
|
||||||
/>
|
image="/placeholder.svg"
|
||||||
<Card
|
link={m.link}
|
||||||
title="🍿 Movies"
|
/>
|
||||||
image="/placeholder.svg"
|
);
|
||||||
link="/movies"
|
})}
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
</>
|
</>
|
||||||
|
@ -3,6 +3,7 @@ import { MainLayout } from "@components/layouts/main.tsx";
|
|||||||
import { getMovie, Movie } from "@lib/resource/movies.ts";
|
import { getMovie, Movie } from "@lib/resource/movies.ts";
|
||||||
import { RecipeHero } from "@components/RecipeHero.tsx";
|
import { RecipeHero } from "@components/RecipeHero.tsx";
|
||||||
import { KMenu } from "@islands/KMenu.tsx";
|
import { KMenu } from "@islands/KMenu.tsx";
|
||||||
|
import { HashTags } from "@components/HashTags.tsx";
|
||||||
|
|
||||||
export const handler: Handlers<Movie | null> = {
|
export const handler: Handlers<Movie | null> = {
|
||||||
async GET(_, ctx) {
|
async GET(_, ctx) {
|
||||||
@ -24,6 +25,12 @@ export default function Greet(props: PageProps<Movie>) {
|
|||||||
backlink="/movies"
|
backlink="/movies"
|
||||||
/>
|
/>
|
||||||
<KMenu type="main" context={movie} />
|
<KMenu type="main" context={movie} />
|
||||||
|
{movie.tags.length > 0 && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<HashTags tags={movie.tags} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div class="px-8 text-white mt-10">
|
<div class="px-8 text-white mt-10">
|
||||||
<pre
|
<pre
|
||||||
class="whitespace-break-spaces"
|
class="whitespace-break-spaces"
|
||||||
|
@ -3,6 +3,7 @@ import { MainLayout } from "@components/layouts/main.tsx";
|
|||||||
import IconArrowLeft from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-left.tsx";
|
import IconArrowLeft from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-left.tsx";
|
||||||
import { getAllMovies, Movie } from "@lib/resource/movies.ts";
|
import { getAllMovies, Movie } from "@lib/resource/movies.ts";
|
||||||
import { MovieCard } from "@components/MovieCard.tsx";
|
import { MovieCard } from "@components/MovieCard.tsx";
|
||||||
|
import { Grid } from "@components/Grid.tsx";
|
||||||
|
|
||||||
export const handler: Handlers<Movie[] | null> = {
|
export const handler: Handlers<Movie[] | null> = {
|
||||||
async GET(_, ctx) {
|
async GET(_, ctx) {
|
||||||
@ -25,11 +26,11 @@ export default function Greet(props: PageProps<Movie[] | null>) {
|
|||||||
|
|
||||||
<h3 class="text-2xl text-white font-light">🍿 Movies</h3>
|
<h3 class="text-2xl text-white font-light">🍿 Movies</h3>
|
||||||
</header>
|
</header>
|
||||||
<div class="flex flex-wrap items-center gap-4 px-4">
|
<Grid>
|
||||||
{props.data?.map((doc) => {
|
{props.data?.map((doc) => {
|
||||||
return <MovieCard movie={doc} />;
|
return <MovieCard movie={doc} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</Grid>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { RecipeCard } from "@components/RecipeCard.tsx";
|
|||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
import { getAllRecipes, Recipe } from "@lib/resource/recipes.ts";
|
import { getAllRecipes, Recipe } from "@lib/resource/recipes.ts";
|
||||||
import IconArrowLeft from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-left.tsx";
|
import IconArrowLeft from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-left.tsx";
|
||||||
|
import { Grid } from "@components/Grid.tsx";
|
||||||
|
|
||||||
export const handler: Handlers<Recipe[] | null> = {
|
export const handler: Handlers<Recipe[] | null> = {
|
||||||
async GET(_, ctx) {
|
async GET(_, ctx) {
|
||||||
@ -25,11 +26,11 @@ export default function Greet(props: PageProps<Recipe[] | null>) {
|
|||||||
|
|
||||||
<h3 class="text-2xl text-white font-light">🍽️ Recipes</h3>
|
<h3 class="text-2xl text-white font-light">🍽️ Recipes</h3>
|
||||||
</header>
|
</header>
|
||||||
<div class="flex flex-wrap items-center gap-4 px-4">
|
<Grid>
|
||||||
{props.data?.map((doc) => {
|
{props.data?.map((doc) => {
|
||||||
return <RecipeCard recipe={doc} />;
|
return <RecipeCard recipe={doc} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</Grid>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user