create-rush-cms 0.0.1 → 0.1.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 +58 -2
- package/dist/cli.js +209 -0
- package/package.json +14 -5
- package/template/.env.example +5 -0
- package/template/README.md +127 -0
- package/template/_gitignore +6 -0
- package/template/astro.config.ts +28 -0
- package/template/package.json +28 -0
- package/template/public/favicon.svg +4 -0
- package/template/src/astro-modules.d.ts +5 -0
- package/template/src/components/language-switcher.astro +39 -0
- package/template/src/components/nav-links.astro +33 -0
- package/template/src/components/site-footer.astro +29 -0
- package/template/src/components/site-header.astro +36 -0
- package/template/src/layouts/base.astro +49 -0
- package/template/src/lib/locale.ts +12 -0
- package/template/src/lib/route-page.ts +15 -0
- package/template/src/pages/404.astro +17 -0
- package/template/src/pages/[...locale]/contact.astro +59 -0
- package/template/src/pages/[...path].astro +32 -0
- package/template/src/pages/robots.txt.ts +10 -0
- package/template/src/pages/sitemap.xml.ts +4 -0
- package/template/src/styles/global.css +278 -0
- package/template/src/views/collection.astro +84 -0
- package/template/src/views/entry.astro +65 -0
- package/template/src/views/home.astro +160 -0
- package/template/src/views/index.ts +41 -0
- package/template/src/views/not-found.astro +21 -0
- package/template/tsconfig.json +9 -0
- package/index.js +0 -3
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Alternate, LocaleCode, NavigationLink } from '@rushcms/client'
|
|
3
|
+
import LanguageSwitcher from './language-switcher.astro'
|
|
4
|
+
import NavLinks from './nav-links.astro'
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
name: string
|
|
8
|
+
home: string
|
|
9
|
+
links: NavigationLink[]
|
|
10
|
+
locale: LocaleCode
|
|
11
|
+
translations: Alternate[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { name, home, links, locale, translations } = Astro.props
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<header class="relative z-30 border-b border-line bg-surface/80 backdrop-blur">
|
|
18
|
+
<div
|
|
19
|
+
class="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-x-8 gap-y-3 px-6 py-4"
|
|
20
|
+
>
|
|
21
|
+
<a href={home} class="flex items-center gap-2 text-lg font-semibold tracking-tight">
|
|
22
|
+
<img src="/favicon.svg" alt="" width="28" height="28" />
|
|
23
|
+
<span>{name}</span>
|
|
24
|
+
</a>
|
|
25
|
+
<div class="flex flex-wrap items-center gap-x-6 gap-y-2">
|
|
26
|
+
{
|
|
27
|
+
links.length > 0 && (
|
|
28
|
+
<nav aria-label="Main" class="site-nav">
|
|
29
|
+
<NavLinks links={links} />
|
|
30
|
+
</nav>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
<LanguageSwitcher locale={locale} translations={translations} />
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</header>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getNavigation, getRush, getUrls } from '@rushcms/astro'
|
|
3
|
+
import PreviewBanner from '@rushcms/astro/components/preview-banner.astro'
|
|
4
|
+
import { optional, toBcp47 } from '@rushcms/client'
|
|
5
|
+
import type { Alternate, LocaleCode } from '@rushcms/client'
|
|
6
|
+
import SiteFooter from '../components/site-footer.astro'
|
|
7
|
+
import SiteHeader from '../components/site-header.astro'
|
|
8
|
+
import '../styles/global.css'
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
locale: LocaleCode
|
|
12
|
+
title?: string | undefined
|
|
13
|
+
translations?: Alternate[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { locale, title, translations = [] } = Astro.props
|
|
17
|
+
const site = await getRush(Astro).site.get()
|
|
18
|
+
const home = getUrls(Astro).homepage(locale)
|
|
19
|
+
// TODO: create a navigation with the key "main" in Rush CMS, or change the key here.
|
|
20
|
+
const main = (await optional(getNavigation(Astro, 'main'))) ?? []
|
|
21
|
+
// TODO: create a navigation with the key "footer" in Rush CMS, or change the key here.
|
|
22
|
+
const footer = (await optional(getNavigation(Astro, 'footer'))) ?? []
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<!doctype html>
|
|
26
|
+
<html lang={toBcp47(locale)}>
|
|
27
|
+
<head>
|
|
28
|
+
<meta charset="utf-8" />
|
|
29
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
30
|
+
<meta name="color-scheme" content="light dark" />
|
|
31
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
32
|
+
{title !== undefined && <title>{title}</title>}
|
|
33
|
+
<slot name="head" />
|
|
34
|
+
</head>
|
|
35
|
+
<body class="flex min-h-screen flex-col bg-canvas font-sans text-ink antialiased">
|
|
36
|
+
<a
|
|
37
|
+
href="#content"
|
|
38
|
+
class="sr-only rounded-md bg-accent px-4 py-2 font-medium text-accent-ink focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50"
|
|
39
|
+
>
|
|
40
|
+
Skip to content
|
|
41
|
+
</a>
|
|
42
|
+
<PreviewBanner />
|
|
43
|
+
<SiteHeader name={site.name} home={home} links={main} locale={locale} translations={translations} />
|
|
44
|
+
<main id="content" tabindex="-1" class="flex-1 focus:outline-none">
|
|
45
|
+
<slot />
|
|
46
|
+
</main>
|
|
47
|
+
<SiteFooter name={site.name} links={footer} />
|
|
48
|
+
</body>
|
|
49
|
+
</html>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { toBcp47 } from '@rushcms/client'
|
|
2
|
+
import type { LocaleCode } from '@rushcms/client'
|
|
3
|
+
|
|
4
|
+
export function languageName(locale: LocaleCode): string {
|
|
5
|
+
const tag = toBcp47(locale)
|
|
6
|
+
const name = new Intl.DisplayNames([tag], { type: 'language' }).of(tag) ?? tag
|
|
7
|
+
return name.charAt(0).toLocaleUpperCase(tag) + name.slice(1)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatDate(value: string, locale: LocaleCode): string {
|
|
11
|
+
return new Intl.DateTimeFormat(toBcp47(locale), { dateStyle: 'long' }).format(new Date(value))
|
|
12
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { resolvePage } from '@rushcms/astro'
|
|
2
|
+
import type { EntryMissing, SitePage, SitePageContext } from '@rushcms/astro'
|
|
3
|
+
import type { APIContext } from 'astro'
|
|
4
|
+
|
|
5
|
+
export type RouteContext = SitePageContext & Pick<APIContext, 'redirect'>
|
|
6
|
+
|
|
7
|
+
export type RoutedPage = SitePage | EntryMissing
|
|
8
|
+
|
|
9
|
+
export async function routePage(context: RouteContext): Promise<RoutedPage | Response> {
|
|
10
|
+
const page = await resolvePage(context)
|
|
11
|
+
if (page.kind === 'moved') {
|
|
12
|
+
return context.redirect(page.location, page.permanent ? 301 : 302)
|
|
13
|
+
}
|
|
14
|
+
return page
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { SitePageProps } from '@rushcms/astro'
|
|
3
|
+
import { routePage } from '../lib/route-page'
|
|
4
|
+
import NotFound from '../views/not-found.astro'
|
|
5
|
+
|
|
6
|
+
export const prerender = false
|
|
7
|
+
|
|
8
|
+
type Props = SitePageProps
|
|
9
|
+
|
|
10
|
+
const page = await routePage(Astro)
|
|
11
|
+
if (page instanceof Response) {
|
|
12
|
+
return page
|
|
13
|
+
}
|
|
14
|
+
Astro.response.status = 404
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<NotFound />
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getRush, getUrls } from '@rushcms/astro'
|
|
3
|
+
import Form from '@rushcms/astro/components/form.astro'
|
|
4
|
+
import { optional } from '@rushcms/client'
|
|
5
|
+
import Base from '../../layouts/base.astro'
|
|
6
|
+
import NotFound from '../../views/not-found.astro'
|
|
7
|
+
|
|
8
|
+
export const prerender = false
|
|
9
|
+
|
|
10
|
+
const locale = getUrls(Astro).localeOf(Astro.params.locale)
|
|
11
|
+
// TODO: create a form with the key "contact" in Rush CMS, or change the key here.
|
|
12
|
+
const form = locale === undefined ? undefined : await optional(getRush(Astro).forms.get('contact'))
|
|
13
|
+
const notice = locale !== undefined && form === undefined && import.meta.env.DEV
|
|
14
|
+
if (form === undefined && !notice) {
|
|
15
|
+
Astro.response.status = 404
|
|
16
|
+
}
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
locale !== undefined && (form !== undefined || notice) ? (
|
|
21
|
+
<Base locale={locale} title={form?.name ?? 'Contact'}>
|
|
22
|
+
<div class="mx-auto max-w-2xl px-6 py-16">
|
|
23
|
+
<h1 class="text-4xl font-semibold tracking-tight">{form?.name ?? 'Contact'}</h1>
|
|
24
|
+
{form ? (
|
|
25
|
+
<div class="contact-form mt-10">
|
|
26
|
+
<Form
|
|
27
|
+
form={form}
|
|
28
|
+
submitLabel="Send"
|
|
29
|
+
messages={{
|
|
30
|
+
sent: 'Thank you, your message was sent.',
|
|
31
|
+
error: 'Your message could not be sent. Please try again.',
|
|
32
|
+
required: 'This field is required.',
|
|
33
|
+
invalid_option: 'Choose one of the options.',
|
|
34
|
+
invalid_email: 'Enter a valid email address.',
|
|
35
|
+
invalid_url: 'Enter a valid URL.',
|
|
36
|
+
invalid_number: 'Enter a number.',
|
|
37
|
+
invalid_format: 'Check the format of this field.',
|
|
38
|
+
too_small: 'Enter {min} or more.',
|
|
39
|
+
too_large: 'Enter {max} or less.',
|
|
40
|
+
too_short: 'Use at least {min} characters.',
|
|
41
|
+
too_long: 'Use at most {max} characters.',
|
|
42
|
+
rate_limited: 'Too many attempts. Try again in a minute.',
|
|
43
|
+
captcha_failed: 'The captcha check failed. Please try again.'
|
|
44
|
+
}}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
) : (
|
|
48
|
+
<p lang="en" class="mt-6 rounded-lg border border-dashed border-line px-5 py-4 text-muted">
|
|
49
|
+
This page shows the Rush CMS form with the key <code class="font-mono">contact</code>.
|
|
50
|
+
Create that form in Rush CMS and reload. This notice appears only in{' '}
|
|
51
|
+
<code class="font-mono">astro dev</code>; production builds answer 404 here.
|
|
52
|
+
</p>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
</Base>
|
|
56
|
+
) : (
|
|
57
|
+
<NotFound />
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getUrls } from '@rushcms/astro'
|
|
3
|
+
import type { SitePageProps } from '@rushcms/astro'
|
|
4
|
+
import { routePage } from '../lib/route-page'
|
|
5
|
+
import Home from '../views/home.astro'
|
|
6
|
+
import { viewsFor } from '../views/index'
|
|
7
|
+
import NotFound from '../views/not-found.astro'
|
|
8
|
+
|
|
9
|
+
export * from '@rushcms/astro/site-paths'
|
|
10
|
+
|
|
11
|
+
type Props = SitePageProps
|
|
12
|
+
|
|
13
|
+
const page = await routePage(Astro)
|
|
14
|
+
if (page instanceof Response) {
|
|
15
|
+
return page
|
|
16
|
+
}
|
|
17
|
+
if (page.kind === 'missing') {
|
|
18
|
+
Astro.response.status = 404
|
|
19
|
+
}
|
|
20
|
+
const collection =
|
|
21
|
+
page.kind === 'collection'
|
|
22
|
+
? page.collection
|
|
23
|
+
: page.kind === 'found'
|
|
24
|
+
? page.entry.collection
|
|
25
|
+
: undefined
|
|
26
|
+
const { list: List, entry: Entry } = viewsFor(collection, getUrls(Astro))
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
{page.kind === 'homepage' && <Home page={page} />}
|
|
30
|
+
{page.kind === 'collection' && <List page={page} />}
|
|
31
|
+
{page.kind === 'found' && <Entry entry={page.entry} locale={page.locale} />}
|
|
32
|
+
{page.kind === 'missing' && <NotFound />}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro'
|
|
2
|
+
import { RUSH_PREVIEW } from 'astro:env/server'
|
|
3
|
+
|
|
4
|
+
export const GET: APIRoute = ({ site }) => {
|
|
5
|
+
const rules = RUSH_PREVIEW ? 'Disallow: /' : 'Allow: /'
|
|
6
|
+
const sitemap = site === undefined ? '' : `\nSitemap: ${new URL('/sitemap.xml', site).href}\n`
|
|
7
|
+
return new Response(`User-agent: *\n${rules}\n${sitemap}`, {
|
|
8
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
|
|
9
|
+
})
|
|
10
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
@plugin '@tailwindcss/typography';
|
|
3
|
+
|
|
4
|
+
@theme {
|
|
5
|
+
--font-sans:
|
|
6
|
+
ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
|
7
|
+
sans-serif;
|
|
8
|
+
--font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
9
|
+
--color-canvas: oklch(0.985 0.004 265);
|
|
10
|
+
--color-surface: oklch(1 0 0);
|
|
11
|
+
--color-ink: oklch(0.23 0.025 265);
|
|
12
|
+
--color-muted: oklch(0.5 0.025 265);
|
|
13
|
+
--color-line: oklch(0.91 0.012 265);
|
|
14
|
+
--color-accent: oklch(0.52 0.21 277);
|
|
15
|
+
--color-accent-ink: oklch(0.99 0.005 277);
|
|
16
|
+
--color-accent-soft: oklch(0.93 0.045 277);
|
|
17
|
+
--color-danger: oklch(0.55 0.2 25);
|
|
18
|
+
--color-success: oklch(0.55 0.14 155);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@media (prefers-color-scheme: dark) {
|
|
22
|
+
:root {
|
|
23
|
+
--color-canvas: oklch(0.17 0.018 265);
|
|
24
|
+
--color-surface: oklch(0.21 0.02 265);
|
|
25
|
+
--color-ink: oklch(0.95 0.008 265);
|
|
26
|
+
--color-muted: oklch(0.74 0.02 265);
|
|
27
|
+
--color-line: oklch(0.31 0.022 265);
|
|
28
|
+
--color-accent: oklch(0.76 0.14 277);
|
|
29
|
+
--color-accent-ink: oklch(0.18 0.03 277);
|
|
30
|
+
--color-accent-soft: oklch(0.3 0.07 277);
|
|
31
|
+
--color-danger: oklch(0.72 0.17 25);
|
|
32
|
+
--color-success: oklch(0.75 0.14 155);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@layer base {
|
|
37
|
+
:root {
|
|
38
|
+
color-scheme: light dark;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
:focus-visible {
|
|
42
|
+
outline: 2px solid var(--color-accent);
|
|
43
|
+
outline-offset: 3px;
|
|
44
|
+
border-radius: 4px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
::selection {
|
|
48
|
+
background-color: var(--color-accent-soft);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@layer components {
|
|
53
|
+
.button,
|
|
54
|
+
.contact-form button[type='submit'] {
|
|
55
|
+
display: inline-flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
gap: 0.5rem;
|
|
59
|
+
border-radius: 9999px;
|
|
60
|
+
padding: 0.625rem 1.25rem;
|
|
61
|
+
font-weight: 500;
|
|
62
|
+
transition:
|
|
63
|
+
background-color 150ms,
|
|
64
|
+
border-color 150ms,
|
|
65
|
+
color 150ms;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.button-primary,
|
|
69
|
+
.contact-form button[type='submit'] {
|
|
70
|
+
background-color: var(--color-accent);
|
|
71
|
+
color: var(--color-accent-ink);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.button-primary:hover,
|
|
75
|
+
.contact-form button[type='submit']:hover {
|
|
76
|
+
background-color: color-mix(in oklch, var(--color-accent) 88%, var(--color-ink));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.button-secondary {
|
|
80
|
+
border: 1px solid var(--color-line);
|
|
81
|
+
background-color: var(--color-surface);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.button-secondary:hover {
|
|
85
|
+
border-color: var(--color-accent);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.card {
|
|
89
|
+
border: 1px solid var(--color-line);
|
|
90
|
+
border-radius: 1rem;
|
|
91
|
+
background-color: var(--color-surface);
|
|
92
|
+
padding: 1.5rem;
|
|
93
|
+
transition:
|
|
94
|
+
border-color 150ms,
|
|
95
|
+
box-shadow 150ms;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.card:hover,
|
|
99
|
+
.card:focus-within {
|
|
100
|
+
border-color: var(--color-accent);
|
|
101
|
+
box-shadow: 0 8px 30px -12px color-mix(in oklch, var(--color-accent) 40%, transparent);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.nav-links-top {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-wrap: wrap;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: 0.25rem 1.25rem;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.nav-item {
|
|
112
|
+
position: relative;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.nav-link,
|
|
116
|
+
.nav-label {
|
|
117
|
+
display: inline-block;
|
|
118
|
+
padding-block: 0.25rem;
|
|
119
|
+
color: var(--color-muted);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.nav-link:hover,
|
|
123
|
+
.nav-link[aria-current] {
|
|
124
|
+
color: var(--color-ink);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.site-nav .nav-links-nested {
|
|
128
|
+
position: absolute;
|
|
129
|
+
top: 100%;
|
|
130
|
+
left: -1rem;
|
|
131
|
+
z-index: 20;
|
|
132
|
+
display: none;
|
|
133
|
+
min-width: 12rem;
|
|
134
|
+
border: 1px solid var(--color-line);
|
|
135
|
+
border-radius: 0.75rem;
|
|
136
|
+
background-color: var(--color-surface);
|
|
137
|
+
padding: 0.5rem 1rem;
|
|
138
|
+
box-shadow: 0 12px 30px -12px rgb(0 0 0 / 0.25);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.site-nav .nav-item:hover > .nav-links-nested,
|
|
142
|
+
.site-nav .nav-item:focus-within > .nav-links-nested {
|
|
143
|
+
display: block;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.footer-nav .nav-links-top {
|
|
147
|
+
align-items: flex-start;
|
|
148
|
+
gap: 1rem 2.5rem;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.footer-nav .nav-links-nested {
|
|
152
|
+
margin-top: 0.25rem;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
[data-rush-preview] {
|
|
156
|
+
position: sticky;
|
|
157
|
+
top: 0;
|
|
158
|
+
z-index: 40;
|
|
159
|
+
display: flex;
|
|
160
|
+
flex-wrap: wrap;
|
|
161
|
+
align-items: center;
|
|
162
|
+
justify-content: center;
|
|
163
|
+
gap: 0.5rem 1rem;
|
|
164
|
+
background-color: var(--color-ink);
|
|
165
|
+
padding: 0.5rem 1rem;
|
|
166
|
+
color: var(--color-canvas);
|
|
167
|
+
font-size: 0.875rem;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
[data-rush-preview] a {
|
|
171
|
+
font-weight: 600;
|
|
172
|
+
text-decoration: underline;
|
|
173
|
+
text-underline-offset: 3px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.contact-form form {
|
|
177
|
+
display: grid;
|
|
178
|
+
gap: 1.5rem;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.contact-form label,
|
|
182
|
+
.contact-form legend {
|
|
183
|
+
display: block;
|
|
184
|
+
margin-bottom: 0.375rem;
|
|
185
|
+
font-weight: 500;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.contact-form fieldset label {
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
gap: 0.5rem;
|
|
192
|
+
font-weight: 400;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.contact-form input:not([type='checkbox'], [type='radio']),
|
|
196
|
+
.contact-form textarea,
|
|
197
|
+
.contact-form select {
|
|
198
|
+
width: 100%;
|
|
199
|
+
border: 1px solid var(--color-line);
|
|
200
|
+
border-radius: 0.5rem;
|
|
201
|
+
background-color: var(--color-surface);
|
|
202
|
+
padding: 0.625rem 0.75rem;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.contact-form textarea {
|
|
206
|
+
min-height: 9rem;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.contact-form input:focus-visible,
|
|
210
|
+
.contact-form textarea:focus-visible,
|
|
211
|
+
.contact-form select:focus-visible {
|
|
212
|
+
outline-offset: 0;
|
|
213
|
+
border-color: var(--color-accent);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.contact-form input[type='checkbox'],
|
|
217
|
+
.contact-form input[type='radio'] {
|
|
218
|
+
accent-color: var(--color-accent);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.contact-form [id$='-help'] {
|
|
222
|
+
margin-top: 0.375rem;
|
|
223
|
+
color: var(--color-muted);
|
|
224
|
+
font-size: 0.875rem;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.contact-form [data-rush-error-for] {
|
|
228
|
+
margin-top: 0.375rem;
|
|
229
|
+
color: var(--color-danger);
|
|
230
|
+
font-size: 0.875rem;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.contact-form [data-rush-status] {
|
|
234
|
+
border: 1px solid var(--color-line);
|
|
235
|
+
border-radius: 0.5rem;
|
|
236
|
+
padding: 0.75rem 1rem;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.contact-form [data-rush-status][data-state='sent'] {
|
|
240
|
+
border-color: var(--color-success);
|
|
241
|
+
color: var(--color-success);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.contact-form [data-rush-status][data-state='error'] {
|
|
245
|
+
border-color: var(--color-danger);
|
|
246
|
+
color: var(--color-danger);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.contact-form button[type='submit'] {
|
|
250
|
+
justify-self: start;
|
|
251
|
+
cursor: pointer;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.contact-form button[type='submit']:disabled {
|
|
255
|
+
cursor: progress;
|
|
256
|
+
opacity: 0.7;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.prose {
|
|
261
|
+
--tw-prose-body: var(--color-ink);
|
|
262
|
+
--tw-prose-headings: var(--color-ink);
|
|
263
|
+
--tw-prose-lead: var(--color-muted);
|
|
264
|
+
--tw-prose-links: var(--color-accent);
|
|
265
|
+
--tw-prose-bold: var(--color-ink);
|
|
266
|
+
--tw-prose-counters: var(--color-muted);
|
|
267
|
+
--tw-prose-bullets: var(--color-line);
|
|
268
|
+
--tw-prose-hr: var(--color-line);
|
|
269
|
+
--tw-prose-quotes: var(--color-ink);
|
|
270
|
+
--tw-prose-quote-borders: var(--color-accent);
|
|
271
|
+
--tw-prose-captions: var(--color-muted);
|
|
272
|
+
--tw-prose-kbd: var(--color-ink);
|
|
273
|
+
--tw-prose-code: var(--color-ink);
|
|
274
|
+
--tw-prose-pre-code: var(--color-ink);
|
|
275
|
+
--tw-prose-pre-bg: var(--color-surface);
|
|
276
|
+
--tw-prose-th-borders: var(--color-line);
|
|
277
|
+
--tw-prose-td-borders: var(--color-line);
|
|
278
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { collectionAlternates, getRush, getUrls } from '@rushcms/astro'
|
|
3
|
+
import RushImage from '@rushcms/astro/components/rush-image.astro'
|
|
4
|
+
import Base from '../layouts/base.astro'
|
|
5
|
+
import { formatDate } from '../lib/locale'
|
|
6
|
+
import type { ListViewProps } from './index'
|
|
7
|
+
|
|
8
|
+
type Props = ListViewProps
|
|
9
|
+
|
|
10
|
+
const { page } = Astro.props
|
|
11
|
+
const { collection, locale, entries, lastPage } = page
|
|
12
|
+
const urls = getUrls(Astro)
|
|
13
|
+
const site = await getRush(Astro).site.get()
|
|
14
|
+
const previous = page.page > 1 ? urls.collection(collection, locale, page.page - 1) : undefined
|
|
15
|
+
const next = page.page < lastPage ? urls.collection(collection, locale, page.page + 1) : undefined
|
|
16
|
+
const canonical = Astro.site && new URL(urls.collection(collection, locale, page.page), Astro.site)
|
|
17
|
+
const heading = page.page > 1 ? `${collection.name} (${page.page}/${lastPage})` : collection.name
|
|
18
|
+
const translations = collectionAlternates(collection.id, urls)
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<Base locale={locale} title={`${heading} | ${site.name}`} translations={translations}>
|
|
22
|
+
{canonical && <link slot="head" rel="canonical" href={canonical.href} />}
|
|
23
|
+
<div class="mx-auto max-w-6xl px-6 py-16">
|
|
24
|
+
<header class="max-w-3xl">
|
|
25
|
+
<h1 class="text-4xl font-semibold tracking-tight">{collection.name}</h1>
|
|
26
|
+
{
|
|
27
|
+
lastPage > 1 && (
|
|
28
|
+
<p class="mt-2 text-muted">
|
|
29
|
+
Page {page.page} of {lastPage}
|
|
30
|
+
</p>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
</header>
|
|
34
|
+
{
|
|
35
|
+
entries.items.length > 0 ? (
|
|
36
|
+
<ul class="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
37
|
+
{entries.items.map((entry) => (
|
|
38
|
+
<li>
|
|
39
|
+
<article class="card group relative flex h-full flex-col">
|
|
40
|
+
{entry.featured_image?.url && (
|
|
41
|
+
<RushImage
|
|
42
|
+
media={entry.featured_image}
|
|
43
|
+
class="mb-4 aspect-video w-full rounded-lg object-cover"
|
|
44
|
+
/>
|
|
45
|
+
)}
|
|
46
|
+
<h2 class="text-lg font-medium group-hover:text-accent">
|
|
47
|
+
<a href={urls.servedPath(entry, locale)} class="after:absolute after:inset-0">
|
|
48
|
+
{entry.title}
|
|
49
|
+
</a>
|
|
50
|
+
</h2>
|
|
51
|
+
{entry.excerpt && <p class="mt-2 line-clamp-3 text-muted">{entry.excerpt}</p>}
|
|
52
|
+
<p class="mt-auto pt-4 text-sm text-muted">
|
|
53
|
+
<time datetime={entry.published_at}>
|
|
54
|
+
{formatDate(entry.published_at, locale)}
|
|
55
|
+
</time>
|
|
56
|
+
</p>
|
|
57
|
+
</article>
|
|
58
|
+
</li>
|
|
59
|
+
))}
|
|
60
|
+
</ul>
|
|
61
|
+
) : (
|
|
62
|
+
<p class="mt-12 text-muted">Nothing has been published here yet.</p>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
{
|
|
66
|
+
lastPage > 1 && (
|
|
67
|
+
<nav aria-label="Pagination" class="mt-12 flex items-center justify-between gap-4">
|
|
68
|
+
{previous ? (
|
|
69
|
+
<a href={previous} rel="prev" class="button button-secondary">
|
|
70
|
+
Previous page
|
|
71
|
+
</a>
|
|
72
|
+
) : (
|
|
73
|
+
<span />
|
|
74
|
+
)}
|
|
75
|
+
{next && (
|
|
76
|
+
<a href={next} rel="next" class="button button-secondary">
|
|
77
|
+
Next page
|
|
78
|
+
</a>
|
|
79
|
+
)}
|
|
80
|
+
</nav>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
</div>
|
|
84
|
+
</Base>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getUrls } from '@rushcms/astro'
|
|
3
|
+
import RichText from '@rushcms/astro/components/rich-text.astro'
|
|
4
|
+
import RushImage from '@rushcms/astro/components/rush-image.astro'
|
|
5
|
+
import Seo from '@rushcms/astro/components/seo.astro'
|
|
6
|
+
import { alternates, isRichText, toBcp47 } from '@rushcms/client'
|
|
7
|
+
import Base from '../layouts/base.astro'
|
|
8
|
+
import { formatDate, languageName } from '../lib/locale'
|
|
9
|
+
import type { EntryViewProps } from './index'
|
|
10
|
+
|
|
11
|
+
type Props = EntryViewProps
|
|
12
|
+
|
|
13
|
+
const { entry, locale } = Astro.props
|
|
14
|
+
const urls = getUrls(Astro)
|
|
15
|
+
const list = urls.collectionRef(entry.collection.id, locale)
|
|
16
|
+
const documents = Object.values(entry.data).filter(isRichText)
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<Base locale={locale} translations={alternates(entry, urls)}>
|
|
20
|
+
<Seo slot="head" entry={entry} />
|
|
21
|
+
<article lang={toBcp47(entry.locale)} class="mx-auto max-w-3xl px-6 py-16">
|
|
22
|
+
<header>
|
|
23
|
+
{
|
|
24
|
+
list && (
|
|
25
|
+
<p class="text-sm">
|
|
26
|
+
<a href={urls.collection(list, locale)} class="text-muted hover:text-accent">
|
|
27
|
+
← Back to the list
|
|
28
|
+
</a>
|
|
29
|
+
</p>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
<h1 class="mt-6 text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
|
|
33
|
+
{entry.title}
|
|
34
|
+
</h1>
|
|
35
|
+
<p class="mt-4 text-sm text-muted">
|
|
36
|
+
<time datetime={entry.published_at}>{formatDate(entry.published_at, entry.locale)}</time>
|
|
37
|
+
{entry.author && <span> · {entry.author.name}</span>}
|
|
38
|
+
</p>
|
|
39
|
+
{
|
|
40
|
+
entry.fallback && (
|
|
41
|
+
<p lang="en" class="mt-6 rounded-lg border border-line bg-surface px-4 py-3 text-sm">
|
|
42
|
+
Not translated yet: shown in {languageName(entry.locale)}.
|
|
43
|
+
</p>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
{entry.excerpt && <p class="mt-8 text-xl text-pretty text-muted">{entry.excerpt}</p>}
|
|
47
|
+
</header>
|
|
48
|
+
{
|
|
49
|
+
entry.featured_image?.url && (
|
|
50
|
+
<figure class="mt-10">
|
|
51
|
+
<RushImage media={entry.featured_image} class="w-full rounded-xl" loading="eager" />
|
|
52
|
+
</figure>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
{
|
|
56
|
+
documents.length > 0 && (
|
|
57
|
+
<div class="prose prose-lg mt-10 max-w-none">
|
|
58
|
+
{documents.map((content) => (
|
|
59
|
+
<RichText content={content} media={entry.media} />
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
</article>
|
|
65
|
+
</Base>
|