duto 0.260915.0 → 0.261011.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/README.md +1 -1
- package/package.json +1 -1
- package/src/commands/new.ts +55 -30
- package/src/commands/templates/pages/README.md +21 -0
- package/src/commands/templates/pages/_gitignore +24 -0
- package/src/commands/templates/pages/components/html.tsx +55 -0
- package/src/commands/templates/pages/components/locale-select.tsx +59 -0
- package/src/commands/templates/pages/components/select.tsx +95 -0
- package/src/commands/templates/pages/components/theme-select.tsx +60 -0
- package/src/commands/templates/pages/i18n/en.ts +23 -0
- package/src/commands/templates/pages/i18n/index.ts +19 -0
- package/src/commands/templates/pages/i18n/pt-br.ts +23 -0
- package/src/commands/templates/pages/package.json +22 -0
- package/src/commands/templates/pages/pages/404.tsx +12 -0
- package/src/commands/templates/pages/pages/blog/[slug].tsx +88 -0
- package/src/commands/templates/pages/pages/blog/index.tsx +74 -0
- package/src/commands/templates/pages/pages/index.tsx +26 -0
- package/src/commands/templates/pages/pages/layout.tsx +26 -0
- package/src/commands/templates/pages/public/favicon.ico +0 -0
- package/src/commands/templates/pages/public/favicon.svg +1 -0
- package/src/commands/templates/pages/style.css +127 -0
- package/src/commands/templates/pages/tsconfig.json +56 -0
- package/src/commands/templates/pages/types.ts +8 -0
- package/src/commands/templates/server/base/.editorconfig +21 -0
- package/src/commands/templates/{base → server/base}/README.md +1 -1
- package/src/pages/cli/commands/dev.ts +10 -4
- package/src/pages/cli/utils.ts +7 -23
- package/src/pages/client/app.tsx +193 -91
- package/src/pages/client/link.ts +15 -41
- package/src/pages/client/storage.ts +1 -1
- package/src/pages/client/theme.ts +49 -0
- package/src/pages/compiler/aliases.ts +2 -8
- package/src/pages/compiler/ast.ts +1 -538
- package/src/pages/compiler/constants.ts +97 -13
- package/src/pages/compiler/index.ts +55 -140
- package/src/pages/compiler/meta.ts +40 -253
- package/src/pages/compiler/routes.ts +4 -5
- package/src/pages/compiler/types.ts +16 -125
- package/src/pages/compiler/vite-plugin.ts +64 -27
- package/src/pages/components/link.tsx +11 -19
- package/src/pages/i18n/client.ts +21 -10
- package/src/pages/i18n/server.ts +21 -6
- package/src/pages/index.ts +4 -22
- package/src/server/app.ts +2 -1
- package/src/server/routes.ts +4 -26
- package/src/utils.ts +34 -3
- package/src/pages/client/router.ts +0 -329
- package/src/pages/compiler/island.ts +0 -108
- /package/src/commands/templates/{base → pages}/.editorconfig +0 -0
- /package/src/commands/templates/{base → server/base}/.env.dev.example +0 -0
- /package/src/commands/templates/{base → server/base}/.env.prod.example +0 -0
- /package/src/commands/templates/{base → server/base}/_gitignore +0 -0
- /package/src/commands/templates/{base → server/base}/actions/blog/index.get.ts +0 -0
- /package/src/commands/templates/{base → server/base}/actions/blog/post/[id].get.ts +0 -0
- /package/src/commands/templates/{base → server/base}/actions/index.get.ts +0 -0
- /package/src/commands/templates/{base → server/base}/configs/duto.ts +0 -0
- /package/src/commands/templates/{base → server/base}/configs/roles.ts +0 -0
- /package/src/commands/templates/{base → server/base}/package.json +0 -0
- /package/src/commands/templates/{base → server/base}/tsconfig.json +0 -0
- /package/src/commands/templates/{workerd → server/workerd}/wrangler.toml +0 -0
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<h1 align="left">
|
|
2
|
-
|
|
2
|
+
⛳️ Duto
|
|
3
3
|
<a href="https://npmjs.com/package/duto"><img src="https://img.shields.io/npm/v/duto.svg" alt="npm package"></a>
|
|
4
4
|
<br/>
|
|
5
5
|
<a href="https://pr.new/attla/duto"><img align="right" src="https://developer.stackblitz.com/img/start_pr_dark_small.svg" alt="Start new PR in StackBlitz Codeflow"></a>
|
package/package.json
CHANGED
package/src/commands/new.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { basename, join,
|
|
1
|
+
import { basename, join, resolve } from 'pathe'
|
|
2
2
|
import { command } from 't0n/cli'
|
|
3
|
-
import { bold, dim,
|
|
4
|
-
import { event,
|
|
3
|
+
import { bold, dim, red } from 't0n/color'
|
|
4
|
+
import { event, step, rn } from 't0n/log'
|
|
5
5
|
import { _root } from '@/utils'
|
|
6
6
|
|
|
7
7
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
@@ -11,6 +11,7 @@ import select from '@inquirer/select'
|
|
|
11
11
|
import { createSpinner } from 'nanospinner'
|
|
12
12
|
import { exit } from 'node:process'
|
|
13
13
|
|
|
14
|
+
const types = ['pages', 'server']
|
|
14
15
|
const templates = [
|
|
15
16
|
'aws',
|
|
16
17
|
'workerd',
|
|
@@ -63,6 +64,11 @@ export default command({
|
|
|
63
64
|
type: 'boolean',
|
|
64
65
|
default: false,
|
|
65
66
|
},
|
|
67
|
+
type: {
|
|
68
|
+
description: 'Project type',
|
|
69
|
+
type: 'enum',
|
|
70
|
+
options: types,
|
|
71
|
+
},
|
|
66
72
|
template: {
|
|
67
73
|
alias: 't',
|
|
68
74
|
description: 'Template to use',
|
|
@@ -71,61 +77,80 @@ export default command({
|
|
|
71
77
|
},
|
|
72
78
|
},
|
|
73
79
|
async run({ args }) { // @ts-ignore
|
|
74
|
-
const root = process.cwd() || process.env?.PWD || process.stdin?.path
|
|
75
80
|
let dir = args._[0]
|
|
76
81
|
if (dir) {
|
|
77
82
|
step(bold('Using target directory: '+ dim('/'+ dir.replace(/^\//, ''))))
|
|
78
83
|
} else {
|
|
79
|
-
dir = await input({ message: 'Target directory', default:
|
|
80
|
-
if (!dir.startsWith(
|
|
81
|
-
dir = join(
|
|
84
|
+
dir = await input({ message: 'Target directory', default: _root })
|
|
85
|
+
if (!dir.startsWith(_root))
|
|
86
|
+
dir = join(_root, dir)
|
|
82
87
|
}
|
|
83
88
|
|
|
84
|
-
const
|
|
85
|
-
const template = args.template || (await select({
|
|
89
|
+
const type = args.type || (await select({
|
|
86
90
|
loop: true,
|
|
87
|
-
message: 'Which
|
|
88
|
-
choices:
|
|
89
|
-
default: 'workerd',
|
|
91
|
+
message: 'Which type of project is it?',
|
|
92
|
+
choices: types.map(v => ({ title: v, value: v})),
|
|
90
93
|
}))
|
|
91
94
|
|
|
92
|
-
if (!
|
|
93
|
-
throw new Error('No
|
|
95
|
+
if (!type)
|
|
96
|
+
throw new Error('No type selected')
|
|
97
|
+
if (!types.includes(type))
|
|
98
|
+
throw new Error(`Invalid type selected: ${type}`)
|
|
99
|
+
|
|
100
|
+
const dirs = [type === 'server' ? 'base' : '']
|
|
101
|
+
const name = basename(/^(\.\/|\.\\|\.)$/.test(dir) ? _root : dir)
|
|
102
|
+
|
|
103
|
+
if (type === 'server') {
|
|
104
|
+
const template = args.template || (await select({
|
|
105
|
+
loop: true,
|
|
106
|
+
message: 'Which template do you want to use?',
|
|
107
|
+
choices: templates.map(v => ({ title: v, value: v})),
|
|
108
|
+
default: 'workerd',
|
|
109
|
+
}))
|
|
94
110
|
|
|
95
|
-
|
|
96
|
-
|
|
111
|
+
if (!template)
|
|
112
|
+
throw new Error('No template selected')
|
|
113
|
+
if (!templates.includes(template))
|
|
114
|
+
throw new Error(`Invalid template selected: ${template}`)
|
|
97
115
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
dirs.push(template)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
existsSync(dir) && readdirSync(dir).length > 0
|
|
121
|
+
&& !(await confirm({ message: 'Directory not empty. Continue?', default: false }))
|
|
122
|
+
) {
|
|
123
|
+
process.exit(1)
|
|
101
124
|
} else {
|
|
102
125
|
mkdirp(dir)
|
|
103
126
|
}
|
|
104
127
|
|
|
105
|
-
const spinner = createSpinner('Cloning
|
|
106
|
-
for (const
|
|
107
|
-
const templateDir = resolve(import.meta.dir,
|
|
128
|
+
const spinner = createSpinner('Cloning project..').start()
|
|
129
|
+
for (const d of dirs) {
|
|
130
|
+
const templateDir = resolve(import.meta.dir, `./templates/${type}/`+ d)
|
|
108
131
|
const files = readdirSync(templateDir)
|
|
109
|
-
for (const file of files
|
|
132
|
+
for (const file of files)
|
|
110
133
|
copy(join(templateDir, file), join(dir, renameFiles[file] ?? file))
|
|
111
134
|
}
|
|
112
135
|
|
|
113
|
-
spinner.success()
|
|
136
|
+
spinner.success(bold('Cloned project'))
|
|
114
137
|
|
|
115
|
-
const pkgPath =
|
|
138
|
+
const pkgPath = join(dir, 'package.json')
|
|
116
139
|
if (existsSync(pkgPath)) {
|
|
117
140
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
118
141
|
pkg.name = name
|
|
119
142
|
writeFileSync(join(dir, 'package.json'), JSON.stringify(pkg, null, 2))
|
|
120
143
|
}
|
|
121
144
|
|
|
122
|
-
|
|
145
|
+
|
|
146
|
+
const install = args.install || (await confirm({ message: 'Install dependencies?', default: false }))
|
|
147
|
+
if (install) {
|
|
123
148
|
const spinner2 = createSpinner('Installing project dependencies').start()
|
|
124
149
|
const proc = Bun.spawn(['bun', 'i'], { stdout: 'ignore', stderr: 'inherit' })
|
|
125
150
|
const procExit = await proc.exited
|
|
126
151
|
|
|
127
152
|
if (procExit === 0) {
|
|
128
|
-
spinner2.success()
|
|
153
|
+
spinner2.success(bold('Installed dependencies'))
|
|
129
154
|
} else {
|
|
130
155
|
spinner2.stop({
|
|
131
156
|
mark: red('×'),
|
|
@@ -135,12 +160,12 @@ export default command({
|
|
|
135
160
|
}
|
|
136
161
|
}
|
|
137
162
|
|
|
138
|
-
event('
|
|
139
|
-
if (dir !=
|
|
163
|
+
event(bold('Project successfully created'))
|
|
164
|
+
if (dir != _root) {
|
|
140
165
|
rn()
|
|
141
166
|
console.log(
|
|
142
167
|
dim('Get started with:'),
|
|
143
|
-
bold(`cd ${name}`),
|
|
168
|
+
bold(`cd ${name} && duto dev`),
|
|
144
169
|
)
|
|
145
170
|
}
|
|
146
171
|
},
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Duto Pages - Starter Template
|
|
2
|
+
|
|
3
|
+
This template should help get you started developing.
|
|
4
|
+
|
|
5
|
+
## Install dependencies
|
|
6
|
+
|
|
7
|
+
To install, run:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun i
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Development environment
|
|
14
|
+
|
|
15
|
+
Test the application locally in an environment identical to production using the command:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
duto dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
[Read the Full Docs to Learn More](https://hub.bi/docs/duto)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
node_modules
|
|
11
|
+
dist
|
|
12
|
+
dist-ssr
|
|
13
|
+
*.local
|
|
14
|
+
|
|
15
|
+
# Editor directories and files
|
|
16
|
+
.vscode/*
|
|
17
|
+
!.vscode/extensions.json
|
|
18
|
+
.idea
|
|
19
|
+
.DS_Store
|
|
20
|
+
*.suo
|
|
21
|
+
*.ntvs*
|
|
22
|
+
*.njsproj
|
|
23
|
+
*.sln
|
|
24
|
+
*.sw?
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { t } from 'duto'
|
|
2
|
+
|
|
3
|
+
export type Props = {
|
|
4
|
+
children?: preact.ComponentChildren
|
|
5
|
+
class?: string
|
|
6
|
+
|
|
7
|
+
locale?: string
|
|
8
|
+
name?: string
|
|
9
|
+
title: string
|
|
10
|
+
description?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Html({
|
|
14
|
+
children,
|
|
15
|
+
class: className,
|
|
16
|
+
|
|
17
|
+
locale = 'en',
|
|
18
|
+
name = 'Duto',
|
|
19
|
+
title,
|
|
20
|
+
description = '',
|
|
21
|
+
...props
|
|
22
|
+
}: Props) {
|
|
23
|
+
const _class = [
|
|
24
|
+
'bg-gradient-to-br from-zinc-200 to-zinc-150 dark:from-zinc-800 dark:to-zinc-950 text-gray-600 dark:text-gray-300',
|
|
25
|
+
className,
|
|
26
|
+
].filter(Boolean).join(' ')
|
|
27
|
+
|
|
28
|
+
const _title = [name, t(title ?? '')].filter(Boolean).join(' › ')
|
|
29
|
+
const _description = t(description || title || '')
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<html class={_class} {...props}>
|
|
33
|
+
<head>
|
|
34
|
+
<meta charset="utf-8" />
|
|
35
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
36
|
+
|
|
37
|
+
<link rel="icon" type="image/x-icon" sizes="48x48" href="/favicon.ico" />
|
|
38
|
+
<link rel="icon" type="image/svg+xml" sizes="any" href="/favicon.svg" />
|
|
39
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
40
|
+
|
|
41
|
+
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)" />
|
|
42
|
+
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)" />
|
|
43
|
+
|
|
44
|
+
{/* <link rel="sitemap" href="/sitemap-index.xml" /> */}
|
|
45
|
+
|
|
46
|
+
<title dangerouslySetInnerHTML={{ __html: _title }} />
|
|
47
|
+
{_description && <meta name="description" content={_description} />}
|
|
48
|
+
</head>
|
|
49
|
+
|
|
50
|
+
<body class="flex flex-col min-h-svh max-w-screen overflow-x-clip relative z-10 ">
|
|
51
|
+
{children}
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useSignal } from '@preact/signals'
|
|
2
|
+
import { options } from '@/i18n'
|
|
3
|
+
import { local, getLocale, setLocale, isPrefixed, r } from 'duto'
|
|
4
|
+
import { useLayoutEffect } from 'preact/hooks'
|
|
5
|
+
|
|
6
|
+
import Select from './select'
|
|
7
|
+
|
|
8
|
+
function Languages(props: any) {
|
|
9
|
+
return (
|
|
10
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={[props.class || '', props.className || ''].filter(Boolean).join(' ')}><path d="m5 8 6 6" /><path d="m4 14 6-6 2-3" /><path d="M2 5h12" /><path d="M7 2h1" /><path d="m22 22-5-10-5 10" /><path d="M14 18h6" /></svg>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
export default function LocaleSelect({ class: className = '' }) {
|
|
14
|
+
const localeOptions = Object.entries(options).map(([value, label]) => ({ label, value }))
|
|
15
|
+
const selectedValue = useSignal(getLocale())
|
|
16
|
+
let unsubscribe: (() => void) | null = null
|
|
17
|
+
const handle = (e: any, lang?: string) => {
|
|
18
|
+
lang ??= getLocale()
|
|
19
|
+
selectedValue.value = lang
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
unsubscribe = local.subscribe('lang', e => !isPrefixed() && handle(null, e.val as string))
|
|
24
|
+
const lang = getLocale()
|
|
25
|
+
selectedValue.value = lang
|
|
26
|
+
|
|
27
|
+
window.addEventListener('popstate', handle)
|
|
28
|
+
return () => {
|
|
29
|
+
window.removeEventListener('popstate', handle)
|
|
30
|
+
unsubscribe?.()
|
|
31
|
+
}
|
|
32
|
+
}, [])
|
|
33
|
+
|
|
34
|
+
const handleChange = async (e: Event) => {
|
|
35
|
+
const target = e.target as HTMLSelectElement
|
|
36
|
+
if (target) {
|
|
37
|
+
await setLocale(target.value)
|
|
38
|
+
selectedValue.value = target.value
|
|
39
|
+
isPrefixed() && r((window?.location?.pathname || '').split('/').filter(Boolean).slice(1).join('/'), target.value)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const containerClasses = [
|
|
44
|
+
'locale-select overflow-hidden text-ellipsis p-1 rounded-lg hover:bg-neutral-200 hover:text-zinc-900',
|
|
45
|
+
className
|
|
46
|
+
].filter(Boolean).join(' ')
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Select
|
|
50
|
+
Icon={Languages}
|
|
51
|
+
options={localeOptions}
|
|
52
|
+
value={selectedValue.value}
|
|
53
|
+
class={'outline-none text-ellipsis absolute inset-0 opacity-0 cursor-pointer' + className}
|
|
54
|
+
containerClass={containerClasses}
|
|
55
|
+
id="locale-select"
|
|
56
|
+
onchange={handleChange}
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { forwardRef } from 'preact/compat'
|
|
2
|
+
|
|
3
|
+
type OptionObject = {
|
|
4
|
+
label: string
|
|
5
|
+
value: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type Option =
|
|
9
|
+
| OptionObject
|
|
10
|
+
| [value: string, label: string]
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
placeholder?: string,
|
|
14
|
+
value?: string,
|
|
15
|
+
Icon?: any,
|
|
16
|
+
// icon?: string,
|
|
17
|
+
id?: string,
|
|
18
|
+
class?: string,
|
|
19
|
+
containerClass?: string,
|
|
20
|
+
options: Record<string, Option[]> | Option[],
|
|
21
|
+
onchange?: (e: Event) => void,
|
|
22
|
+
onclick?: (e: Event) => void,
|
|
23
|
+
required?: boolean,
|
|
24
|
+
ref?: (el: HTMLSelectElement | null) => void,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderOption(op: Option, value: string) {
|
|
28
|
+
if (Array.isArray(op)) {
|
|
29
|
+
const [val, label] = op
|
|
30
|
+
return <option key={val} value={val}>{label}</option>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return <option key={op.value} value={op.value}>{op.label}</option>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default forwardRef<HTMLSelectElement, Props>(function Select({
|
|
37
|
+
placeholder,
|
|
38
|
+
value = '',
|
|
39
|
+
Icon,
|
|
40
|
+
id,
|
|
41
|
+
class: className,
|
|
42
|
+
containerClass,
|
|
43
|
+
options,
|
|
44
|
+
...props
|
|
45
|
+
}, ref) {
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<label
|
|
49
|
+
htmlFor={id}
|
|
50
|
+
class={[
|
|
51
|
+
'relative inline-flex items-center cursor-pointer select',
|
|
52
|
+
containerClass,
|
|
53
|
+
].filter(Boolean).join(' ')}
|
|
54
|
+
>
|
|
55
|
+
{Icon && <Icon class="size-6 shrink-0" />}
|
|
56
|
+
{/* {icon && (
|
|
57
|
+
<Icon
|
|
58
|
+
name={icon}
|
|
59
|
+
class="icon label-icon pointer-events-none"
|
|
60
|
+
/>
|
|
61
|
+
)} */}
|
|
62
|
+
|
|
63
|
+
<select
|
|
64
|
+
ref={ref}
|
|
65
|
+
id={id}
|
|
66
|
+
value={value}
|
|
67
|
+
autocomplete="off"
|
|
68
|
+
class={className}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{placeholder && (
|
|
72
|
+
<option disabled value="">{placeholder}</option>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{Array.isArray(options)
|
|
76
|
+
? options.map(op => renderOption(op, value))
|
|
77
|
+
: Object.entries(options).map(([label, opts]) => (
|
|
78
|
+
<optgroup
|
|
79
|
+
key={label}
|
|
80
|
+
label={label}
|
|
81
|
+
>
|
|
82
|
+
{opts.map(op => renderOption(op, value))}
|
|
83
|
+
</optgroup>
|
|
84
|
+
))}
|
|
85
|
+
</select>
|
|
86
|
+
|
|
87
|
+
{/* <Icon
|
|
88
|
+
name="lucide:chevron-down"
|
|
89
|
+
class="icon caret"
|
|
90
|
+
style="scale:115% 115%"
|
|
91
|
+
/> */}
|
|
92
|
+
</label>
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useSignal } from '@preact/signals'
|
|
2
|
+
import { useLayoutEffect } from 'preact/hooks'
|
|
3
|
+
import { t, getTheme, setTheme, onThemeChange } from 'duto'
|
|
4
|
+
|
|
5
|
+
import Select from './select'
|
|
6
|
+
|
|
7
|
+
function ThemeIcon(props: any) {
|
|
8
|
+
return (
|
|
9
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={[props.class || '', props.className || ''].filter(Boolean).join(' ')}><path d="M11 17a4 4 0 0 1-8 0V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2Z"/><path d="M16.7 13H19a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H7"/><path d="M 7 17h.01"/><path d="m11 8 2.3-2.3a2.4 2.4 0 0 1 3.404.004L18.6 7.6a2.4 2.4 0 0 1 .026 3.434L9.9 19.8"/></svg>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function ThemeSelect({ class: className = '' }) {
|
|
14
|
+
const options = [['auto', t`auto`], ['dark', t`dark`], ['light', t`light`]].map(([value, label]) => ({ value, label, selected: false }))
|
|
15
|
+
const selectedValue = useSignal(getTheme())
|
|
16
|
+
let unsubscribe: (() => void)[] = []
|
|
17
|
+
const handle = (e: any, theme?: string) => {
|
|
18
|
+
theme ??= getTheme()
|
|
19
|
+
selectedValue.value = theme
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
unsubscribe = onThemeChange(handle)
|
|
24
|
+
selectedValue.value = getTheme()
|
|
25
|
+
|
|
26
|
+
window.addEventListener('popstate', handle)
|
|
27
|
+
return () => {
|
|
28
|
+
window.removeEventListener('popstate', handle)
|
|
29
|
+
unsubscribe.forEach(unsub => unsub?.())
|
|
30
|
+
}
|
|
31
|
+
}, [])
|
|
32
|
+
|
|
33
|
+
const handleChange = async (e: Event) => {
|
|
34
|
+
const target = e.target as HTMLSelectElement
|
|
35
|
+
if (target) {
|
|
36
|
+
setTheme(target.value)
|
|
37
|
+
selectedValue.value = target.value
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const containerClasses = [
|
|
42
|
+
'locale-select overflow-hidden text-ellipsis p-1 rounded-lg hover:bg-neutral-200 hover:text-zinc-900',
|
|
43
|
+
className,
|
|
44
|
+
].filter(Boolean).join(' ')
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Select
|
|
48
|
+
Icon={ThemeIcon}
|
|
49
|
+
options={options}
|
|
50
|
+
value={selectedValue.value}
|
|
51
|
+
width="6.25em"
|
|
52
|
+
class="theme-select outline-none text-ellipsis absolute inset-0 opacity-0 cursor-pointer"
|
|
53
|
+
containerClass={containerClasses}
|
|
54
|
+
id="theme-select"
|
|
55
|
+
onchange={handleChange}
|
|
56
|
+
width="6.25em"
|
|
57
|
+
id="theme-select"
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
routes: {
|
|
3
|
+
about: 'about',
|
|
4
|
+
shop: 'shop',
|
|
5
|
+
notfound: 'not-found',
|
|
6
|
+
},
|
|
7
|
+
messages: {
|
|
8
|
+
auto: 'System default',
|
|
9
|
+
dark: 'Dark theme',
|
|
10
|
+
light: 'Light theme',
|
|
11
|
+
title: 'Welcome to Duto',
|
|
12
|
+
description: 'A fast, lightweight framework that keeps things simple!',
|
|
13
|
+
gotodocs: 'Go to docs',
|
|
14
|
+
notfound: 'Page not found',
|
|
15
|
+
sorry404: 'Sorry, the page you are looking for does not exist.',
|
|
16
|
+
backhome: 'Back to homepage',
|
|
17
|
+
about: 'About',
|
|
18
|
+
shop: 'Shop',
|
|
19
|
+
|
|
20
|
+
blog: 'Blog',
|
|
21
|
+
b1: 'This page is also static, the data was loaded via',
|
|
22
|
+
},
|
|
23
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const fallbackLocale = 'pt-br'
|
|
2
|
+
export const locales = ['en', 'pt-br']
|
|
3
|
+
|
|
4
|
+
export const options = {
|
|
5
|
+
en: 'English',
|
|
6
|
+
'pt-br': 'Português (Brasil)',
|
|
7
|
+
// de: 'Deutsch',
|
|
8
|
+
// es: 'Español',
|
|
9
|
+
// 'zh-cn': '简体中文',
|
|
10
|
+
// 'zh-tw': '正體中文',
|
|
11
|
+
// fr: 'Français',
|
|
12
|
+
// hi: 'हिन्दी',
|
|
13
|
+
// ar: 'العربية',
|
|
14
|
+
// ja: '日本語',
|
|
15
|
+
// ko: '한국어',
|
|
16
|
+
// pl: 'Polski',
|
|
17
|
+
// ru: 'Русский',
|
|
18
|
+
// it: 'Italiano',
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
routes: {
|
|
3
|
+
about: 'sobre',
|
|
4
|
+
shop: 'loja',
|
|
5
|
+
notfound: 'pagina-desconhecida',
|
|
6
|
+
},
|
|
7
|
+
messages: {
|
|
8
|
+
auto: 'Padrão do sistema',
|
|
9
|
+
dark: 'Escuro',
|
|
10
|
+
light: 'Claro',
|
|
11
|
+
title: 'Bem-vindo ao Duto',
|
|
12
|
+
description: 'Um framework rápido e leve que mantém as coisas simples!',
|
|
13
|
+
gotodocs: 'Documentação',
|
|
14
|
+
notfound: 'Página não encontrada',
|
|
15
|
+
sorry404: 'Esta página pode ter sido excluída ou movida...',
|
|
16
|
+
backhome: 'Ir para página inicial',
|
|
17
|
+
about: 'Sobre',
|
|
18
|
+
shop: 'Loja',
|
|
19
|
+
|
|
20
|
+
blog: 'Blog',
|
|
21
|
+
b1: 'Esta página também é estática, os dados foram carregados via',
|
|
22
|
+
},
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "duto-docs",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"duto": "workspace:*",
|
|
7
|
+
"@preact/signals": "catalog:",
|
|
8
|
+
"preact": "catalog:"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"bun-types": "catalog:",
|
|
12
|
+
"tailwindcss": "catalog:",
|
|
13
|
+
"typescript": "catalog:"
|
|
14
|
+
},
|
|
15
|
+
"duto": {
|
|
16
|
+
"type": "pages",
|
|
17
|
+
"platform": "workerd",
|
|
18
|
+
"dev": {
|
|
19
|
+
"port": 3000
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { t, Link } from 'duto'
|
|
2
|
+
|
|
3
|
+
export default function NotFound() {
|
|
4
|
+
return (
|
|
5
|
+
<div class="flex flex-col items-center justify-center text-center w-full text-lg py-16">
|
|
6
|
+
<h1 class="text-sky-gradient font-mono text-7xl lg:text-8xl">404</h1>
|
|
7
|
+
<p class="body-large mb-2 mt-4 text-3xl lg:text-4xl">{t`notfound`}</p>
|
|
8
|
+
<p class="body lg:body-large">{t`sorry404`}</p>
|
|
9
|
+
<Link to="/" class="border-b border-current mt-8">{t`backhome`}</Link>
|
|
10
|
+
</div>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
|
|
2
|
+
import { t, Link } from 'duto'
|
|
3
|
+
import type { Post, Posts } from '@/types'
|
|
4
|
+
import { JSJSON } from '../../../../packages/t0n/src/json'
|
|
5
|
+
import NotFound from '../404'
|
|
6
|
+
|
|
7
|
+
// Mock data
|
|
8
|
+
const ALL_POSTS: Posts = [
|
|
9
|
+
{
|
|
10
|
+
id: "hello-world",
|
|
11
|
+
title: "Hello World",
|
|
12
|
+
body:
|
|
13
|
+
"Welcome to my blog! This post was statically generated at build time using `getStaticProps`. When navigating here via the SPA router, `getProps` fetches the same data dynamically.",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "getting-started",
|
|
17
|
+
title: "Getting Started with SSG",
|
|
18
|
+
body:
|
|
19
|
+
"Static site generation combines the best of both worlds: fast initial loads with the flexibility of dynamic data fetching.",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "signals-islands",
|
|
23
|
+
title: "Signals & Islands",
|
|
24
|
+
body:
|
|
25
|
+
"Preact Signals give us fine-grained reactivity. Combined with the islands architecture, we can ship near-zero JS by default.",
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
export { Html } from '@/components/html'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns one entry per post.
|
|
34
|
+
* Each entry MUST include the route param (`slug`) plus any page props.
|
|
35
|
+
* The build script uses `slug` to compute the output path.
|
|
36
|
+
*/
|
|
37
|
+
export function getStaticProps() {
|
|
38
|
+
return ALL_POSTS.map((post) => ({
|
|
39
|
+
slug: post.id, // required: matches [slug] in the filename
|
|
40
|
+
post,
|
|
41
|
+
}))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Browser-time data ─────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Called during SPA navigation. Receives route params from wouter.
|
|
48
|
+
*/
|
|
49
|
+
export async function getProps({ slug }: { slug?: string }) {
|
|
50
|
+
const post = (await (await fetch('https://jsonplaceholder.typicode.com/posts/'+ slug)).json())
|
|
51
|
+
return { post }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Component ─────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
type Props = {
|
|
57
|
+
post?: Post | null,
|
|
58
|
+
slug?: string,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default function BlogPost({ post, slug }: Props) {
|
|
62
|
+
if (!post?.title)
|
|
63
|
+
return <NotFound />
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
<article class="mt-6 max-w-2xl">
|
|
68
|
+
<div>
|
|
69
|
+
<Link href="/blog">
|
|
70
|
+
←
|
|
71
|
+
<span class="ml-2">{t`blog`}</span>
|
|
72
|
+
</Link>
|
|
73
|
+
</div>
|
|
74
|
+
<h1 class="mt-2 text-4xl font-black text-sky-gradient">{post.title}</h1>
|
|
75
|
+
<p class="mt-6 leading-relaxed">{post.body}</p>
|
|
76
|
+
</article>
|
|
77
|
+
|
|
78
|
+
<div class="mt-12 p-4 rounded-lg text-sm bg-sky-50 text-sky-700 dark:bg-zinc-700 dark:text-white/80">
|
|
79
|
+
{[
|
|
80
|
+
['Route', window.$duto?.route?.pattern],
|
|
81
|
+
['Route path', window.$duto?.route?.path],
|
|
82
|
+
['Route params', JSJSON(window?.$duto?.route?.params || {})],
|
|
83
|
+
['Route search params', window?.$duto?.search?.toString()],
|
|
84
|
+
].map(([label, info]) => ( <div><b>{label}:</b> {info || 'n/a'}</div>))}
|
|
85
|
+
</div>
|
|
86
|
+
</>
|
|
87
|
+
)
|
|
88
|
+
}
|