Clone starter

This commit is contained in:
2024-03-07 15:31:41 +01:00
commit 1b7ee20d05
87 changed files with 17015 additions and 0 deletions

17
apps/web/.eslintrc.cjs Normal file
View File

@ -0,0 +1,17 @@
module.exports = {
root: true,
extends: ["custom", "plugin:astro/recommended"],
overrides: [
{
files: ["*.astro"],
parser: "astro-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
extraFileExtensions: [".astro"],
},
rules: {
"@typescript-eslint/no-explicit-any": "off",
},
},
],
};

21
apps/web/.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store

13
apps/web/README.md Normal file
View File

@ -0,0 +1,13 @@
# Astro with Tailwind
```
npm create astro@latest -- --template with-tailwindcss
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/with-tailwindcss)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/with-tailwindcss)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/with-tailwindcss/devcontainer.json)
Astro comes with [Tailwind](https://tailwindcss.com) support out of the box. This example showcases how to style your Astro project with Tailwind.
For complete setup instructions, please see our [Tailwind Integration Guide](https://docs.astro.build/en/guides/integrations-guide/tailwind).

24
apps/web/astro.config.mjs Normal file
View File

@ -0,0 +1,24 @@
import mdx from "@astrojs/mdx";
import node from "@astrojs/node";
import svelte from "@astrojs/svelte";
import tailwind from "@astrojs/tailwind";
import { defineConfig } from "astro/config";
export default defineConfig({
integrations: [mdx(), tailwind(), svelte()],
server: {
port: parseInt(process.env.ASTRO_PORT ?? "3000"),
},
output: "server",
adapter: node({
mode: "standalone",
}),
vite: {
define: {
"import.meta.env.PAYLOAD_PUBLIC_SERVER_URL": JSON.stringify(
process.env.PAYLOAD_PUBLIC_SERVER_URL,
),
},
},
});

43
apps/web/package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "@turbopress/web",
"description": "Front end website based on Astro",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"lint": "eslint \"./src/**/*.{js,ts,tsx,astro}\" --fix",
"serve": "node dist/server/entry.mjs"
},
"dependencies": {
"@astrojs/mdx": "latest",
"@astrojs/node": "latest",
"@astrojs/svelte": "latest",
"@astrojs/tailwind": "latest",
"@turbopress/api": "*",
"astro": "latest",
"autoprefixer": "latest",
"@iconify/svelte": "latest",
"postcss": "latest",
"qs": "^6.11.2",
"slate": "latest",
"tailwindcss": "latest",
"astro-icon": "latest",
"nanostores": "latest",
"svelte": "latest",
"svelte-inview": "4.0.1 "
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@types/escape-html": "^1.0.2",
"@types/qs": "^6.9.7",
"escape-html": "^1.0.3",
"eslint": "latest",
"eslint-config-custom": "*",
"eslint-plugin-astro": "latest",
"svelte-breakpoints": "latest"
}
}

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

View File

@ -0,0 +1,6 @@
<div class="grid place-items-center h-full w-full content-center">
<div class="text-xl font-bold">No homepage has been setup.</div>
<div class="text-lg">
Create a new page with slug 'home' in the admin panel.
</div>
</div>

View File

@ -0,0 +1,18 @@
---
import type {
FormattedElement,
FormattedText,
Layout,
} from "@turbopress/api/types";
import RenderBody from "./body/RenderBody.astro";
import RenderHeader from "./header/RenderHeader.astro";
interface Props {
layout: Layout;
content?: (FormattedElement | FormattedText)[];
}
const { layout, content } = Astro.props;
---
<RenderHeader blocks={layout.header?.blocks} />
<RenderBody blocks={layout.body?.blocks} {content} />

View File

@ -0,0 +1,30 @@
---
import type { Content } from "@turbopress/api/types";
import { getContentSingle } from "../services/api/content.service";
import RenderSiteTitle from "./header/RenderSiteTitle.astro";
import RenderMenu from "./menu/RenderMenu.astro";
interface Props {
content: string | Content;
}
const content: Content | undefined =
typeof Astro.props.content == "string"
? await getContentSingle(Astro.props.content)
: Astro.props.content;
if (!content?.blocks) return;
const blocks = content.blocks;
---
{
blocks.map((block) => {
if (!block.id) return;
if (block.blockType == "siteTitle")
return <RenderSiteTitle siteTitle={block} />;
if (block.blockType == "menu" && block.menus)
return <RenderMenu menu={block} />;
return <div>block = {block.id}</div>;
})
}

View File

@ -0,0 +1,36 @@
---
import type {
FormattedElement,
FormattedText,
PageContent,
PageList,
ReusableContent,
} from "@turbopress/api/types";
import RenderContent from "../RenderReusableContent.astro";
import RenderPageContent from "./RenderPageContent.astro";
// import RenderPageList from "./page-list/RenderPageList.astro";
import RenderPageList from "./page-list/RenderPageList.svelte";
interface Props {
blocks?: (PageList | ReusableContent | PageContent)[];
content?: (FormattedElement | FormattedText)[];
}
const { blocks = [], content } = Astro.props;
if (blocks.length === 0) return;
---
<main class="p-6 flex flex-wrap">
{
blocks.map((block) => {
if (!block.id) return;
if (block.blockType == "reusableContent" && block.reference?.value)
return <RenderContent content={block.reference.value} />;
if (block.blockType == "pageContent")
return <RenderPageContent {content} />;
if (block.blockType == "pageList")
return <RenderPageList {block} client:only />;
return <div>{block.id}</div>;
})
}
</main>

View File

@ -0,0 +1,17 @@
---
import type { FormattedElement, FormattedText } from "@turbopress/api/types";
import RichText from "../rich-text/RichText.astro";
interface Props {
content?: (FormattedElement | FormattedText)[];
title?: string;
}
const { content } = Astro.props;
---
{
content && (
<article class="w-full justify-center prose max-w-none prose-headings:m-0 prose-headings:my-3 prose-p:m-0 prose-p:my-2">
<RichText richText={content} />
</article>
)
}

View File

@ -0,0 +1,18 @@
<script lang="ts">
import type { Page } from "@turbopress/api/types";
import RichText from "../../rich-text/RichText.svelte";
export let page: Page;
</script>
{#if page.content}
<section class="">
<header>
<h2>{page.title}</h2>
</header>
<article>
<RichText richText={page.content}></RichText>
</article>
<hr />
</section>
{/if}

View File

@ -0,0 +1,65 @@
<script lang="ts">
import type { Page, PageList } from "@turbopress/api/types";
import { inview } from "svelte-inview";
import { writable, type Writable } from "svelte/store";
import { getPageCollection } from "../../../services/api";
import type { PayloadCollection } from "../../../types";
import PageListItem from "./PageListItem.svelte";
export let block: PageList;
const query = {
where: {
or: [
{
categories: {
in: block.filterByCategories?.map((f) => f.value),
},
},
{
tags: {
in: block.filterByTags?.map((f) => f.value),
},
},
],
},
limit: block.numberOfItems ?? 5,
page: 1,
sort: block.sortBy,
};
const queryState = writable(query);
const collection: Writable<PayloadCollection<Page> | undefined> = writable();
async function getPages() {
const pages = await getPageCollection($queryState);
collection.set(pages);
}
queryState.subscribe((s) => {
getPages();
});
$: pages = $collection?.docs;
function handleChange({ detail }: CustomEvent<ObserverEventDetails>) {
isInView = detail.inView;
if (detail.inView && $collection?.hasNextPage) {
queryState.set({ ...$queryState, limit: $queryState.limit + 1 });
}
}
let isInView: boolean;
</script>
<div
class="w-full prose max-w-none prose-headings:m-0 prose-headings:my-3 prose-p:m-0 prose-p:my-2"
>
{#if pages}
{#each pages as page}
<PageListItem {page}></PageListItem>
{/each}
{/if}
<div use:inview on:inview_change={handleChange}></div>
</div>

View File

@ -0,0 +1,29 @@
---
import type { Menu, ReusableContent, SiteTitle } from "@turbopress/api/types";
import RenderContent from "../RenderReusableContent.astro";
import RenderMenu from "../menu/RenderMenu.astro";
import RenderSiteTitle from "./RenderSiteTitle.astro";
// import SiteTitle from "./SiteTitle.astro";
interface Props {
blocks?: (Menu | ReusableContent | SiteTitle)[];
}
const { blocks = [] } = Astro.props;
if (blocks.length === 0) return;
---
<header class="shadow p-6 flex flex-wrap">
{
blocks.map((block) => {
if (!block.id) return;
if (block.blockType == "siteTitle")
return <RenderSiteTitle siteTitle={block} />;
if (block.blockType == "reusableContent" && block.reference?.value)
return <RenderContent content={block.reference.value} />;
if (block.blockType == "menu" && block.menus)
return <RenderMenu menu={block} />;
return <div>{block.id}</div>;
})
}
</header>

View File

@ -0,0 +1,16 @@
---
import type { SiteTitle } from "@turbopress/api/types";
import Link from "../link/Link.astro";
interface Props {
siteTitle: SiteTitle;
}
const { siteTitle } = Astro.props;
---
<div class="flex-grow">
<Link link="/">
<div class="font-bold text-lg">{siteTitle.siteName}</div>
</Link>
<div class="w-full"></div>
</div>

View File

@ -0,0 +1,22 @@
---
interface Props {
link?: string | undefined;
target?: "_self" | "_blank" | "_top" | "_parent";
class?: string;
}
const { link, target, class: className } = Astro.props;
---
{
link && (
<a
href={link}
{target}
class={"cursor-pointer hover:text-indigo-600 " + className}
>
<slot />
</a>
)
}
{!link && <slot />}

View File

@ -0,0 +1,22 @@
<script lang="ts">
export let link: string | undefined = undefined;
export let target:
| "_self"
| "_blank"
| "_top"
| "_parent"
| undefined
| null = undefined;
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if link}
<a
href={link}
{target}
class="cursor-pointer hover:text-indigo-600 {$$props.class ?? ''}"
><slot /></a
>
{:else}
<slot />
{/if}

View File

@ -0,0 +1,18 @@
---
import type { MainMenu, Menu } from "@turbopress/api/types";
import DefaultMenu from "./default/DefaultMenu.svelte";
interface Props {
menu: Menu;
}
const { menu } = Astro.props;
const menus = menu.menus ?? [];
const mainMenus: MainMenu[] = menus.map((menu) => menu.mainMenu);
---
{
() => {
return <DefaultMenu menus={mainMenus} client:only="svelte" />;
}
}

View File

@ -0,0 +1,18 @@
<script lang="ts">
import type { MainMenu } from "@turbopress/api/types";
import { useMediaQuery } from "svelte-breakpoints";
import DefaultDesktopMenu from "./desktop/DefaultDesktopMenu.svelte";
import DefaultMobileMenu from "./mobile/DefaultMobileMenu.svelte";
export let menus: MainMenu[];
const isDesktop = useMediaQuery("(min-width: 1024px)");
</script>
{#if !$isDesktop}
<DefaultMobileMenu {menus}></DefaultMobileMenu>
{/if}
{#if $isDesktop}
<DefaultDesktopMenu {menus}></DefaultDesktopMenu>
{/if}

View File

@ -0,0 +1,11 @@
import { map } from "nanostores";
interface MobileMenuState {
isOpen: boolean;
activeIndex?: number;
}
export const mobileMenuState = map<MobileMenuState>({
isOpen: false,
activeIndex: undefined,
});

View File

@ -0,0 +1,32 @@
<script lang="ts">
import type { MainMenu } from "@turbopress/api/types";
import { mobileMenuState } from "../defaultMenu";
import MainMenuSvelte from "./_MainMenu.svelte";
export let menus: MainMenu[];
function handleClick() {
mobileMenuState.setKey("isOpen", !isOpen);
}
$: isOpen = $mobileMenuState.isOpen;
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="flex items-center cursor-pointer text-sm"
on:click={handleClick}
on:keypress={handleClick}
>
{#each menus as menu, i}
<MainMenuSvelte {menu} />
{/each}
</div>
<!-- {#if isOpen}
<div class="w-full cursor-pointer lg:hidden">
{#each menus as menu, i}
<MainMenuSvelte {menu} index={i} />
{/each}
</div>
{/if} -->

View File

@ -0,0 +1,39 @@
<script lang="ts">
import type { MainMenu } from "@turbopress/api/types";
import Link from "../../../link/Link.svelte";
export let menu: MainMenu;
const subMenus = menu.subMenu ?? [];
</script>
<div class="group relative inline-block text-left group">
<div id="menu-button" aria-expanded="false" aria-haspopup="true">
<Link
link={menu.url}
class="hover:text-black w-full group-hover:bg-slate-100 p-2 "
>
{menu.label}
</Link>
</div>
<div
class="mt-2 absolute right-0 z-10 w-56 origin-top-right rounded-sm bg-white ring-1 ring-slate-200 focus:outline-none hidden group-hover:block"
role="menu"
aria-orientation="vertical"
aria-labelledby="menu-button"
tabindex="-1"
>
<div class="" role="none">
{#each subMenus as subMenu}
<Link link={subMenu.link.url}>
<div
class="px-3 py-1.5 block hover:bg-slate-100 ring-1 ring-inset ring-gray-200 ring-opacity-30"
>
{subMenu.link.label}
</div>
</Link>
{/each}
</div>
</div>
</div>

View File

@ -0,0 +1,17 @@
<script lang="ts">
import type { Link } from "@turbopress/api/types";
import LinkSvelte from "../../../link/Link.svelte";
export let subMenu: {
link: Link;
id?: string;
};
</script>
<LinkSvelte
link={subMenu.link.url}
class="h-7 hover:text-black hover:bg-slate-100 flex items-center"
>
<div class="w-full px-3">
{subMenu.link.label}
</div>
</LinkSvelte>

View File

@ -0,0 +1,36 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import type { MainMenu } from "@turbopress/api/types";
import { mobileMenuState } from "../defaultMenu";
import MainMenuSvelte from "./_MainMenu.svelte";
export let menus: MainMenu[];
function handleClick() {
mobileMenuState.setKey("isOpen", !isOpen);
}
$: isOpen = $mobileMenuState.isOpen;
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="flex items-center cursor-pointer font-semibold hover:text-indigo-600"
on:click={handleClick}
on:keypress={handleClick}
>
{#if isOpen}
<Icon icon="ic:round-close" class="h-5 w-5 mr-2" />
{:else}
<Icon icon="ic:baseline-menu" class="h-5 w-5 mr-2" />
{/if}
<div>Menu</div>
</div>
{#if isOpen}
<div class="w-full cursor-pointer lg:hidden">
{#each menus as menu, i}
<MainMenuSvelte {menu} index={i} />
{/each}
</div>
{/if}

View File

@ -0,0 +1,44 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import type { MainMenu } from "@turbopress/api/types";
import Link from "../../../link/Link.svelte";
import { mobileMenuState } from "../defaultMenu";
import SubMenu from "./_SubMenu.svelte";
export let menu: MainMenu;
export let index: number;
$: isOpen = $mobileMenuState.activeIndex == index;
function handleClick() {
if (isOpen) mobileMenuState.setKey("activeIndex", undefined);
else mobileMenuState.setKey("activeIndex", index);
}
</script>
<div class="flex items-center h-7">
<Link link={menu.url} class="hover:text-black w-full hover:bg-slate-100 p-1 ">
<div class="w-full">{menu.label}</div>
</Link>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="hover:bg-slate-100 h-8 w-10 {isOpen ? 'bg-indigo-50' : ''}"
on:click={handleClick}
on:keydown={handleClick}
>
<Icon
icon="ic:sharp-keyboard-arrow-down"
class="text-2xl mx-auto mt-1 transition-transform duration-200 {isOpen
? 'rotate-180 text-indigo-800 '
: ''}"
></Icon>
</div>
</div>
{#if isOpen}
{#if menu.subMenu}
{#each menu.subMenu as subMenu}
<SubMenu {subMenu} />
{/each}
{/if}
{/if}

View File

@ -0,0 +1,17 @@
<script lang="ts">
import type { Link } from "@turbopress/api/types";
import LinkSvelte from "../../../link/Link.svelte";
export let subMenu: {
link: Link;
id?: string;
};
</script>
<LinkSvelte
link={subMenu.link.url}
class="h-7 hover:text-black hover:bg-slate-100 flex items-center"
>
<div class="w-full px-3">
{subMenu.link.label}
</div>
</LinkSvelte>

View File

@ -0,0 +1,65 @@
---
import type { FormattedElement, FormattedText } from "@turbopress/api/types";
import escapeHTML from "escape-html";
import { Text } from "slate";
interface Props {
richText: (FormattedElement | FormattedText)[];
}
const { richText } = Astro.props;
---
{
richText.map((node) => {
return Text.isText(node) ? (
<Fragment>
{node.bold && <strong>{node.text}</strong>}
{node.code && <code>{node.text}</code>}
{node.italic && <em>{node.text}</em>}
{!node.bold && !node.code && !node.italic && (
<Fragment>{node.text}</Fragment>
)}
</Fragment>
) : (
<Fragment>
{node.type === "h1" && (
<h1>{<Astro.self richText={node.children} />}</h1>
)}
{node.type === "h2" && (
<h2>{<Astro.self richText={node.children} />}</h2>
)}
{node.type === "h3" && (
<h3>{<Astro.self richText={node.children} />}</h3>
)}
{node.type === "h4" && (
<h4>{<Astro.self richText={node.children} />}</h4>
)}
{node.type === "h5" && (
<h5>{<Astro.self richText={node.children} />}</h5>
)}
{node.type === "h6" && (
<h6>{<Astro.self richText={node.children} />}</h6>
)}
{node.type === "quote" && (
<p>{<Astro.self richText={node.children} />}</p>
)}
{node.type === "ul" && (
<ul>{<Astro.self richText={node.children} />}</ul>
)}
{node.type === "ol" && (
<ol>{<Astro.self richText={node.children} />}</ol>
)}
{node.type === "li" && (
<li>{<Astro.self richText={node.children} />}</li>
)}
{node.type === "link" && (
<a href={escapeHTML(node.url)}>
{<Astro.self richText={node.children} />}
</a>
)}
{!node.type && <p>{<Astro.self richText={node.children} />}</p>}
</Fragment>
);
})
}

View File

@ -0,0 +1,45 @@
<script lang="ts">
import type { FormattedElement, FormattedText } from "@turbopress/api/types";
import { Text } from "slate";
export let richText: (FormattedElement | FormattedText | any)[];
</script>
{#each richText as node}
{#if Text.isText(node)}
{#if node.bold}
<strong>{node.text}</strong>
{/if}
{#if node.code}
<strong>{node.text}</strong>
{/if}
{#if node.italic}
<strong>{node.text}</strong>
{/if}
{#if !node.bold && !node.code && !node.italic}
{node.text}
{/if}
{:else}
{#if node.type === "h1"}
<h1><svelte:self richText={node.children}></svelte:self></h1>
{/if}
{#if node.type === "h2"}
<h2><svelte:self richText={node.children}></svelte:self></h2>
{/if}
{#if node.type === "h3"}
<h3><svelte:self richText={node.children}></svelte:self></h3>
{/if}
{#if node.type === "h4"}
<h4><svelte:self richText={node.children}></svelte:self></h4>
{/if}
{#if node.type === "h5"}
<h5><svelte:self richText={node.children}></svelte:self></h5>
{/if}
{#if node.type === "h6"}
<h6><svelte:self richText={node.children}></svelte:self></h6>
{/if}
{#if !node.type}
<p><svelte:self richText={node.children}></svelte:self></p>
{/if}
{/if}
{/each}

5
apps/web/src/env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="astro/client" />
interface ImportMetaEnv {
readonly ASTRO_PORT: string;
}

View File

@ -0,0 +1,48 @@
---
import type {
FormattedElement,
FormattedText,
Layout,
Media,
} from "@turbopress/api/types";
import RenderLayout from "../components/RenderLayout.astro";
interface Props {
title?: string;
description?: string;
layout?: string | Layout;
content?: (FormattedElement | FormattedText | any)[];
image?: string | Media;
}
const {
title = "AstroCMS",
description = "Astro, TailwindCSS, and PayloadCMS",
layout,
content,
image,
} = Astro.props;
const metaImage = image
? typeof image === "string"
? image
: image.url
: undefined;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="description" content={description} />
{metaImage && <meta property="og:image" content={metaImage} />}
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
</head>
<body class="h-screen">
{
layout && typeof layout != "string" && (
<RenderLayout layout={layout} {content} />
)
}
{!layout && <slot />}
</body>
</html>

View File

@ -0,0 +1,12 @@
---
import MainLayout from "../layouts/MainLayout.astro";
import { getLayoutSingle } from "../services/api/layout.service";
const layout = await getLayoutSingle("404");
---
<MainLayout title="Error 404" layout={layout} description="Page not found">
Not found, error 404 The page you are looking for no longer exists. Perhaps
you can return back to the homepage and see if you can find what you are
looking for. Or, you can try finding it by using the search form below.
</MainLayout>

View File

@ -0,0 +1,16 @@
---
import MainLayout from "../layouts/MainLayout.astro";
import { getPageSingle } from "../services/api";
const { slug } = Astro.params;
const page = await getPageSingle(slug!);
if (!page) return Astro.redirect("/404");
if (page.slug == "home") return Astro.redirect("/");
---
<MainLayout title={page.title} layout={page.layout} description="">
{page.title}
{page.layout}
<!-- {homePage && <RenderPage page={homePage} />}
{!homePage && <NoHomePage />} -->
</MainLayout>

View File

@ -0,0 +1,18 @@
---
import MainLayout from "../layouts/MainLayout.astro";
import { getPageSingle } from "../services/api";
const homePage = await getPageSingle("home");
const pageTitle = homePage?.meta?.title ?? homePage?.title ?? "TurboPress";
const layout = homePage?.layout;
---
<MainLayout
title={pageTitle}
{layout}
description={homePage?.meta?.description}
content={homePage?.content}
image={homePage?.meta?.image}
/>

View File

@ -0,0 +1,31 @@
import qs from "qs";
import type { PayloadCollection } from "../../types";
export async function apiFetch<T = any>(
url: string | URL,
options: RequestInit = {},
) {
const defaultOptions = {
headers: {
"Content-Type": "application/json",
},
};
const res = await fetch(url, { ...defaultOptions, ...options });
if (res.ok) {
return res.json() as T;
}
throw new Error(`Error fetching data: ${res.statusText} (${res.status})}`);
}
export async function getPayloadCollection<CollectionType>(
url: string | URL,
query: any = null,
) {
const stringifiedQuery = qs.stringify(query, { addQueryPrefix: true });
return apiFetch<PayloadCollection<CollectionType>>(url + stringifiedQuery);
}
export async function getPayloadDocument<CollectionType>(url: string | URL) {
return apiFetch<CollectionType>(url);
}

View File

@ -0,0 +1,16 @@
import type { Layout } from "@turbopress/api/types";
import { getPayloadCollection } from "./api.service";
export async function getContentCollection(query: any = null) {
const url = ` ${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/layouts`;
return getPayloadCollection<Layout>(url, query);
}
export async function getContentSingle(
name: string,
): Promise<Layout | undefined> {
const pages = await getContentCollection({
where: { name: { equals: name } },
});
if (pages.docs[0]) return pages.docs[0];
}

View File

@ -0,0 +1,2 @@
export * from "./api.service";
export * from "./page.service";

View File

@ -0,0 +1,16 @@
import type { Layout } from "@turbopress/api/types";
import { getPayloadCollection } from "./api.service";
export async function getLayoutCollection(query: any = null) {
const url = ` ${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/layouts`;
return getPayloadCollection<Layout>(url, query);
}
export async function getLayoutSingle(
name: string,
): Promise<Layout | undefined> {
const pages = await getLayoutCollection({
where: { name: { equals: name } },
});
if (pages.docs[0]) return pages.docs[0];
}

View File

@ -0,0 +1,19 @@
import type { Media } from "@turbopress/api/types";
import { getPayloadCollection, getPayloadDocument } from "./api.service";
export async function getMediaCollection(query: any = null) {
const url = `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/medias`;
return getPayloadCollection<Media>(url, query);
}
export async function getMediaSingle(slug: string): Promise<Media | undefined> {
const medias = await getMediaCollection({
where: { slug: { equals: slug } },
});
if (medias.docs[0]) return medias.docs[0];
}
export async function getMediaById(id: string): Promise<Media | undefined> {
const url = `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/media/` + id;
return getPayloadDocument<Media>(url);
}

View File

@ -0,0 +1,14 @@
import type { Page } from "@turbopress/api/types";
import { getPayloadCollection } from "./api.service";
export async function getPageCollection(query: any = null) {
const url = `${import.meta.env.PAYLOAD_PUBLIC_SERVER_URL}/api/pages`;
return getPayloadCollection<Page>(url, query);
}
export async function getPageSingle(slug: string): Promise<Page | undefined> {
const pages = await getPageCollection({
where: { slug: { equals: slug } },
});
if (pages.docs[0]) return pages.docs[0];
}

22
apps/web/src/types.ts Normal file
View File

@ -0,0 +1,22 @@
import type { FormattedElement, FormattedText } from "@turbopress/api/types";
declare module "slate" {
interface CustomTypes {
Element: FormattedElement;
Text: FormattedText;
}
}
export type PayloadCollection<CollectionType = any> = {
totalDocs?: number;
limit?: number;
totalPages?: number;
page?: number;
pagingCounter?: number;
hasPrevPage?: boolean;
hasNextPage?: boolean;
prevPage?: number;
nextPage?: number;
hasMore?: boolean;
docs: CollectionType[];
};

View File

@ -0,0 +1,7 @@
module.exports = {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: {
extend: {},
},
plugins: [require("@tailwindcss/typography")],
};

4
apps/web/tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "astro/tsconfigs/strict",
"exclude": ["node_modules"]
}