create-ampless 0.2.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +229 -0
- package/dist/templates/_shared/RUNBOOK.md +178 -0
- package/dist/templates/_shared/amplify/auth/post-confirmation/handler.ts +4 -0
- package/dist/templates/_shared/amplify/auth/post-confirmation/resource.ts +6 -0
- package/dist/templates/_shared/amplify/auth/resource.ts +8 -0
- package/dist/templates/_shared/amplify/backend.ts +29 -0
- package/dist/templates/_shared/amplify/data/get-published-post.js +33 -0
- package/dist/templates/_shared/amplify/data/list-posts-by-tag.js +52 -0
- package/dist/templates/_shared/amplify/data/list-published-posts.js +57 -0
- package/dist/templates/_shared/amplify/data/resource.ts +30 -0
- package/dist/templates/_shared/amplify/events/dispatcher/handler.ts +4 -0
- package/dist/templates/_shared/amplify/events/dispatcher/resource.ts +12 -0
- package/dist/templates/_shared/amplify/events/processor-trusted/handler.ts +12 -0
- package/dist/templates/_shared/amplify/events/processor-trusted/resource.ts +14 -0
- package/dist/templates/_shared/amplify/events/processor-untrusted/handler.ts +10 -0
- package/dist/templates/_shared/amplify/events/processor-untrusted/resource.ts +9 -0
- package/dist/templates/_shared/amplify/functions/api-key-renewer/handler.ts +4 -0
- package/dist/templates/_shared/amplify/functions/api-key-renewer/resource.ts +12 -0
- package/dist/templates/_shared/amplify/storage/resource.ts +7 -0
- package/dist/templates/_shared/app/(admin)/admin/layout.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/media/page.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/page.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/posts/[postId]/page.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/posts/new/page.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/posts/page.tsx +4 -0
- package/dist/templates/_shared/app/(admin)/admin/sites/[siteId]/page.tsx +5 -0
- package/dist/templates/_shared/app/(admin)/admin/sites/[siteId]/theme/page.tsx +6 -0
- package/dist/templates/_shared/app/(admin)/admin/sites/page.tsx +5 -0
- package/dist/templates/_shared/app/api/media/[...path]/route.ts +5 -0
- package/dist/templates/_shared/app/globals.css +114 -0
- package/dist/templates/_shared/app/layout.tsx +48 -0
- package/dist/templates/_shared/app/login/page.tsx +4 -0
- package/dist/templates/_shared/app/providers.tsx +13 -0
- package/dist/templates/_shared/app/site/[siteId]/[slug]/page.tsx +10 -0
- package/dist/templates/_shared/app/site/[siteId]/feed.xml/route.ts +5 -0
- package/dist/templates/_shared/app/site/[siteId]/og/[slug]/route.ts +6 -0
- package/dist/templates/_shared/app/site/[siteId]/page.tsx +10 -0
- package/dist/templates/_shared/app/site/[siteId]/raw/[slug]/route.ts +5 -0
- package/dist/templates/_shared/app/site/[siteId]/sitemap.xml/route.ts +5 -0
- package/dist/templates/_shared/app/site/[siteId]/tag/[tag]/page.tsx +10 -0
- package/dist/templates/_shared/cms.config.ts +110 -0
- package/dist/templates/_shared/components/i18n-provider.tsx +7 -0
- package/dist/templates/_shared/components/lightbox-content.tsx +69 -0
- package/dist/templates/_shared/components/site-chrome/collapsible-sidebar.tsx +54 -0
- package/dist/templates/_shared/components/site-chrome/mobile-menu.tsx +68 -0
- package/dist/templates/_shared/components/site-chrome/site-footer.tsx +43 -0
- package/dist/templates/_shared/components/site-chrome/site-header.tsx +94 -0
- package/dist/templates/_shared/components/site-chrome/site-sidebar.tsx +81 -0
- package/dist/templates/_shared/components/tag-list.tsx +25 -0
- package/dist/templates/_shared/components.json +21 -0
- package/dist/templates/_shared/lib/admin-site-client.ts +10 -0
- package/dist/templates/_shared/lib/admin-site.ts +8 -0
- package/dist/templates/_shared/lib/admin.ts +24 -0
- package/dist/templates/_shared/lib/ampless.ts +23 -0
- package/dist/templates/_shared/lib/amplify-server.ts +7 -0
- package/dist/templates/_shared/lib/amplify.ts +9 -0
- package/dist/templates/_shared/lib/auth-server.ts +11 -0
- package/dist/templates/_shared/lib/cn.ts +5 -0
- package/dist/templates/_shared/lib/i18n.ts +31 -0
- package/dist/templates/_shared/lib/kv-provider.ts +7 -0
- package/dist/templates/_shared/lib/media.ts +6 -0
- package/dist/templates/_shared/lib/posts-provider.ts +7 -0
- package/dist/templates/_shared/lib/posts-public.ts +19 -0
- package/dist/templates/_shared/lib/posts.ts +12 -0
- package/dist/templates/_shared/lib/seo.ts +8 -0
- package/dist/templates/_shared/lib/site-settings.ts +8 -0
- package/dist/templates/_shared/lib/storage.ts +7 -0
- package/dist/templates/_shared/lib/theme-actions.ts +5 -0
- package/dist/templates/_shared/lib/theme-active.ts +8 -0
- package/dist/templates/_shared/lib/theme-config.ts +10 -0
- package/dist/templates/_shared/lib/upload.ts +6 -0
- package/dist/templates/_shared/middleware.ts +13 -0
- package/dist/templates/_shared/next.config.mjs +11 -0
- package/dist/templates/_shared/package.json +63 -0
- package/dist/templates/_shared/postcss.config.mjs +5 -0
- package/dist/templates/_shared/themes-registry.ts +38 -0
- package/dist/templates/_shared/tsconfig.json +23 -0
- package/dist/templates/blog/README.md +52 -0
- package/dist/templates/blog/index.ts +29 -0
- package/dist/templates/blog/manifest.ts +144 -0
- package/dist/templates/blog/pages/feed.ts +31 -0
- package/dist/templates/blog/pages/home.tsx +108 -0
- package/dist/templates/blog/pages/post.tsx +94 -0
- package/dist/templates/blog/pages/sitemap.ts +30 -0
- package/dist/templates/blog/pages/tag.tsx +76 -0
- package/dist/templates/blog/tokens.css +54 -0
- package/dist/templates/corporate/README.md +20 -0
- package/dist/templates/corporate/index.ts +25 -0
- package/dist/templates/corporate/manifest.ts +94 -0
- package/dist/templates/corporate/pages/feed.ts +29 -0
- package/dist/templates/corporate/pages/home.tsx +130 -0
- package/dist/templates/corporate/pages/post.tsx +96 -0
- package/dist/templates/corporate/pages/sitemap.ts +28 -0
- package/dist/templates/corporate/pages/tag.tsx +81 -0
- package/dist/templates/corporate/tokens.css +47 -0
- package/dist/templates/dads/README.md +35 -0
- package/dist/templates/dads/index.ts +25 -0
- package/dist/templates/dads/manifest.ts +84 -0
- package/dist/templates/dads/pages/feed.ts +29 -0
- package/dist/templates/dads/pages/home.tsx +126 -0
- package/dist/templates/dads/pages/post.tsx +102 -0
- package/dist/templates/dads/pages/sitemap.ts +28 -0
- package/dist/templates/dads/pages/tag.tsx +86 -0
- package/dist/templates/dads/tokens.css +67 -0
- package/dist/templates/docs/README.md +27 -0
- package/dist/templates/docs/index.ts +25 -0
- package/dist/templates/docs/manifest.ts +89 -0
- package/dist/templates/docs/pages/feed.ts +29 -0
- package/dist/templates/docs/pages/home.tsx +88 -0
- package/dist/templates/docs/pages/post.tsx +96 -0
- package/dist/templates/docs/pages/sitemap.ts +28 -0
- package/dist/templates/docs/pages/tag.tsx +79 -0
- package/dist/templates/docs/tokens.css +55 -0
- package/dist/templates/landing/README.md +25 -0
- package/dist/templates/landing/index.ts +25 -0
- package/dist/templates/landing/manifest.ts +118 -0
- package/dist/templates/landing/pages/feed.ts +31 -0
- package/dist/templates/landing/pages/home.tsx +123 -0
- package/dist/templates/landing/pages/post.tsx +95 -0
- package/dist/templates/landing/pages/sitemap.ts +28 -0
- package/dist/templates/landing/pages/tag.tsx +85 -0
- package/dist/templates/landing/tokens.css +47 -0
- package/dist/templates/minimal/README.md +52 -0
- package/dist/templates/minimal/index.ts +25 -0
- package/dist/templates/minimal/manifest.ts +35 -0
- package/dist/templates/minimal/pages/feed.ts +31 -0
- package/dist/templates/minimal/pages/home.tsx +44 -0
- package/dist/templates/minimal/pages/post.tsx +65 -0
- package/dist/templates/minimal/pages/sitemap.ts +30 -0
- package/dist/templates/minimal/pages/tag.tsx +46 -0
- package/dist/templates/minimal/tokens.css +46 -0
- package/package.json +41 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { Menu } from 'lucide-react'
|
|
5
|
+
import { Sheet, SheetContent, SheetTrigger } from '@ampless/runtime/ui'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Sidebar content — typically `<SiteSidebar ... />`. */
|
|
9
|
+
children: React.ReactNode
|
|
10
|
+
/** Label on the toggle button (mobile only). */
|
|
11
|
+
label?: string
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a sidebar so it slides in from the left as a Sheet drawer on
|
|
17
|
+
* small screens, and renders inline / sticky at `lg` and above. The
|
|
18
|
+
* wrapped content can still be a server component (passed as children
|
|
19
|
+
* and rendered as-is); only the open/closed toggle lives client-side.
|
|
20
|
+
*
|
|
21
|
+
* The children render twice — once inline (visible only at lg+) and
|
|
22
|
+
* once inside the Sheet (mounted only when open, portaled to body) —
|
|
23
|
+
* which is fine because they're already-evaluated React elements with
|
|
24
|
+
* no per-instance state of their own.
|
|
25
|
+
*/
|
|
26
|
+
export function CollapsibleSidebar({ children, label = 'Menu', className }: Props) {
|
|
27
|
+
const [open, setOpen] = useState(false)
|
|
28
|
+
return (
|
|
29
|
+
<div className={className}>
|
|
30
|
+
<Sheet open={open} onOpenChange={setOpen}>
|
|
31
|
+
<SheetTrigger
|
|
32
|
+
aria-label={label}
|
|
33
|
+
className="flex w-full items-center gap-2 rounded-md border bg-[var(--card)] px-4 py-2 text-sm font-medium hover:bg-[var(--accent)] lg:hidden"
|
|
34
|
+
>
|
|
35
|
+
<Menu className="h-4 w-4" />
|
|
36
|
+
<span>{label}</span>
|
|
37
|
+
</SheetTrigger>
|
|
38
|
+
<SheetContent
|
|
39
|
+
side="left"
|
|
40
|
+
className="w-72 overflow-y-auto px-6 py-12"
|
|
41
|
+
onClick={(e) => {
|
|
42
|
+
// SiteSidebar is a server component, so we can't wrap each
|
|
43
|
+
// Link in <SheetClose>. Detect anchor clicks via delegation
|
|
44
|
+
// instead and close the sheet on navigation.
|
|
45
|
+
if ((e.target as HTMLElement).closest('a')) setOpen(false)
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
{children}
|
|
49
|
+
</SheetContent>
|
|
50
|
+
</Sheet>
|
|
51
|
+
<div className="hidden lg:block">{children}</div>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { isTagListUrl, type LinkListItem } from 'ampless'
|
|
6
|
+
import { Sheet, SheetClose, SheetContent, SheetTrigger } from '@ampless/runtime/ui'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
items: LinkListItem[]
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Mobile nav for SiteHeader. Animated hamburger toggle that opens a
|
|
15
|
+
* right-side Sheet drawer with the linkList. The button morphs from
|
|
16
|
+
* three lines to an X via CSS transforms when the sheet is open. ESC,
|
|
17
|
+
* overlay click, and link click all close the drawer.
|
|
18
|
+
*
|
|
19
|
+
* Tag references collapse to plain text — same rule as the desktop
|
|
20
|
+
* header. Sidebars are the right surface for tag-driven post lists.
|
|
21
|
+
*/
|
|
22
|
+
export function MobileMenu({ items, className }: Props) {
|
|
23
|
+
const [open, setOpen] = useState(false)
|
|
24
|
+
return (
|
|
25
|
+
<Sheet open={open} onOpenChange={setOpen}>
|
|
26
|
+
<SheetTrigger
|
|
27
|
+
aria-label="Menu"
|
|
28
|
+
className={`group relative inline-flex h-9 w-9 items-center justify-center rounded-md hover:bg-[var(--accent)] ${className ?? ''}`}
|
|
29
|
+
>
|
|
30
|
+
{/* Three lines that morph into an X. Radix sets `data-state`
|
|
31
|
+
("open" / "closed") on this trigger element; the spans
|
|
32
|
+
below tap into it via `group-data-[state=open]:`. */}
|
|
33
|
+
<span className="sr-only">Menu</span>
|
|
34
|
+
<span aria-hidden className="block h-4 w-5 relative">
|
|
35
|
+
<span className="absolute left-0 top-0 h-0.5 w-5 bg-current origin-center transition-transform duration-200 group-data-[state=open]:translate-y-[7px] group-data-[state=open]:rotate-45" />
|
|
36
|
+
<span className="absolute left-0 top-1.5 h-0.5 w-5 bg-current transition-opacity duration-200 group-data-[state=open]:opacity-0" />
|
|
37
|
+
<span className="absolute left-0 top-3 h-0.5 w-5 bg-current origin-center transition-transform duration-200 group-data-[state=open]:-translate-y-[7px] group-data-[state=open]:-rotate-45" />
|
|
38
|
+
</span>
|
|
39
|
+
</SheetTrigger>
|
|
40
|
+
<SheetContent side="right" className="w-72">
|
|
41
|
+
<nav className="flex flex-col gap-1 px-6 py-16 text-base">
|
|
42
|
+
{items.map((item, i) => {
|
|
43
|
+
if (isTagListUrl(item.url)) {
|
|
44
|
+
return (
|
|
45
|
+
<span
|
|
46
|
+
key={i}
|
|
47
|
+
className="px-2 py-3 text-muted-foreground"
|
|
48
|
+
>
|
|
49
|
+
{item.label}
|
|
50
|
+
</span>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
return (
|
|
54
|
+
<SheetClose asChild key={i}>
|
|
55
|
+
<Link
|
|
56
|
+
href={item.url}
|
|
57
|
+
className="rounded-md px-2 py-3 text-foreground hover:bg-[var(--accent)] hover:text-[var(--primary)]"
|
|
58
|
+
>
|
|
59
|
+
{item.label}
|
|
60
|
+
</Link>
|
|
61
|
+
</SheetClose>
|
|
62
|
+
)
|
|
63
|
+
})}
|
|
64
|
+
</nav>
|
|
65
|
+
</SheetContent>
|
|
66
|
+
</Sheet>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import Link from 'next/link'
|
|
2
|
+
import { parseLinkList, isTagListUrl } from 'ampless'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
links: string | undefined
|
|
6
|
+
/** Optional below-links text (copyright, tagline). */
|
|
7
|
+
legend?: React.ReactNode
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Theme-agnostic footer rendering a `linkList` plus optional legend
|
|
13
|
+
* below. Same tag: handling rule as the header — collapse to plain
|
|
14
|
+
* text rather than blow out the chrome with post lists.
|
|
15
|
+
*/
|
|
16
|
+
export function SiteFooter({ links, legend, className }: Props) {
|
|
17
|
+
const items = parseLinkList(links)
|
|
18
|
+
return (
|
|
19
|
+
<footer className={`border-t px-6 py-8 ${className ?? ''}`}>
|
|
20
|
+
<div className="mx-auto max-w-5xl space-y-4">
|
|
21
|
+
{items.length > 0 && (
|
|
22
|
+
<nav className="flex flex-wrap gap-x-6 gap-y-2 text-sm text-muted-foreground">
|
|
23
|
+
{items.map((item, i) => {
|
|
24
|
+
if (isTagListUrl(item.url)) {
|
|
25
|
+
return <span key={i}>{item.label}</span>
|
|
26
|
+
}
|
|
27
|
+
return (
|
|
28
|
+
<Link
|
|
29
|
+
key={i}
|
|
30
|
+
href={item.url}
|
|
31
|
+
className="hover:text-[var(--primary)]"
|
|
32
|
+
>
|
|
33
|
+
{item.label}
|
|
34
|
+
</Link>
|
|
35
|
+
)
|
|
36
|
+
})}
|
|
37
|
+
</nav>
|
|
38
|
+
)}
|
|
39
|
+
{legend && <div className="text-xs text-muted-foreground">{legend}</div>}
|
|
40
|
+
</div>
|
|
41
|
+
</footer>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import Link from 'next/link'
|
|
2
|
+
import { parseLinkList, isTagListUrl } from 'ampless'
|
|
3
|
+
import { MobileMenu } from './mobile-menu'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** JSON-stringified linkList from theme.values.<key>. */
|
|
7
|
+
links: string | undefined
|
|
8
|
+
/** Optional logo image URL. When set, rendered as an <img>; the
|
|
9
|
+
* siteName is used as alt text. Empty falls back to siteName text. */
|
|
10
|
+
logoUrl?: string
|
|
11
|
+
/** Site name. Used as the brand text when no logo, and as alt
|
|
12
|
+
* text on the logo image. */
|
|
13
|
+
siteName?: string
|
|
14
|
+
/** Tailwind classes for the brand text wrapper (only when no logo). */
|
|
15
|
+
brandClassName?: string
|
|
16
|
+
/** Tailwind classes for the logo `<img>`. Defaults to a 32px-tall
|
|
17
|
+
* auto-width sizing — themes can override for taller / specific
|
|
18
|
+
* branding placements. */
|
|
19
|
+
logoClassName?: string
|
|
20
|
+
className?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Theme-agnostic header that consumes a `linkList` value (JSON string
|
|
25
|
+
* from a theme manifest field). Renders a logo image when `logoUrl`
|
|
26
|
+
* is set; otherwise shows `siteName` text. Themes pick the brand
|
|
27
|
+
* styling (font size / weight) via `brandClassName` so each theme can
|
|
28
|
+
* keep its own typographic identity.
|
|
29
|
+
*
|
|
30
|
+
* Responsive: the regular `<nav>` is hidden below `md` and replaced
|
|
31
|
+
* by a hamburger toggle (MobileMenu) that drops a panel overlay below
|
|
32
|
+
* the header.
|
|
33
|
+
*
|
|
34
|
+
* `tag:<name>` URLs in the link list collapse to plain text — header
|
|
35
|
+
* isn't the right surface for inline post lists; use SiteSidebar for
|
|
36
|
+
* tag-driven nav.
|
|
37
|
+
*/
|
|
38
|
+
export function SiteHeader({
|
|
39
|
+
links,
|
|
40
|
+
logoUrl,
|
|
41
|
+
siteName,
|
|
42
|
+
brandClassName,
|
|
43
|
+
logoClassName = 'h-8 w-auto',
|
|
44
|
+
className,
|
|
45
|
+
}: Props) {
|
|
46
|
+
const items = parseLinkList(links)
|
|
47
|
+
const trimmedLogo = logoUrl?.trim()
|
|
48
|
+
return (
|
|
49
|
+
<header
|
|
50
|
+
className={`relative flex items-center justify-between border-b px-6 py-4 ${className ?? ''}`}
|
|
51
|
+
>
|
|
52
|
+
<Link
|
|
53
|
+
href="/"
|
|
54
|
+
className={
|
|
55
|
+
trimmedLogo
|
|
56
|
+
? 'inline-flex items-center'
|
|
57
|
+
: (brandClassName ?? 'font-semibold hover:text-[var(--primary)]')
|
|
58
|
+
}
|
|
59
|
+
>
|
|
60
|
+
{trimmedLogo ? (
|
|
61
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
62
|
+
<img src={trimmedLogo} alt={siteName ?? ''} className={logoClassName} />
|
|
63
|
+
) : (
|
|
64
|
+
(siteName ?? 'Home')
|
|
65
|
+
)}
|
|
66
|
+
</Link>
|
|
67
|
+
{items.length > 0 && (
|
|
68
|
+
<>
|
|
69
|
+
<nav className="hidden items-center gap-5 text-sm md:flex">
|
|
70
|
+
{items.map((item, i) => {
|
|
71
|
+
if (isTagListUrl(item.url)) {
|
|
72
|
+
return (
|
|
73
|
+
<span key={i} className="text-muted-foreground">
|
|
74
|
+
{item.label}
|
|
75
|
+
</span>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
return (
|
|
79
|
+
<Link
|
|
80
|
+
key={i}
|
|
81
|
+
href={item.url}
|
|
82
|
+
className="text-foreground hover:text-[var(--primary)]"
|
|
83
|
+
>
|
|
84
|
+
{item.label}
|
|
85
|
+
</Link>
|
|
86
|
+
)
|
|
87
|
+
})}
|
|
88
|
+
</nav>
|
|
89
|
+
<MobileMenu items={items} className="md:hidden" />
|
|
90
|
+
</>
|
|
91
|
+
)}
|
|
92
|
+
</header>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import Link from 'next/link'
|
|
2
|
+
import { parseLinkList, isTagListUrl } from 'ampless'
|
|
3
|
+
import { listPostsByTag } from '@/lib/posts-public'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
links: string | undefined
|
|
7
|
+
siteId: string
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Sidebar nav with tag expansion. For docs-style sites: a `linkList`
|
|
13
|
+
* entry whose URL is `tag:<name>` is rendered as a labelled section
|
|
14
|
+
* with every published post tagged `<name>` listed underneath. Plain
|
|
15
|
+
* URLs render as a single link in the same flat list.
|
|
16
|
+
*
|
|
17
|
+
* Each tag: entry triggers one AppSync query. With ~10 tag sections
|
|
18
|
+
* in a sidebar that's 10 queries per render; revalidation should be
|
|
19
|
+
* paired with `force-dynamic` on the page so fresh content shows up
|
|
20
|
+
* after publish events.
|
|
21
|
+
*/
|
|
22
|
+
export async function SiteSidebar({ links, siteId, className }: Props) {
|
|
23
|
+
const items = parseLinkList(links)
|
|
24
|
+
if (items.length === 0) return null
|
|
25
|
+
|
|
26
|
+
// Resolve tag: entries up front so we render after all data is in.
|
|
27
|
+
const sections = await Promise.all(
|
|
28
|
+
items.map(async (item) => {
|
|
29
|
+
const tagRef = isTagListUrl(item.url)
|
|
30
|
+
if (!tagRef) return { type: 'link' as const, label: item.label, url: item.url }
|
|
31
|
+
const { items: posts } = await listPostsByTag(tagRef.tag, { siteId, limit: 50 })
|
|
32
|
+
return {
|
|
33
|
+
type: 'tagSection' as const,
|
|
34
|
+
label: item.label,
|
|
35
|
+
tag: tagRef.tag,
|
|
36
|
+
posts: posts.map((p) => ({ slug: p.slug, title: p.title })),
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<aside className={`space-y-6 ${className ?? ''}`}>
|
|
43
|
+
{sections.map((section, i) => {
|
|
44
|
+
if (section.type === 'link') {
|
|
45
|
+
return (
|
|
46
|
+
<Link
|
|
47
|
+
key={i}
|
|
48
|
+
href={section.url}
|
|
49
|
+
className="block text-sm font-medium hover:text-[var(--primary)]"
|
|
50
|
+
>
|
|
51
|
+
{section.label}
|
|
52
|
+
</Link>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
return (
|
|
56
|
+
<div key={i}>
|
|
57
|
+
<p className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
58
|
+
{section.label}
|
|
59
|
+
</p>
|
|
60
|
+
<ul className="space-y-1">
|
|
61
|
+
{section.posts.length === 0 ? (
|
|
62
|
+
<li className="text-xs text-muted-foreground">No posts.</li>
|
|
63
|
+
) : (
|
|
64
|
+
section.posts.map((post) => (
|
|
65
|
+
<li key={post.slug}>
|
|
66
|
+
<Link
|
|
67
|
+
href={`/${post.slug}`}
|
|
68
|
+
className="block text-sm text-foreground hover:text-[var(--primary)]"
|
|
69
|
+
>
|
|
70
|
+
{post.title}
|
|
71
|
+
</Link>
|
|
72
|
+
</li>
|
|
73
|
+
))
|
|
74
|
+
)}
|
|
75
|
+
</ul>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
})}
|
|
79
|
+
</aside>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import Link from 'next/link'
|
|
2
|
+
|
|
3
|
+
interface TagListProps {
|
|
4
|
+
tags?: string[] | null
|
|
5
|
+
className?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Renders post tags as chip-style links to /tag/[tag]. Pure-server, no JS.
|
|
9
|
+
export function TagList({ tags, className }: TagListProps) {
|
|
10
|
+
if (!tags?.length) return null
|
|
11
|
+
return (
|
|
12
|
+
<ul className={`flex flex-wrap gap-2 ${className ?? ''}`}>
|
|
13
|
+
{tags.map((tag) => (
|
|
14
|
+
<li key={tag}>
|
|
15
|
+
<Link
|
|
16
|
+
href={`/tag/${encodeURIComponent(tag)}`}
|
|
17
|
+
className="inline-block rounded-full border px-3 py-0.5 text-xs text-muted-foreground hover:border-foreground hover:text-foreground"
|
|
18
|
+
>
|
|
19
|
+
#{tag}
|
|
20
|
+
</Link>
|
|
21
|
+
</li>
|
|
22
|
+
))}
|
|
23
|
+
</ul>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "@/components",
|
|
15
|
+
"utils": "@/lib/cn",
|
|
16
|
+
"ui": "@/components/ui",
|
|
17
|
+
"lib": "@/lib",
|
|
18
|
+
"hooks": "@/hooks"
|
|
19
|
+
},
|
|
20
|
+
"iconLibrary": "lucide"
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Back-compat shim. Client-side admin-site helpers moved to
|
|
2
|
+
// `@ampless/admin` (L2 extraction). Existing call sites
|
|
3
|
+
// (`readAdminSiteIdFromCookie`, the `ADMIN_SITE_COOKIE` constant) keep
|
|
4
|
+
// working through this shim — the cms.config registration is performed
|
|
5
|
+
// inside the admin's <AdminProviders> bootstrap.
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
ADMIN_SITE_COOKIE,
|
|
9
|
+
readAdminSiteIdFromCookie,
|
|
10
|
+
} from '@ampless/admin/components'
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Back-compat shim. Server-side admin-site helpers moved to
|
|
2
|
+
// `@ampless/admin` (L2 extraction). New code should call the same
|
|
3
|
+
// methods on the `admin` instance directly.
|
|
4
|
+
|
|
5
|
+
import { admin } from './admin'
|
|
6
|
+
|
|
7
|
+
export const currentAdminSiteId = admin.currentAdminSiteId.bind(admin)
|
|
8
|
+
export const adminSiteOptions = admin.adminSiteOptions.bind(admin)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Wired-up admin UI factory. Single source of truth for the admin
|
|
2
|
+
// library — every admin route shell, API route shell, and form
|
|
3
|
+
// re-export shim imports the `admin` value from here.
|
|
4
|
+
//
|
|
5
|
+
// L2 architectural change (admin extraction): admin UI now lives in
|
|
6
|
+
// `@ampless/admin`. This module wires the project's
|
|
7
|
+
// `amplify_outputs.json`, `cms.config`, and `ampless` runtime into a
|
|
8
|
+
// single `Admin` instance.
|
|
9
|
+
|
|
10
|
+
import outputs from '../amplify_outputs.json'
|
|
11
|
+
import cmsConfig from '@/cms.config'
|
|
12
|
+
import { createAdmin } from '@ampless/admin'
|
|
13
|
+
import { ampless } from './ampless'
|
|
14
|
+
|
|
15
|
+
export const admin = createAdmin({
|
|
16
|
+
outputs,
|
|
17
|
+
cmsConfig,
|
|
18
|
+
ampless,
|
|
19
|
+
locale: (cmsConfig as { locale?: string }).locale ?? 'en',
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// Convenience: the server-side translation helper. Client components
|
|
23
|
+
// should use `useT()` from `@/components/i18n-provider` instead.
|
|
24
|
+
export const t = admin.t
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Wired-up ampless runtime instance. Single source of truth for the
|
|
2
|
+
// public-side library — every route handler, dispatcher, and theme
|
|
3
|
+
// component imports the `ampless` value from here.
|
|
4
|
+
//
|
|
5
|
+
// L1 architectural change (runtime extraction): public-side
|
|
6
|
+
// behaviour now lives in `@ampless/runtime`. This module wires the
|
|
7
|
+
// project's `amplify_outputs.json`, `cms.config`, and themes registry
|
|
8
|
+
// into a single `Ampless` instance.
|
|
9
|
+
//
|
|
10
|
+
// Admin-side modules (post providers, kv-provider, auth, etc.) stay
|
|
11
|
+
// in `templates/_shared/lib/` for now — they move into `@ampless/admin`
|
|
12
|
+
// in L2.
|
|
13
|
+
|
|
14
|
+
import outputs from '../amplify_outputs.json'
|
|
15
|
+
import cmsConfig from '@/cms.config'
|
|
16
|
+
import { themes, DEFAULT_THEME } from '@/themes-registry'
|
|
17
|
+
import { createAmpless } from '@ampless/runtime'
|
|
18
|
+
|
|
19
|
+
export const ampless = createAmpless({
|
|
20
|
+
outputs,
|
|
21
|
+
cmsConfig,
|
|
22
|
+
themes: { themes, defaultTheme: DEFAULT_THEME },
|
|
23
|
+
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Back-compat shim. The Amplify SSR server runner moved to
|
|
2
|
+
// `@ampless/admin` (L2 extraction). Existing call sites that import
|
|
3
|
+
// `runWithAmplifyServerContext` from here keep working.
|
|
4
|
+
|
|
5
|
+
import { admin } from './admin'
|
|
6
|
+
|
|
7
|
+
export const { runWithAmplifyServerContext } = admin.amplifyServer
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Back-compat shim. The Amplify SDK is now configured by the admin's
|
|
2
|
+
// `<AdminProviders>` bootstrap (mounted by the layout factory in
|
|
3
|
+
// `@ampless/admin/pages`), so most call sites no longer need to call
|
|
4
|
+
// `configureAmplify()` themselves. Kept as a no-op so any lingering
|
|
5
|
+
// `import '@/lib/amplify'` side-effect imports stay safe.
|
|
6
|
+
|
|
7
|
+
export function configureAmplify() {
|
|
8
|
+
// intentionally empty — admin bootstrap handles this
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Back-compat shim. Auth helpers moved to `@ampless/admin` (L2
|
|
2
|
+
// extraction). New code should call `admin.getServerSession` /
|
|
3
|
+
// `admin.isAdmin` / `admin.isEditor` directly.
|
|
4
|
+
|
|
5
|
+
import { admin } from './admin'
|
|
6
|
+
|
|
7
|
+
export type { ServerSession } from '@ampless/admin'
|
|
8
|
+
|
|
9
|
+
export const getServerSession = admin.getServerSession.bind(admin)
|
|
10
|
+
export const isAdmin = admin.isAdmin.bind(admin)
|
|
11
|
+
export const isEditor = admin.isEditor.bind(admin)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Back-compat shim. Admin i18n moved to `@ampless/admin` (L2
|
|
2
|
+
// extraction). The dictionary is bound at admin-factory time in
|
|
3
|
+
// `lib/admin.ts`; this file keeps the existing call sites (`t(...)`,
|
|
4
|
+
// `getLocale()`, `getDictionary()`) working.
|
|
5
|
+
//
|
|
6
|
+
// New code should use `admin.t` (from `@/lib/admin`) or the client-side
|
|
7
|
+
// `useT()` (from `@/components/i18n-provider`).
|
|
8
|
+
|
|
9
|
+
import { admin } from './admin'
|
|
10
|
+
import { getDictionary as adminGetDictionary, type Dictionary, type Locale } from '@ampless/admin'
|
|
11
|
+
|
|
12
|
+
export type { Dictionary, Locale }
|
|
13
|
+
export const FALLBACK_LOCALE: Locale = 'en'
|
|
14
|
+
|
|
15
|
+
export function getLocale(): Locale {
|
|
16
|
+
return admin.locale
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getDictionary(locale: Locale = admin.locale): Dictionary {
|
|
20
|
+
return adminGetDictionary(locale)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const t = admin.t
|
|
24
|
+
|
|
25
|
+
export function translate(
|
|
26
|
+
_dict: Dictionary,
|
|
27
|
+
key: string,
|
|
28
|
+
vars?: Record<string, string | number>
|
|
29
|
+
): string {
|
|
30
|
+
return admin.t(key, vars)
|
|
31
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Back-compat shim. KvStore provider installation moved to
|
|
2
|
+
// `@ampless/admin` (L2 extraction). The install runs automatically
|
|
3
|
+
// inside the admin's <AdminProviders> bootstrap, so a side-effect
|
|
4
|
+
// `import '@/lib/kv-provider'` is no longer required — kept as a
|
|
5
|
+
// safe no-op for legacy callers.
|
|
6
|
+
|
|
7
|
+
export {}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Back-compat shim. Media URL helper moved to `@ampless/admin` (L2
|
|
2
|
+
// extraction). The client-side state is registered inside the admin's
|
|
3
|
+
// <AdminProviders> bootstrap; existing call sites
|
|
4
|
+
// (`publicMediaUrl(...)`) keep working through this shim.
|
|
5
|
+
|
|
6
|
+
export { publicMediaUrl } from '@ampless/admin/components'
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Back-compat shim. Admin Posts provider installation moved to
|
|
2
|
+
// `@ampless/admin` (L2 extraction). The install runs automatically
|
|
3
|
+
// inside the admin's <AdminProviders> bootstrap, so a side-effect
|
|
4
|
+
// `import '@/lib/posts-provider'` is no longer required — kept as a
|
|
5
|
+
// safe no-op for legacy callers.
|
|
6
|
+
|
|
7
|
+
export {}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Back-compat shim. Implementation moved to `@ampless/runtime` (L1
|
|
2
|
+
// extraction). Theme files keep importing from `@/lib/posts-public`
|
|
3
|
+
// unchanged.
|
|
4
|
+
//
|
|
5
|
+
// New code should import from `@/lib/ampless` directly:
|
|
6
|
+
// import { ampless } from '@/lib/ampless'
|
|
7
|
+
// const posts = await ampless.listPublishedPosts({ siteId })
|
|
8
|
+
|
|
9
|
+
import { ampless } from './ampless'
|
|
10
|
+
|
|
11
|
+
export const listPublishedPosts = ampless.listPublishedPosts.bind(ampless)
|
|
12
|
+
export const getPublishedPost = ampless.getPublishedPost.bind(ampless)
|
|
13
|
+
export const listPostsByTag = ampless.listPostsByTag.bind(ampless)
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
ListPostsOptions,
|
|
17
|
+
ListPostsByTagOptions,
|
|
18
|
+
ListPostsResult,
|
|
19
|
+
} from '@ampless/runtime'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Back-compat shim. Body rendering + format converters moved to
|
|
2
|
+
// `@ampless/runtime` (L1 extraction). The renderer is reachable
|
|
3
|
+
// directly via `ampless.renderBody`, but theme files and the admin
|
|
4
|
+
// post form still import these names from `@/lib/posts`.
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
renderBody,
|
|
8
|
+
tiptapToHtml,
|
|
9
|
+
markdownToHtml,
|
|
10
|
+
tiptapToMarkdown,
|
|
11
|
+
htmlToMarkdown,
|
|
12
|
+
} from '@ampless/runtime'
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Back-compat shim. SEO metadata aggregation moved to
|
|
2
|
+
// `@ampless/runtime`. New code should call `ampless.postMetadata` /
|
|
3
|
+
// `ampless.siteMetadata` directly.
|
|
4
|
+
|
|
5
|
+
import { ampless } from './ampless'
|
|
6
|
+
|
|
7
|
+
export const postMetadata = ampless.postMetadata.bind(ampless)
|
|
8
|
+
export const siteMetadata = ampless.siteMetadata.bind(ampless)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Back-compat shim. Site-settings loader moved to `@ampless/runtime`.
|
|
2
|
+
// New code should call `ampless.loadSiteSettings` directly.
|
|
3
|
+
|
|
4
|
+
import { ampless } from './ampless'
|
|
5
|
+
|
|
6
|
+
export const loadSiteSettings = ampless.loadSiteSettings.bind(ampless)
|
|
7
|
+
|
|
8
|
+
export type { EffectiveSiteSettings } from '@ampless/runtime'
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Back-compat shim. Storage URL helpers moved to `@ampless/runtime`.
|
|
2
|
+
// New code should call `ampless.publicAssetUrl` / `ampless.isStorageConfigured` directly.
|
|
3
|
+
|
|
4
|
+
import { ampless } from './ampless'
|
|
5
|
+
|
|
6
|
+
export const publicAssetUrl = ampless.publicAssetUrl.bind(ampless)
|
|
7
|
+
export const isStorageConfigured = ampless.isStorageConfigured.bind(ampless)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Back-compat shim. Active-theme resolution moved to `@ampless/runtime`.
|
|
2
|
+
// New code should call `ampless.resolveActiveTheme` directly.
|
|
3
|
+
|
|
4
|
+
import { ampless } from './ampless'
|
|
5
|
+
|
|
6
|
+
export const resolveActiveTheme = ampless.resolveActiveTheme.bind(ampless)
|
|
7
|
+
|
|
8
|
+
export type { ResolvedTheme } from '@ampless/runtime'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Back-compat shim. Theme-config loader moved to `@ampless/runtime`.
|
|
2
|
+
// New code should call `ampless.loadThemeConfig` / `ampless.renderThemeCss` directly.
|
|
3
|
+
|
|
4
|
+
import { ampless } from './ampless'
|
|
5
|
+
|
|
6
|
+
export const loadThemeConfig = ampless.loadThemeConfig.bind(ampless)
|
|
7
|
+
|
|
8
|
+
export { renderThemeCss } from '@ampless/runtime'
|
|
9
|
+
|
|
10
|
+
export type { EffectiveThemeConfig } from '@ampless/runtime'
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Back-compat shim. Upload helper moved to `@ampless/admin` (L2
|
|
2
|
+
// extraction). Existing call sites
|
|
3
|
+
// (`uploadProcessedImage(...)`, `sanitizeName(...)`) keep working
|
|
4
|
+
// through this shim.
|
|
5
|
+
|
|
6
|
+
export { uploadProcessedImage, sanitizeName } from '@ampless/admin/components'
|