create-freakpunch 0.0.0-stage → 0.2.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 +24 -3
- package/bin/create-freakpunch.mjs +227 -0
- package/files/README.md +19 -0
- package/files/app.config.ts +50 -0
- package/files/locales.ts +10 -0
- package/files/registry.ts +17 -0
- package/package.json +23 -6
- package/template/.dockerignore +17 -0
- package/template/.oxlintrc.json +9 -0
- package/template/AGENTS.md +6 -0
- package/template/BACKEND.md +108 -0
- package/template/CLAUDE.md +297 -0
- package/template/Dockerfile +19 -0
- package/template/_gitignore +26 -0
- package/template/components.json +27 -0
- package/template/design/COMPONENTS.md +145 -0
- package/template/design/DESIGN.md +212 -0
- package/template/design/PATTERNS.md +110 -0
- package/template/index.html +25 -0
- package/template/nginx.conf +32 -0
- package/template/package-lock.json +6484 -0
- package/template/package.json +51 -0
- package/template/public/favicon.svg +1 -0
- package/template/public/robots.txt +2 -0
- package/template/src/app.config.ts +50 -0
- package/template/src/app.roles.ts +24 -0
- package/template/src/components/ui/alert-dialog.tsx +181 -0
- package/template/src/components/ui/avatar.tsx +116 -0
- package/template/src/components/ui/badge.tsx +57 -0
- package/template/src/components/ui/breadcrumb.tsx +130 -0
- package/template/src/components/ui/button.tsx +58 -0
- package/template/src/components/ui/calendar.tsx +277 -0
- package/template/src/components/ui/card.tsx +99 -0
- package/template/src/components/ui/chart.tsx +370 -0
- package/template/src/components/ui/checkbox.tsx +28 -0
- package/template/src/components/ui/combobox.tsx +329 -0
- package/template/src/components/ui/command.tsx +191 -0
- package/template/src/components/ui/dialog.tsx +162 -0
- package/template/src/components/ui/dropdown-menu.tsx +271 -0
- package/template/src/components/ui/empty.tsx +103 -0
- package/template/src/components/ui/field.tsx +236 -0
- package/template/src/components/ui/input-group.tsx +263 -0
- package/template/src/components/ui/input.tsx +45 -0
- package/template/src/components/ui/kbd.tsx +31 -0
- package/template/src/components/ui/label.tsx +17 -0
- package/template/src/components/ui/pagination.tsx +134 -0
- package/template/src/components/ui/popover.tsx +87 -0
- package/template/src/components/ui/radio-group.tsx +35 -0
- package/template/src/components/ui/select.tsx +204 -0
- package/template/src/components/ui/separator.tsx +25 -0
- package/template/src/components/ui/sheet.tsx +139 -0
- package/template/src/components/ui/sidebar.tsx +741 -0
- package/template/src/components/ui/skeleton.tsx +13 -0
- package/template/src/components/ui/sonner.tsx +78 -0
- package/template/src/components/ui/spinner.tsx +42 -0
- package/template/src/components/ui/switch.tsx +29 -0
- package/template/src/components/ui/table.tsx +121 -0
- package/template/src/components/ui/tabs.tsx +191 -0
- package/template/src/components/ui/textarea.tsx +17 -0
- package/template/src/components/ui/toggle-group.tsx +94 -0
- package/template/src/components/ui/toggle.tsx +48 -0
- package/template/src/components/ui/tooltip.tsx +91 -0
- package/template/src/core/api/demo.ts +8 -0
- package/template/src/core/api/http.ts +76 -0
- package/template/src/core/api/mock.ts +88 -0
- package/template/src/core/api/rest.ts +67 -0
- package/template/src/core/api/types.ts +21 -0
- package/template/src/core/audit/audit.service.ts +94 -0
- package/template/src/core/audit/hooks.ts +19 -0
- package/template/src/core/auth/auth-store.ts +52 -0
- package/template/src/core/auth/auth.service.ts +86 -0
- package/template/src/core/auth/can.tsx +39 -0
- package/template/src/core/auth/dev-role-switcher.tsx +64 -0
- package/template/src/core/auth/local-credentials.ts +46 -0
- package/template/src/core/auth/permissions.ts +115 -0
- package/template/src/core/auth/require-auth.tsx +16 -0
- package/template/src/core/auth/require-permission.tsx +24 -0
- package/template/src/core/auth/roles.ts +9 -0
- package/template/src/core/auth/types.ts +20 -0
- package/template/src/core/auth/use-auth.ts +19 -0
- package/template/src/core/company/company.service.ts +94 -0
- package/template/src/core/company/hooks.ts +40 -0
- package/template/src/core/company/password-policy.ts +40 -0
- package/template/src/core/company/types.ts +46 -0
- package/template/src/core/company/use-app-name.ts +7 -0
- package/template/src/core/components/button-link.tsx +14 -0
- package/template/src/core/components/confirm-dialog.tsx +116 -0
- package/template/src/core/components/data-table/data-table.tsx +297 -0
- package/template/src/core/components/data-table/features.ts +24 -0
- package/template/src/core/components/data-table/table-header-cell.tsx +13 -0
- package/template/src/core/components/data-table/toolbar.tsx +100 -0
- package/template/src/core/components/data-table/use-list-url-state.ts +108 -0
- package/template/src/core/components/date-picker.tsx +71 -0
- package/template/src/core/components/file-attachments.tsx +84 -0
- package/template/src/core/components/form-error.tsx +9 -0
- package/template/src/core/components/form-page.tsx +93 -0
- package/template/src/core/components/image-upload.tsx +86 -0
- package/template/src/core/components/page.tsx +75 -0
- package/template/src/core/components/phone-input.tsx +58 -0
- package/template/src/core/components/search-select.tsx +57 -0
- package/template/src/core/components/segmented.tsx +41 -0
- package/template/src/core/components/states.tsx +97 -0
- package/template/src/core/components/status-badge.tsx +26 -0
- package/template/src/core/components/user-avatar.tsx +23 -0
- package/template/src/core/config/runtime.ts +16 -0
- package/template/src/core/config/types.ts +37 -0
- package/template/src/core/i18n/i18n.ts +69 -0
- package/template/src/core/i18n/i18next.d.ts +12 -0
- package/template/src/core/i18n/keys.ts +37 -0
- package/template/src/core/i18n/language-switcher.tsx +45 -0
- package/template/src/core/i18n/types.ts +17 -0
- package/template/src/core/i18n/use-format.ts +31 -0
- package/template/src/core/layout/app-shell.tsx +50 -0
- package/template/src/core/layout/app-sidebar.tsx +285 -0
- package/template/src/core/layout/command-menu.tsx +117 -0
- package/template/src/core/layout/top-bar.tsx +100 -0
- package/template/src/core/lib/csv.ts +27 -0
- package/template/src/core/lib/format.ts +87 -0
- package/template/src/core/lib/phone.ts +50 -0
- package/template/src/core/lib/storage.ts +27 -0
- package/template/src/core/modules/lazy-page.ts +9 -0
- package/template/src/core/modules/navigation.ts +39 -0
- package/template/src/core/modules/types.ts +55 -0
- package/template/src/core/notifications/hooks.ts +84 -0
- package/template/src/core/notifications/notification-center.tsx +251 -0
- package/template/src/core/notifications/notifications.service.ts +132 -0
- package/template/src/core/notifications/types.ts +70 -0
- package/template/src/core/preferences/preferences-store.ts +70 -0
- package/template/src/core/setup/setup-gate.tsx +20 -0
- package/template/src/core/setup/setup.service.ts +101 -0
- package/template/src/core/theme/brand-context.tsx +53 -0
- package/template/src/core/theme/brand.ts +29 -0
- package/template/src/core/theme/theme-context.tsx +62 -0
- package/template/src/core/theme/theme-options.ts +8 -0
- package/template/src/core/theme/theme-toggle.tsx +45 -0
- package/template/src/core/users/hooks.ts +51 -0
- package/template/src/core/users/types.ts +37 -0
- package/template/src/core/users/users.service.ts +167 -0
- package/template/src/hooks/use-mobile.ts +19 -0
- package/template/src/index.css +321 -0
- package/template/src/lib/utils.ts +1 -0
- package/template/src/locales/en/account.ts +180 -0
- package/template/src/locales/en/auth.ts +18 -0
- package/template/src/locales/en/common.ts +151 -0
- package/template/src/locales/en/company.ts +380 -0
- package/template/src/locales/en/dashboard.ts +46 -0
- package/template/src/locales/en/errors.ts +18 -0
- package/template/src/locales/en/notifications.ts +43 -0
- package/template/src/locales/en/settings.ts +25 -0
- package/template/src/locales/en/setup.ts +45 -0
- package/template/src/locales/index.ts +58 -0
- package/template/src/locales/tr/account.ts +184 -0
- package/template/src/locales/tr/auth.ts +22 -0
- package/template/src/locales/tr/common.ts +154 -0
- package/template/src/locales/tr/company.ts +384 -0
- package/template/src/locales/tr/dashboard.ts +50 -0
- package/template/src/locales/tr/errors.ts +22 -0
- package/template/src/locales/tr/notifications.ts +47 -0
- package/template/src/locales/tr/settings.ts +29 -0
- package/template/src/locales/tr/setup.ts +49 -0
- package/template/src/main.tsx +16 -0
- package/template/src/modules/locales.ts +10 -0
- package/template/src/modules/registry.ts +17 -0
- package/template/src/pages/dashboard/dashboard-page.tsx +55 -0
- package/template/src/pages/dashboard/quick-actions.tsx +37 -0
- package/template/src/pages/login-page.tsx +152 -0
- package/template/src/pages/no-access-page.tsx +11 -0
- package/template/src/pages/not-found-page.tsx +61 -0
- package/template/src/pages/settings/account/account.service.ts +128 -0
- package/template/src/pages/settings/account/components/mock-qr.tsx +50 -0
- package/template/src/pages/settings/account/components/password-strength.tsx +38 -0
- package/template/src/pages/settings/account/components/two-factor-dialog.tsx +140 -0
- package/template/src/pages/settings/account/hooks.ts +73 -0
- package/template/src/pages/settings/account/notifications-page.tsx +103 -0
- package/template/src/pages/settings/account/preferences-page.tsx +157 -0
- package/template/src/pages/settings/account/profile-page.tsx +242 -0
- package/template/src/pages/settings/account/security-page.tsx +364 -0
- package/template/src/pages/settings/account/types.ts +37 -0
- package/template/src/pages/settings/company/audit-log-page.tsx +126 -0
- package/template/src/pages/settings/company/branding-page.tsx +278 -0
- package/template/src/pages/settings/company/components/invite-dialog.tsx +106 -0
- package/template/src/pages/settings/company/components/read-only-notice.tsx +13 -0
- package/template/src/pages/settings/company/danger-zone-page.tsx +127 -0
- package/template/src/pages/settings/company/general-page.tsx +234 -0
- package/template/src/pages/settings/company/integrations/api-keys-panel.tsx +170 -0
- package/template/src/pages/settings/company/integrations/hooks.ts +37 -0
- package/template/src/pages/settings/company/integrations/integrations-page.tsx +57 -0
- package/template/src/pages/settings/company/integrations/integrations.service.ts +153 -0
- package/template/src/pages/settings/company/integrations/settings-forms.tsx +299 -0
- package/template/src/pages/settings/company/integrations/webhooks-panel.tsx +179 -0
- package/template/src/pages/settings/company/modules-page.tsx +65 -0
- package/template/src/pages/settings/company/roles-page.tsx +116 -0
- package/template/src/pages/settings/company/security-policies-page.tsx +164 -0
- package/template/src/pages/settings/company/users-page.tsx +262 -0
- package/template/src/pages/settings/settings-layout.tsx +57 -0
- package/template/src/pages/settings/settings-nav.ts +69 -0
- package/template/src/pages/settings/settings-routes.tsx +49 -0
- package/template/src/pages/setup-page.tsx +387 -0
- package/template/src/providers.tsx +68 -0
- package/template/src/router.tsx +59 -0
- package/template/tools/check-design.mjs +73 -0
- package/template/tools/new-module.mjs +90 -0
- package/template/tools/templates/module/api/__id__.mock.ts.tpl +17 -0
- package/template/tools/templates/module/api/__id__.service.ts.tpl +51 -0
- package/template/tools/templates/module/hooks.ts.tpl +57 -0
- package/template/tools/templates/module/index.ts.tpl +44 -0
- package/template/tools/templates/module/locales/en.ts.tpl +51 -0
- package/template/tools/templates/module/locales/tr.ts.tpl +55 -0
- package/template/tools/templates/module/pages/__id__-detail-page.tsx.tpl +103 -0
- package/template/tools/templates/module/pages/__id__-form-page.tsx.tpl +121 -0
- package/template/tools/templates/module/pages/__id__-list-page.tsx.tpl +234 -0
- package/template/tools/templates/module/types.ts.tpl +32 -0
- package/template/tsconfig.app.json +32 -0
- package/template/tsconfig.json +12 -0
- package/template/tsconfig.node.json +23 -0
- package/template/vite.config.ts +47 -0
package/README.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# create-freakpunch
|
|
2
|
+
|
|
3
|
+
Adds a clean admin panel to any project as a self-contained `fp-admin/` folder. It has its own
|
|
4
|
+
`package.json`, talks to your backend over HTTP, and works next to PHP, Laravel, Next.js or anything else.
|
|
5
|
+
The first time you open it, a setup wizard creates your company and admin account.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd my-project
|
|
9
|
+
npm create freakpunch@latest
|
|
10
|
+
cd fp-admin
|
|
11
|
+
npm run dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Options: `npm create freakpunch@latest my-folder`, `-- --no-install`, `-- --no-agent-files`.
|
|
15
|
+
|
|
16
|
+
The command also adds a short pointer to `fp-admin/` in your project's `CLAUDE.md` and `AGENTS.md`
|
|
17
|
+
(creating them if needed), so AI agents started at the project root follow the panel's design rules.
|
|
18
|
+
|
|
19
|
+
Get newer freakpunch files later (your modules and config are never touched; `--dry-run` previews):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd fp-admin
|
|
23
|
+
npm run fp:update
|
|
24
|
+
```
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* npm create freakpunch@latest adds ./fp-admin to the current project
|
|
4
|
+
* npx create-freakpunch@latest update refreshes freakpunch's own files inside fp-admin
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import { createHash } from 'node:crypto'
|
|
8
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
|
|
11
|
+
const pkgDir = path.resolve(import.meta.dirname, '..')
|
|
12
|
+
const templateDir = path.join(pkgDir, 'template')
|
|
13
|
+
const { version } = JSON.parse(readFileSync(path.join(pkgDir, 'package.json'), 'utf8'))
|
|
14
|
+
const MANIFEST = '.freakpunch/manifest.json'
|
|
15
|
+
|
|
16
|
+
/** These belong to the project: created on install, never touched by update. */
|
|
17
|
+
const PROJECT_OWNED = [
|
|
18
|
+
/^src\/app\.config\.ts$/,
|
|
19
|
+
/^src\/app\.roles\.ts$/,
|
|
20
|
+
/^src\/modules\//,
|
|
21
|
+
/^public\//,
|
|
22
|
+
/^README\.md$/,
|
|
23
|
+
/^\.gitignore$/,
|
|
24
|
+
/^package\.json$/,
|
|
25
|
+
/^package-lock\.json$/,
|
|
26
|
+
/^\.env/,
|
|
27
|
+
]
|
|
28
|
+
const isProjectOwned = (file) => PROJECT_OWNED.some((r) => r.test(file))
|
|
29
|
+
|
|
30
|
+
const AGENT_BLOCK_START = '<!-- freakpunch:start -->'
|
|
31
|
+
const AGENT_BLOCK_END = '<!-- freakpunch:end -->'
|
|
32
|
+
const agentBlock = (dir) => `${AGENT_BLOCK_START}
|
|
33
|
+
## Admin panel (\`${dir}/\`)
|
|
34
|
+
|
|
35
|
+
The admin panel lives in \`${dir}/\` and is built on freakpunch. Before changing anything there, read
|
|
36
|
+
\`${dir}/CLAUDE.md\` and \`${dir}/design/\` and follow them exactly. New features are modules
|
|
37
|
+
(\`cd ${dir} && npm run new-module -- <id>\`) and must use the existing design and components. Run
|
|
38
|
+
\`npm run check\` inside \`${dir}/\` before finishing.
|
|
39
|
+
${AGENT_BLOCK_END}
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
function fail(message) {
|
|
43
|
+
console.error(`\n✖ ${message}\n`)
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const hash = (buffer) => createHash('sha256').update(buffer).digest('hex')
|
|
48
|
+
/** Text compares with LF so Windows checkouts (CRLF) don't look edited. */
|
|
49
|
+
const hashFile = (abs) => hash(readFileSync(abs, 'utf8').replaceAll('\r\n', '\n'))
|
|
50
|
+
|
|
51
|
+
function walk(dir, base = dir) {
|
|
52
|
+
return readdirSync(dir).flatMap((name) => {
|
|
53
|
+
const abs = path.join(dir, name)
|
|
54
|
+
return statSync(abs).isDirectory() ? walk(abs, base) : [path.relative(base, abs).replaceAll('\\', '/')]
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Template files by their installed path (`_gitignore` → `.gitignore`). */
|
|
59
|
+
const templateFiles = () => walk(templateDir).map((file) => ({ source: file, target: file === '_gitignore' ? '.gitignore' : file }))
|
|
60
|
+
|
|
61
|
+
function copyInto(root, source, target) {
|
|
62
|
+
const abs = path.join(root, target)
|
|
63
|
+
mkdirSync(path.dirname(abs), { recursive: true })
|
|
64
|
+
copyFileSync(path.join(templateDir, source), abs)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function writeManifest(root, files) {
|
|
68
|
+
mkdirSync(path.join(root, '.freakpunch'), { recursive: true })
|
|
69
|
+
writeFileSync(path.join(root, MANIFEST), `${JSON.stringify({ version, files }, null, 2)}\n`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function addAgentNotes(project, dir) {
|
|
73
|
+
const changed = []
|
|
74
|
+
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
75
|
+
const abs = path.join(project, name)
|
|
76
|
+
const current = existsSync(abs) ? readFileSync(abs, 'utf8') : ''
|
|
77
|
+
if (current.includes(AGENT_BLOCK_START)) continue
|
|
78
|
+
writeFileSync(abs, current ? `${current.trimEnd()}\n\n${agentBlock(dir)}` : agentBlock(dir))
|
|
79
|
+
changed.push(name)
|
|
80
|
+
}
|
|
81
|
+
return changed
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function create(args) {
|
|
85
|
+
const flags = new Set(args.filter((a) => a.startsWith('--')))
|
|
86
|
+
const dirName = args.find((a) => !a.startsWith('--')) ?? 'fp-admin'
|
|
87
|
+
const project = process.cwd()
|
|
88
|
+
const root = path.resolve(project, dirName)
|
|
89
|
+
if (existsSync(root) && readdirSync(root).length > 0) {
|
|
90
|
+
fail(`${dirName}/ already exists. To get newer freakpunch files, run: cd ${dirName} && npm run fp:update`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`\nAdding freakpunch ${version} to ${dirName}/`)
|
|
94
|
+
const files = {}
|
|
95
|
+
for (const { source, target } of templateFiles()) {
|
|
96
|
+
copyInto(root, source, target)
|
|
97
|
+
if (!isProjectOwned(target)) files[target] = hashFile(path.join(root, target))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
copyFileSync(path.join(pkgDir, 'files/README.md'), path.join(root, 'README.md'))
|
|
101
|
+
// Browser data is per address, so each project gets its own dev port and panels never share accounts.
|
|
102
|
+
const port = 5200 + (parseInt(hash(root).slice(0, 6), 16) % 700)
|
|
103
|
+
writeFileSync(path.join(root, '.env.development'), `VITE_DEV_PORT=${port}\n`)
|
|
104
|
+
writeManifest(root, files)
|
|
105
|
+
|
|
106
|
+
const notes = flags.has('--no-agent-files') ? [] : addAgentNotes(project, dirName)
|
|
107
|
+
if (notes.length > 0) console.log(`Added a pointer to ${dirName}/ in ${notes.join(' and ')} so AI agents follow its rules.`)
|
|
108
|
+
|
|
109
|
+
if (!flags.has('--no-install')) {
|
|
110
|
+
console.log('Installing dependencies…')
|
|
111
|
+
const result = spawnSync('npm install --no-fund --no-audit', { cwd: root, stdio: 'inherit', shell: true })
|
|
112
|
+
if (result.status !== 0) console.error(`npm install failed; run it yourself inside ${dirName}/.`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (existsSync(path.join(project, 'tsconfig.json'))) {
|
|
116
|
+
console.log(`\n! This project has a tsconfig.json. Add "${dirName}" to its "exclude" so the host's build ignores the panel.`)
|
|
117
|
+
}
|
|
118
|
+
console.log(`
|
|
119
|
+
Done. Next:
|
|
120
|
+
cd ${dirName}
|
|
121
|
+
npm run dev
|
|
122
|
+
|
|
123
|
+
The first time you open it, a setup wizard creates your company and admin account.
|
|
124
|
+
Add a feature: npm run new-module -- orders --en "Orders" --tr "Siparişler"
|
|
125
|
+
Connect a backend, serve under /admin: see ${dirName}/BACKEND.md
|
|
126
|
+
`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function mergeDependencies(root) {
|
|
130
|
+
const localPath = path.join(root, 'package.json')
|
|
131
|
+
const local = JSON.parse(readFileSync(localPath, 'utf8'))
|
|
132
|
+
const next = JSON.parse(readFileSync(path.join(templateDir, 'package.json'), 'utf8'))
|
|
133
|
+
let changed = false
|
|
134
|
+
for (const key of ['dependencies', 'devDependencies', 'scripts']) {
|
|
135
|
+
for (const [name, value] of Object.entries(next[key] ?? {})) {
|
|
136
|
+
if (local[key]?.[name] !== value) {
|
|
137
|
+
local[key] = { ...local[key], [name]: value }
|
|
138
|
+
changed = true
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (changed) writeFileSync(localPath, `${JSON.stringify(local, null, 2)}\n`)
|
|
143
|
+
return changed
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function update(args) {
|
|
147
|
+
const dryRun = args.includes('--dry-run')
|
|
148
|
+
const root = [process.cwd(), path.resolve('fp-admin')].find((dir) => existsSync(path.join(dir, MANIFEST)))
|
|
149
|
+
if (!root) fail('No freakpunch install here. Run this inside fp-admin/ (or the folder that contains it).')
|
|
150
|
+
|
|
151
|
+
const old = JSON.parse(readFileSync(path.join(root, MANIFEST), 'utf8'))
|
|
152
|
+
const nextFiles = {}
|
|
153
|
+
const updated = []
|
|
154
|
+
const added = []
|
|
155
|
+
const conflicts = []
|
|
156
|
+
const conflictDir = path.join(root, '.freakpunch/conflicts')
|
|
157
|
+
if (!dryRun) rmSync(conflictDir, { recursive: true, force: true })
|
|
158
|
+
|
|
159
|
+
for (const { source, target } of templateFiles()) {
|
|
160
|
+
if (isProjectOwned(target)) continue
|
|
161
|
+
const incoming = hashFile(path.join(templateDir, source))
|
|
162
|
+
const abs = path.join(root, target)
|
|
163
|
+
const base = old.files[target]
|
|
164
|
+
if (!existsSync(abs)) {
|
|
165
|
+
added.push(target)
|
|
166
|
+
} else {
|
|
167
|
+
const local = hashFile(abs)
|
|
168
|
+
if (local === incoming) {
|
|
169
|
+
nextFiles[target] = incoming
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
if (local !== base) {
|
|
173
|
+
// Edited here and changed upstream: keep the local file, leave the new one beside it.
|
|
174
|
+
conflicts.push(target)
|
|
175
|
+
nextFiles[target] = base ?? incoming
|
|
176
|
+
if (!dryRun) {
|
|
177
|
+
const copy = path.join(conflictDir, target)
|
|
178
|
+
mkdirSync(path.dirname(copy), { recursive: true })
|
|
179
|
+
copyFileSync(path.join(templateDir, source), copy)
|
|
180
|
+
}
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
updated.push(target)
|
|
184
|
+
}
|
|
185
|
+
nextFiles[target] = incoming
|
|
186
|
+
if (!dryRun) copyInto(root, source, target)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Files freakpunch no longer ships: remove them unless they were edited here.
|
|
190
|
+
const removed = []
|
|
191
|
+
for (const [target, base] of Object.entries(old.files)) {
|
|
192
|
+
if (target in nextFiles) continue
|
|
193
|
+
const abs = path.join(root, target)
|
|
194
|
+
if (!existsSync(abs)) continue
|
|
195
|
+
if (hashFile(abs) !== base) {
|
|
196
|
+
conflicts.push(`${target} (removed upstream, kept because it was edited)`)
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
removed.push(target)
|
|
200
|
+
if (!dryRun) rmSync(abs)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let installNeeded = false
|
|
204
|
+
if (!dryRun) {
|
|
205
|
+
installNeeded = mergeDependencies(root)
|
|
206
|
+
writeManifest(root, nextFiles)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const list = (title, items) => items.length > 0 && console.log(`${title} (${items.length}):\n${items.map((f) => ` ${f}`).join('\n')}`)
|
|
210
|
+
console.log(`\nfreakpunch ${old.version} → ${version}${dryRun ? ' (dry run, nothing changed)' : ''}`)
|
|
211
|
+
list('Updated', updated)
|
|
212
|
+
list('Added', added)
|
|
213
|
+
list('Removed', removed)
|
|
214
|
+
list('Needs your decision: edited here and changed in freakpunch', conflicts)
|
|
215
|
+
if (conflicts.length > 0 && !dryRun) console.log('New versions are in .freakpunch/conflicts/. Merge what you need, then delete that folder.')
|
|
216
|
+
if (updated.length + added.length + removed.length + conflicts.length === 0) console.log('Already up to date.')
|
|
217
|
+
if (installNeeded) {
|
|
218
|
+
const result = spawnSync('npm install --no-fund --no-audit', { cwd: root, stdio: 'inherit', shell: true })
|
|
219
|
+
if (result.status !== 0) console.error('npm install failed; run it yourself.')
|
|
220
|
+
}
|
|
221
|
+
if (!dryRun) console.log('\nRun npm run check, then review the changes with git diff.\n')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!existsSync(templateDir)) fail('This package has no template. Run: npm run build:template')
|
|
225
|
+
const [command, ...rest] = process.argv.slice(2)
|
|
226
|
+
if (command === 'update') update(rest)
|
|
227
|
+
else create(process.argv.slice(2))
|
package/files/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Admin panel
|
|
2
|
+
|
|
3
|
+
Built on [freakpunch](https://www.npmjs.com/package/create-freakpunch).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm run dev # start it
|
|
7
|
+
npm run new-module -- orders --en "Orders" --tr "Siparişler" # add a feature
|
|
8
|
+
npm run check # design rules + lint + build
|
|
9
|
+
npm run fp:update # get newer freakpunch files
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The first time you open it, a setup wizard creates your company and admin account. To start over without a
|
|
13
|
+
backend, clear this site's data in your browser. Built-in pages such as Users and Audit log are off:
|
|
14
|
+
turn them on in `src/app.config.ts` → `features`.
|
|
15
|
+
|
|
16
|
+
- Your code: `src/modules/`, `src/app.config.ts`, `src/app.roles.ts`, `public/`.
|
|
17
|
+
- freakpunch's code: everything else. `npm run fp:update` refreshes it, so don't edit it here.
|
|
18
|
+
- Rules for people and AI agents: [CLAUDE.md](./CLAUDE.md) and [design/](./design).
|
|
19
|
+
- Connecting a backend and deploying under `/admin`: [BACKEND.md](./BACKEND.md).
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AppConfig, AuthMode } from '@/core/config/types'
|
|
2
|
+
|
|
3
|
+
/** This panel's settings. The company name and logo come from the setup wizard and Settings. */
|
|
4
|
+
export const appConfig = {
|
|
5
|
+
name: 'Admin',
|
|
6
|
+
/** A preset key from src/core/theme/brand.ts, or a custom { color, foreground } pair. */
|
|
7
|
+
brand: 'indigo',
|
|
8
|
+
|
|
9
|
+
workspaces: [{ id: 'main', name: 'Admin' }],
|
|
10
|
+
|
|
11
|
+
/** Sidebar group order; labels are translation keys. Modules attach nav items to these ids. */
|
|
12
|
+
navGroups: [
|
|
13
|
+
{ id: 'overview', label: 'common:nav.groups.overview' },
|
|
14
|
+
{ id: 'catalog', label: 'common:nav.groups.catalog' },
|
|
15
|
+
{ id: 'content', label: 'common:nav.groups.content' },
|
|
16
|
+
{ id: 'system', label: 'common:nav.groups.system' },
|
|
17
|
+
],
|
|
18
|
+
|
|
19
|
+
/** Turn modules on/off. `npm run new-module` adds them here. */
|
|
20
|
+
modules: {
|
|
21
|
+
// fp:modules
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
/** No marketing page: `/` opens the panel. */
|
|
25
|
+
landing: false,
|
|
26
|
+
|
|
27
|
+
/** Built-in areas, off until a project needs them. Setting one to true adds its pages and menu items. */
|
|
28
|
+
features: {
|
|
29
|
+
notifications: false,
|
|
30
|
+
users: false,
|
|
31
|
+
roles: false,
|
|
32
|
+
securityPolicies: false,
|
|
33
|
+
modules: false,
|
|
34
|
+
integrations: false,
|
|
35
|
+
auditLog: false,
|
|
36
|
+
dangerZone: false,
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
/** Default ISO 4217 currency for the setup wizard. */
|
|
40
|
+
currency: 'USD',
|
|
41
|
+
|
|
42
|
+
/** Real REST API base URL. */
|
|
43
|
+
apiBaseUrl: import.meta.env.VITE_API_URL ?? '/api',
|
|
44
|
+
|
|
45
|
+
/** mock (no backend, data stays in this browser) | bearer (JWT) | cookie (Laravel Sanctum). See BACKEND.md. */
|
|
46
|
+
auth: {
|
|
47
|
+
mode: (import.meta.env.VITE_AUTH_MODE ?? 'mock') as AuthMode,
|
|
48
|
+
csrfUrl: import.meta.env.VITE_CSRF_URL ?? '/sanctum/csrf-cookie',
|
|
49
|
+
},
|
|
50
|
+
} satisfies AppConfig
|
package/files/locales.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { appConfig } from '@/app.config'
|
|
2
|
+
import type { AppModule } from '@/core/modules/types'
|
|
3
|
+
// fp:module-imports
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Every module in this panel. `npm run new-module` registers new ones here and
|
|
7
|
+
* enables them in src/app.config.ts → `modules`.
|
|
8
|
+
*/
|
|
9
|
+
export const moduleRegistry = {
|
|
10
|
+
// fp:module-registry
|
|
11
|
+
} satisfies Record<string, AppModule>
|
|
12
|
+
|
|
13
|
+
export type ModuleId = keyof typeof moduleRegistry
|
|
14
|
+
|
|
15
|
+
export const enabledModules: AppModule[] = (Object.keys(moduleRegistry) as ModuleId[])
|
|
16
|
+
.filter((id) => appConfig.modules[id])
|
|
17
|
+
.map((id) => moduleRegistry[id])
|
package/package.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "create-freakpunch",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "create-freakpunch",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Add the freakpunch admin panel to any project as an fp-admin folder.",
|
|
5
|
+
"keywords": ["admin", "admin-panel", "dashboard", "react", "vite", "template"],
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"create-freakpunch": "bin/create-freakpunch.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"files",
|
|
14
|
+
"template"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20.11"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build:template": "node scripts/build-template.mjs",
|
|
21
|
+
"prepack": "node scripts/build-template.mjs"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
|
3
|
+
"plugins": ["react", "typescript", "oxc"],
|
|
4
|
+
"ignorePatterns": ["dist", "src/components/ui/**", "src/hooks/**", "packages/**", "tools/**"],
|
|
5
|
+
"rules": {
|
|
6
|
+
"react/rules-of-hooks": "error",
|
|
7
|
+
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Agent instructions
|
|
2
|
+
|
|
3
|
+
Read [CLAUDE.md](./CLAUDE.md) and [design/](./design) completely before changing anything, and follow them
|
|
4
|
+
exactly. They apply to every AI agent working here, not only Claude.
|
|
5
|
+
|
|
6
|
+
Before you finish, run `npm run check` and fix everything it reports.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Connecting a backend
|
|
2
|
+
|
|
3
|
+
The panel is a static single-page app. It talks to your backend over HTTP, so the backend can be
|
|
4
|
+
Laravel, Next.js route handlers, Spring Boot or anything else. Until you switch it, everything runs on demo data.
|
|
5
|
+
|
|
6
|
+
## 1. Settings
|
|
7
|
+
|
|
8
|
+
Put these in `.env` (`.env.development` holds this panel's own dev port) (build time, not secret: never put secrets in `VITE_` variables).
|
|
9
|
+
|
|
10
|
+
| Variable | Example | Meaning |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| `VITE_AUTH_MODE` | `bearer` or `cookie` | Default `mock` (demo data). `bearer`: token in the `Authorization` header. `cookie`: session cookie + `X-XSRF-TOKEN` (Laravel Sanctum). |
|
|
13
|
+
| `VITE_API_URL` | `/api` or `https://api.example.com` | API base URL. |
|
|
14
|
+
| `VITE_CSRF_URL` | `/sanctum/csrf-cookie` | Cookie mode: fetched before login. |
|
|
15
|
+
| `VITE_BASE_PATH` | `/admin/` | Where the panel is served. Keep the trailing slash. Default `/`. |
|
|
16
|
+
| `VITE_DEV_API_PROXY` | `http://localhost:8000` | `npm run dev` only: forwards `VITE_API_URL` calls to your backend. |
|
|
17
|
+
|
|
18
|
+
## 2. Auth endpoints
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
POST {API}/auth/login { email, password } → { user, token?, expiresAt? }
|
|
22
|
+
POST {API}/auth/logout
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- `token` is required in bearer mode and omitted in cookie mode. `expiresAt` is epoch milliseconds (default 7 days).
|
|
26
|
+
- `user` matches `User` in `src/core/users/types.ts`. `role` must be a role the panel knows: `owner`, `admin`, `editor`, `viewer` or one from `src/app.roles.ts`.
|
|
27
|
+
- Any `401` signs the user out. Errors may return `{ "message": "…" }`, which is shown to the user.
|
|
28
|
+
|
|
29
|
+
**UI permission checks are not security.** Enforce every permission on the server, using the same strings (`orders.delete`).
|
|
30
|
+
|
|
31
|
+
## 3. First-run setup
|
|
32
|
+
|
|
33
|
+
Without a backend the setup wizard keeps the Owner account in this browser (salted hash, not real security),
|
|
34
|
+
so it is for building only. With a backend it uses:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
GET {API}/setup/status → { needsSetup: boolean }
|
|
38
|
+
POST {API}/setup → same response as /auth/login
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```jsonc
|
|
42
|
+
{
|
|
43
|
+
"company": { "name": "", "language": "en", "timeZone": "Europe/Istanbul", "currency": "USD" },
|
|
44
|
+
"admin": { "firstName": "", "lastName": "", "email": "", "password": "" },
|
|
45
|
+
"branding": { "brand": "indigo", "logo": "data:image/png;base64,…" } // logo is optional
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**`POST /setup` must return 403 once an Owner exists**, or anyone could take over the panel. Check that, validate the
|
|
50
|
+
password and hash it on the server. If you create the first admin another way (seeder, CLI), skip these
|
|
51
|
+
endpoints: when `/setup/status` fails, the panel goes straight to login.
|
|
52
|
+
|
|
53
|
+
## 4. Module endpoints
|
|
54
|
+
|
|
55
|
+
Modules created with `npm run new-module` switch to these automatically when `VITE_AUTH_MODE` is not `mock`:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
GET {API}/orders?page=1&pageSize=10&search=x&sort=name.asc&status=active
|
|
59
|
+
GET {API}/orders/:id
|
|
60
|
+
POST {API}/orders
|
|
61
|
+
PUT {API}/orders/:id
|
|
62
|
+
DELETE {API}/orders/:id
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Pages are 1-based. List responses may use any of these shapes (`src/core/api/rest.ts`):
|
|
66
|
+
|
|
67
|
+
```jsonc
|
|
68
|
+
{ "items": [], "total": 0, "page": 1, "pageSize": 10 } // freakpunch
|
|
69
|
+
{ "data": [], "total": 0, "current_page": 1, "per_page": 10 } // Laravel paginate()
|
|
70
|
+
{ "data": [], "meta": { "total": 0, "current_page": 1, "per_page": 10 } } // Laravel API resource
|
|
71
|
+
{ "content": [], "totalElements": 0, "number": 0, "size": 10 } // Spring Data Page
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Spring Data expects a 0-based `page` in the request, and a backend may use other parameter names. For those,
|
|
75
|
+
or for module-specific calls, write the method by hand in the module's `api/*.service.ts` using `http`.
|
|
76
|
+
|
|
77
|
+
## 5. Built-in areas that still save locally
|
|
78
|
+
|
|
79
|
+
These services keep their data in the browser in every auth mode, and they start empty. Most are switched off in a new panel. For each one you use, implement its interface with
|
|
80
|
+
`http` and change its one `export const xService = …` line. Switch off the rest in `src/app.config.ts`
|
|
81
|
+
→ `features` (their pages and menu items disappear). Don't leave an unconnected area on for a real site.
|
|
82
|
+
|
|
83
|
+
| Area | Service | Feature flag |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| Users, profile | `src/core/users/users.service.ts` | `users` (Profile always shows) |
|
|
86
|
+
| Company settings, branding, security policies | `src/core/company/company.service.ts` | `securityPolicies`, `dangerZone` (General and Branding always show) |
|
|
87
|
+
| Audit log | `src/core/audit/audit.service.ts` | `auditLog` |
|
|
88
|
+
| Notifications | `src/core/notifications/notifications.service.ts` | `notifications` |
|
|
89
|
+
| Integrations, webhooks, API keys | `src/pages/settings/company/integrations/integrations.service.ts` | `integrations` |
|
|
90
|
+
| Password, 2FA, sessions | `src/pages/settings/account/account.service.ts` | none |
|
|
91
|
+
|
|
92
|
+
Updating a core service means editing a freakpunch file, which `npm run check` reports. Until there is a
|
|
93
|
+
built-in way to replace them from a project, fix them in freakpunch itself so every project benefits.
|
|
94
|
+
|
|
95
|
+
## 6. Deploying
|
|
96
|
+
|
|
97
|
+
`npm run build` writes static files to `dist/`. Any URL inside the panel must fall back to `index.html`.
|
|
98
|
+
|
|
99
|
+
**Subdomain (`admin.example.com`):** deploy `dist/` anywhere static, or use the included `Dockerfile`.
|
|
100
|
+
|
|
101
|
+
**Same domain (`example.com/admin`):** set `VITE_BASE_PATH=/admin/` and build, then:
|
|
102
|
+
|
|
103
|
+
- *Laravel / PHP:* copy `dist/` to `public/admin/` and rewrite unknown `/admin/*` paths to `public/admin/index.html`.
|
|
104
|
+
- *Next.js:* copy `dist/` to `public/admin/` and add a rewrite from `/admin/:path*` to `/admin/index.html`.
|
|
105
|
+
- *nginx:* `location /admin/ { alias /var/www/admin/; try_files $uri $uri/ /admin/index.html; }`
|
|
106
|
+
|
|
107
|
+
Same-domain hosting is the simplest route for cookie mode, because it avoids cross-site cookie and CORS setup.
|
|
108
|
+
If the host project has its own `tsconfig.json`, add `fp-admin` to its `exclude`.
|