create-freakpunch 0.1.0 → 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 -23
- package/bin/create-freakpunch.mjs +6 -6
- package/files/README.md +19 -15
- package/files/app.config.ts +50 -58
- package/files/locales.ts +10 -0
- package/files/registry.ts +17 -0
- package/package.json +23 -23
- package/template/BACKEND.md +108 -86
- package/template/CLAUDE.md +15 -10
- package/template/design/COMPONENTS.md +1 -1
- package/template/design/DESIGN.md +3 -3
- package/template/design/PATTERNS.md +5 -5
- package/template/index.html +1 -1
- package/template/src/app.config.ts +50 -61
- package/template/src/app.roles.ts +24 -24
- package/template/src/core/api/demo.ts +8 -0
- package/template/src/core/audit/audit.service.ts +3 -34
- package/template/src/core/auth/auth.service.ts +25 -14
- package/template/src/core/auth/local-credentials.ts +46 -0
- package/template/src/core/auth/permissions.ts +1 -1
- package/template/src/core/company/company.service.ts +14 -12
- package/template/src/core/company/use-app-name.ts +7 -0
- package/template/src/core/components/page.tsx +4 -3
- package/template/src/core/config/runtime.ts +16 -14
- package/template/src/core/config/types.ts +3 -2
- package/template/src/core/layout/app-sidebar.tsx +19 -2
- package/template/src/core/notifications/notifications.service.ts +10 -115
- package/template/src/core/notifications/types.ts +5 -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/users/users.service.ts +2 -2
- package/template/src/locales/en/auth.ts +2 -1
- package/template/src/locales/en/common.ts +1 -0
- package/template/src/locales/en/company.ts +3 -1
- package/template/src/locales/en/dashboard.ts +5 -0
- package/template/src/locales/en/setup.ts +45 -0
- package/template/src/locales/index.ts +4 -4
- package/template/src/locales/tr/auth.ts +2 -1
- package/template/src/locales/tr/common.ts +1 -0
- package/template/src/locales/tr/company.ts +3 -1
- package/template/src/locales/tr/dashboard.ts +5 -0
- package/template/src/locales/tr/setup.ts +49 -0
- package/template/src/modules/locales.ts +0 -4
- package/template/src/modules/registry.ts +17 -19
- package/template/src/pages/dashboard/dashboard-page.tsx +28 -243
- package/template/src/pages/dashboard/quick-actions.tsx +37 -0
- package/template/src/pages/login-page.tsx +26 -14
- package/template/src/pages/not-found-page.tsx +1 -2
- package/template/src/pages/settings/account/account.service.ts +22 -41
- package/template/src/pages/settings/account/notifications-page.tsx +3 -3
- package/template/src/pages/settings/company/general-page.tsx +6 -4
- package/template/src/pages/settings/company/integrations/integrations.service.ts +8 -28
- package/template/src/pages/settings/company/modules-page.tsx +10 -2
- package/template/src/pages/settings/company/roles-page.tsx +2 -1
- package/template/src/pages/setup-page.tsx +387 -0
- package/template/src/router.tsx +9 -4
- package/template/tools/new-module.mjs +90 -90
- package/template/tools/templates/module/index.ts.tpl +44 -37
- package/template/tools/templates/module/locales/en.ts.tpl +51 -45
- package/template/tools/templates/module/locales/tr.ts.tpl +55 -49
- package/template/{src/modules/products/pages/product-detail-page.tsx → tools/templates/module/pages/__id__-detail-page.tsx.tpl} +103 -108
- package/template/tools/templates/module/pages/__id__-form-page.tsx.tpl +121 -121
- package/template/tools/templates/module/pages/__id__-list-page.tsx.tpl +234 -237
- package/template/vite.config.ts +6 -3
- package/template/src/core/users/users.mock.ts +0 -24
- package/template/src/locales/en/landing.ts +0 -15
- package/template/src/locales/tr/landing.ts +0 -19
- package/template/src/modules/products/api/products.mock.ts +0 -46
- package/template/src/modules/products/api/products.service.ts +0 -84
- package/template/src/modules/products/components/product-status.tsx +0 -21
- package/template/src/modules/products/hooks.ts +0 -73
- package/template/src/modules/products/index.ts +0 -50
- package/template/src/modules/products/locales/en.ts +0 -78
- package/template/src/modules/products/locales/tr.ts +0 -82
- package/template/src/modules/products/pages/product-form-page.tsx +0 -211
- package/template/src/modules/products/pages/products-list-page.tsx +0 -286
- package/template/src/modules/products/types.ts +0 -61
- package/template/src/pages/dashboard/dashboard.service.ts +0 -92
- package/template/src/pages/landing-page.tsx +0 -79
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { appConfig } from '@/app.config'
|
|
2
1
|
import { ApiError } from '@/core/api/http'
|
|
2
|
+
import { demoSeed } from '@/core/api/demo'
|
|
3
3
|
import { createMockDocument, delay } from '@/core/api/mock'
|
|
4
4
|
import { authStore } from '@/core/auth/auth-store'
|
|
5
5
|
import type { Role } from '@/core/auth/roles'
|
|
@@ -39,118 +39,15 @@ type NotificationsDoc = {
|
|
|
39
39
|
lastArrivalAt: number
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
type Draft = Omit<AppNotification, 'id' | 'createdAt' | 'readAt'>
|
|
42
|
+
export type Draft = Omit<AppNotification, 'id' | 'createdAt' | 'readAt'>
|
|
43
43
|
|
|
44
44
|
const minute = 60_000
|
|
45
|
-
const hasProducts = appConfig.modules.products
|
|
46
|
-
const links = {
|
|
47
|
-
users: '/app/settings/company/users',
|
|
48
|
-
general: '/app/settings/company/general',
|
|
49
|
-
branding: '/app/settings/company/branding',
|
|
50
|
-
security: '/app/settings/security',
|
|
51
|
-
lowStock: '/app/products?status=active&sort=stock.asc',
|
|
52
|
-
recentProducts: '/app/products?sort=updatedAt.desc',
|
|
53
|
-
dashboard: '/app',
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Building blocks for the seed and for the notifications that arrive later.
|
|
57
|
-
const drafts = {
|
|
58
|
-
userInvited: (email: string, role: Role, name: string): Draft => ({
|
|
59
|
-
event: 'userJoined', type: 'info', template: 'userInvited', params: { email, role, name }, link: links.users,
|
|
60
|
-
}),
|
|
61
|
-
userJoined: (name: string): Draft => ({
|
|
62
|
-
event: 'userJoined', type: 'success', template: 'userJoined', params: { name }, link: links.users,
|
|
63
|
-
}),
|
|
64
|
-
roleChangedOther: (name: string, role: Role): Draft => ({
|
|
65
|
-
event: 'roleChanged', type: 'info', template: 'roleChangedOther', params: { name, role }, link: links.users,
|
|
66
|
-
}),
|
|
67
|
-
roleChangedYou: (role: Role, name: string): Draft => ({
|
|
68
|
-
event: 'roleChanged', type: 'info', template: 'roleChangedYou', params: { role, name }, link: '/app/settings/profile',
|
|
69
|
-
}),
|
|
70
|
-
lowStock: (count: number): Draft => ({
|
|
71
|
-
event: 'lowStock', type: 'warning', template: 'lowStock', params: { count }, link: links.lowStock,
|
|
72
|
-
}),
|
|
73
|
-
productUpdated: (name: string, count: number): Draft => ({
|
|
74
|
-
event: 'productChanged', type: 'info', template: 'productUpdated', params: { name, count }, link: links.recentProducts,
|
|
75
|
-
}),
|
|
76
|
-
settingsChanged: (name: string): Draft => ({
|
|
77
|
-
event: 'settingsChanged', type: 'info', template: 'settingsChanged', params: { name }, link: links.general,
|
|
78
|
-
}),
|
|
79
|
-
brandingChanged: (name: string): Draft => ({
|
|
80
|
-
event: 'settingsChanged', type: 'success', template: 'brandingChanged', params: { name }, link: links.branding,
|
|
81
|
-
}),
|
|
82
|
-
securityAlert: (browser: string, os: string, location: string): Draft => ({
|
|
83
|
-
event: 'newSignIn', type: 'error', template: 'securityAlert', params: { browser, os, location }, link: links.security,
|
|
84
|
-
}),
|
|
85
|
-
passwordChanged: (): Draft => ({
|
|
86
|
-
event: 'passwordChanged', type: 'success', template: 'passwordChanged', params: {}, link: links.security,
|
|
87
|
-
}),
|
|
88
|
-
weeklySummary: (orders: number, revenue: number): Draft => ({
|
|
89
|
-
event: 'weeklySummary', type: 'success', template: 'weeklySummary', params: { orders, revenue }, link: links.dashboard,
|
|
90
|
-
}),
|
|
91
|
-
announcement: (): Draft => ({ event: 'announcements', type: 'info', template: 'announcement', params: {} }),
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const isProductEvent = (d: Draft) => d.event === 'lowStock' || d.event === 'productChanged'
|
|
95
|
-
|
|
96
|
-
/** What each role starts with: [draft, minutes ago, read]. */
|
|
97
|
-
function seedFor(role: Role): [Draft, number, boolean][] {
|
|
98
|
-
const peer = role === 'owner' ? 'Maya Chen' : 'Deniz Aydın'
|
|
99
|
-
switch (role) {
|
|
100
|
-
case 'owner':
|
|
101
|
-
case 'admin':
|
|
102
|
-
return [
|
|
103
|
-
[drafts.userInvited('lena.fischer@freakpunch.site', 'editor', peer), 18, false],
|
|
104
|
-
[drafts.lowStock(3), 95, false],
|
|
105
|
-
[drafts.securityAlert('Edge', 'Windows', 'Berlin, DE'), 260, false],
|
|
106
|
-
[drafts.roleChangedOther('Can Yılmaz', 'admin'), 60 * 26, true],
|
|
107
|
-
[drafts.settingsChanged(peer), 60 * 49, true],
|
|
108
|
-
[drafts.userJoined('Burak Öztürk'), 60 * 75, true],
|
|
109
|
-
[drafts.brandingChanged(peer), 60 * 24 * 6, true],
|
|
110
|
-
]
|
|
111
|
-
case 'editor':
|
|
112
|
-
return [
|
|
113
|
-
[drafts.lowStock(3), 40, false],
|
|
114
|
-
[drafts.productUpdated('Elif Şahin', 4), 130, false],
|
|
115
|
-
[drafts.securityAlert('Safari', 'iOS', 'Ankara, TR'), 60 * 20, true],
|
|
116
|
-
[drafts.roleChangedYou('editor', 'Maya Chen'), 60 * 24 * 3, true],
|
|
117
|
-
[drafts.weeklySummary(128, 18_420), 60 * 24 * 5, true],
|
|
118
|
-
]
|
|
119
|
-
case 'viewer':
|
|
120
|
-
default:
|
|
121
|
-
return [
|
|
122
|
-
[drafts.weeklySummary(128, 18_420), 55, false],
|
|
123
|
-
[drafts.roleChangedYou('viewer', 'Maya Chen'), 60 * 7, false],
|
|
124
|
-
[drafts.lowStock(2), 60 * 30, true],
|
|
125
|
-
[drafts.passwordChanged(), 60 * 24 * 4, true],
|
|
126
|
-
[drafts.announcement(), 60 * 24 * 8, true],
|
|
127
|
-
]
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Pool the mock picks from when a "new" notification arrives. */
|
|
132
|
-
function arrivalsFor(role: Role): (() => Draft)[] {
|
|
133
|
-
const pick = <T,>(items: readonly T[]) => items[Math.floor(Math.random() * items.length)]!
|
|
134
|
-
const names = ['Elif Şahin', 'Can Yılmaz', 'Maya Chen', 'Jonas Weber']
|
|
135
|
-
const emails = ['mert.kaya', 'zeynep.arslan', 'noah.becker', 'ayse.demir'].map((n) => `${n}@freakpunch.site`)
|
|
136
|
-
const lowStock = () => drafts.lowStock(1 + Math.floor(Math.random() * 4))
|
|
137
|
-
switch (role) {
|
|
138
|
-
case 'owner':
|
|
139
|
-
case 'admin':
|
|
140
|
-
return [
|
|
141
|
-
() => drafts.userInvited(pick(emails), pick(['editor', 'viewer'] as const), pick(names)),
|
|
142
|
-
() => drafts.settingsChanged(pick(names)),
|
|
143
|
-
() => drafts.securityAlert('Chrome', 'Linux', pick(['Amsterdam, NL', 'İzmir, TR'])),
|
|
144
|
-
lowStock,
|
|
145
|
-
]
|
|
146
|
-
case 'editor':
|
|
147
|
-
return [() => drafts.productUpdated(pick(names), 1 + Math.floor(Math.random() * 5)), lowStock]
|
|
148
|
-
case 'viewer':
|
|
149
|
-
default:
|
|
150
|
-
return [() => drafts.weeklySummary(90 + Math.floor(Math.random() * 80), 12_000 + Math.floor(Math.random() * 9_000)), lowStock]
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
45
|
|
|
46
|
+
/** Sample feed; without it the list starts empty and only a real backend adds to it. */
|
|
47
|
+
const demo = demoSeed<{
|
|
48
|
+
seedFor(role: Role): [Draft, number, boolean][]
|
|
49
|
+
arrivalsFor(role: Role): (() => Draft)[]
|
|
50
|
+
}>('notifications')
|
|
154
51
|
function createMockNotificationsService(): NotificationsService {
|
|
155
52
|
const docs = new Map<string, ReturnType<typeof createMockDocument<NotificationsDoc>>>()
|
|
156
53
|
|
|
@@ -162,9 +59,7 @@ function createMockNotificationsService(): NotificationsService {
|
|
|
162
59
|
user.id,
|
|
163
60
|
createMockDocument(`notifications:${user.id}`, () => {
|
|
164
61
|
const now = Date.now()
|
|
165
|
-
const items = seedFor(user.role)
|
|
166
|
-
.filter(([d]) => hasProducts || !isProductEvent(d))
|
|
167
|
-
.map(([draft, ago, read], i): AppNotification => {
|
|
62
|
+
const items = (demo?.seedFor(user.role) ?? []).map(([draft, ago, read], i): AppNotification => {
|
|
168
63
|
const createdAt = new Date(now - ago * minute).toISOString()
|
|
169
64
|
return { ...draft, id: `n_${user.id}_${i + 1}`, createdAt, readAt: read ? createdAt : undefined }
|
|
170
65
|
})
|
|
@@ -189,9 +84,9 @@ function createMockNotificationsService(): NotificationsService {
|
|
|
189
84
|
const data = doc.get()
|
|
190
85
|
if (Date.now() - data.lastArrivalAt < 45_000 || Math.random() > 0.4) return
|
|
191
86
|
const settings = settingsOf(data)
|
|
192
|
-
const pool = arrivalsFor(role)
|
|
87
|
+
const pool = (demo?.arrivalsFor(role) ?? [])
|
|
193
88
|
.map((make) => make())
|
|
194
|
-
.filter((d) => settings[d.event].inApp
|
|
89
|
+
.filter((d) => settings[d.event].inApp)
|
|
195
90
|
const draft = pool[Math.floor(Math.random() * pool.length)]
|
|
196
91
|
const now = Date.now()
|
|
197
92
|
if (!draft) return save({ lastArrivalAt: now })
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isDemo } from '@/core/config/runtime'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Notification event types. One catalog drives both the Settings → Notifications
|
|
3
5
|
* toggles (email / in-app per event) and the in-app notification feed: a
|
|
@@ -15,6 +17,9 @@ export const notificationEvents = [
|
|
|
15
17
|
{ id: 'announcements', group: 'updates' },
|
|
16
18
|
] as const
|
|
17
19
|
|
|
20
|
+
/** What Settings → Notifications lists; catalog events belong to the demo's sample module. */
|
|
21
|
+
export const visibleNotificationEvents = notificationEvents.filter((e) => isDemo || e.group !== 'catalog')
|
|
22
|
+
|
|
18
23
|
export type NotificationEventId = (typeof notificationEvents)[number]['id']
|
|
19
24
|
export type NotificationGroup = (typeof notificationEvents)[number]['group']
|
|
20
25
|
export type NotificationChannels = { email: boolean; inApp: boolean }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { Navigate, Outlet, useLocation } from 'react-router'
|
|
3
|
+
|
|
4
|
+
import { setupService } from './setup.service'
|
|
5
|
+
|
|
6
|
+
/** Sends a brand-new install to the setup wizard, and keeps finished installs out of it. */
|
|
7
|
+
export function SetupGate() {
|
|
8
|
+
const { pathname } = useLocation()
|
|
9
|
+
const { data: needsSetup, isPending } = useQuery({
|
|
10
|
+
queryKey: ['setup', 'status'],
|
|
11
|
+
queryFn: () => setupService.needsSetup(),
|
|
12
|
+
staleTime: Infinity,
|
|
13
|
+
retry: false,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
if (isPending) return <div className="min-h-svh bg-canvas" />
|
|
17
|
+
if (needsSetup && pathname !== '/setup') return <Navigate to="/setup" replace />
|
|
18
|
+
if (!needsSetup && pathname === '/setup') return <Navigate to="/app" replace />
|
|
19
|
+
return <Outlet />
|
|
20
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ApiError, http } from '@/core/api/http'
|
|
2
|
+
import { delay } from '@/core/api/mock'
|
|
3
|
+
import { authStore } from '@/core/auth/auth-store'
|
|
4
|
+
import { fetchCsrfCookie, startLocalSession, type AuthResponse } from '@/core/auth/auth.service'
|
|
5
|
+
import { localCredentials } from '@/core/auth/local-credentials'
|
|
6
|
+
import { createCompanySettings } from '@/core/company/company.service'
|
|
7
|
+
import type { CompanyGeneral, Currency } from '@/core/company/types'
|
|
8
|
+
import { authMode, isDemo } from '@/core/config/runtime'
|
|
9
|
+
import { storage } from '@/core/lib/storage'
|
|
10
|
+
import type { Language } from '@/core/i18n/types'
|
|
11
|
+
import type { BrandSetting } from '@/core/theme/brand'
|
|
12
|
+
import type { User } from '@/core/users/types'
|
|
13
|
+
|
|
14
|
+
export type SetupInput = {
|
|
15
|
+
company: { name: string; language: Language; timeZone: string; currency: Currency }
|
|
16
|
+
admin: { firstName: string; lastName: string; email: string; password: string }
|
|
17
|
+
branding: { brand: BrandSetting; logo?: string }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* First run: create the company and its Owner. With a backend these are
|
|
22
|
+
* GET /setup/status and POST /setup (see BACKEND.md), and the server must
|
|
23
|
+
* refuse POST /setup once an Owner exists.
|
|
24
|
+
*/
|
|
25
|
+
export interface SetupService {
|
|
26
|
+
needsSetup(): Promise<boolean>
|
|
27
|
+
complete(input: SetupInput): Promise<void>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Company name, logo and brand stay in the company service until it is connected to a backend too. */
|
|
31
|
+
function saveCompany({ company, admin, branding }: SetupInput) {
|
|
32
|
+
const general: Partial<CompanyGeneral> = {
|
|
33
|
+
name: company.name,
|
|
34
|
+
contactEmail: admin.email,
|
|
35
|
+
defaultLanguage: company.language,
|
|
36
|
+
timeZone: company.timeZone,
|
|
37
|
+
currency: company.currency,
|
|
38
|
+
}
|
|
39
|
+
storage.set('mock:company', createCompanySettings(general, { brand: branding.brand, logoLight: branding.logo, logoDark: branding.logo }))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Clears anything left in this browser, such as data from another project on the same address. */
|
|
43
|
+
function clearLocalData() {
|
|
44
|
+
for (const key of Object.keys(localStorage)) {
|
|
45
|
+
if (key.startsWith('fp:mock:')) storage.remove(key.slice(3))
|
|
46
|
+
}
|
|
47
|
+
authStore.clear()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const localSetupService: SetupService = {
|
|
51
|
+
async needsSetup() {
|
|
52
|
+
return !localCredentials.any()
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async complete(input) {
|
|
56
|
+
await delay(300, 500)
|
|
57
|
+
if (localCredentials.any()) throw new ApiError(409, 'already_set_up')
|
|
58
|
+
clearLocalData()
|
|
59
|
+
const { firstName, lastName, email, password } = input.admin
|
|
60
|
+
const now = new Date().toISOString()
|
|
61
|
+
const owner: User = {
|
|
62
|
+
id: 'u_owner',
|
|
63
|
+
firstName,
|
|
64
|
+
lastName,
|
|
65
|
+
email: email.trim().toLowerCase(),
|
|
66
|
+
role: 'owner',
|
|
67
|
+
status: 'active',
|
|
68
|
+
createdAt: now,
|
|
69
|
+
lastActiveAt: now,
|
|
70
|
+
}
|
|
71
|
+
storage.set('mock:users', [owner])
|
|
72
|
+
saveCompany(input)
|
|
73
|
+
await localCredentials.set(owner.id, password)
|
|
74
|
+
startLocalSession(owner)
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const restSetupService: SetupService = {
|
|
79
|
+
async needsSetup() {
|
|
80
|
+
// A backend that creates its admin another way (seeder, CLI) simply doesn't have this endpoint.
|
|
81
|
+
try {
|
|
82
|
+
return (await http.get<{ needsSetup: boolean }>('/setup/status')).needsSetup
|
|
83
|
+
} catch {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async complete(input) {
|
|
89
|
+
await fetchCsrfCookie()
|
|
90
|
+
const { user, token, expiresAt } = await http.post<AuthResponse>('/setup', input)
|
|
91
|
+
saveCompany(input)
|
|
92
|
+
authStore.set({ user, token, expiresAt: expiresAt ?? Date.now() + 7 * 86_400_000 })
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const demoSetupService: SetupService = {
|
|
97
|
+
needsSetup: async () => false,
|
|
98
|
+
complete: async () => {},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const setupService: SetupService = isDemo ? demoSetupService : authMode === 'mock' ? localSetupService : restSetupService
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { demoSeed } from '@/core/api/demo'
|
|
1
2
|
import { ApiError } from '@/core/api/http'
|
|
2
3
|
import { createMockCollection, delay, notFound, paginate } from '@/core/api/mock'
|
|
3
4
|
import { auditService } from '@/core/audit/audit.service'
|
|
@@ -5,7 +6,6 @@ import type { ListParams, Paginated } from '@/core/api/types'
|
|
|
5
6
|
import { authStore } from '@/core/auth/auth-store'
|
|
6
7
|
import { canManageUser, type Role } from '@/core/auth/permissions'
|
|
7
8
|
import { fullName, type AssignableRole, type InviteInput, type ProfilePatch, type User, type UserFilters } from './types'
|
|
8
|
-
import { createUserSeed } from './users.mock'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Team members. Screens depend only on this interface; a real backend
|
|
@@ -37,7 +37,7 @@ export const userErrors = {
|
|
|
37
37
|
} as const
|
|
38
38
|
|
|
39
39
|
function createMockUsersService(): UsersService {
|
|
40
|
-
const db = createMockCollection('users',
|
|
40
|
+
const db = createMockCollection('users', () => demoSeed<() => User[]>('users')?.() ?? [])
|
|
41
41
|
const find = (id: string) => db.find(id) ?? notFound('User', id)
|
|
42
42
|
const nextId = () => `u_${String(db.all().length + 1).padStart(4, '0')}_${Date.now().toString(36)}`
|
|
43
43
|
const actor = () => authStore.getSession()?.user ?? null
|
|
@@ -7,7 +7,8 @@ export default {
|
|
|
7
7
|
email: 'Email',
|
|
8
8
|
password: 'Password',
|
|
9
9
|
submit: 'Continue',
|
|
10
|
-
demo: 'Demo mode: any password works. Sign in as owner@, admin@, editor@ or viewer@
|
|
10
|
+
demo: 'Demo mode: any password works. Sign in as owner@, admin@, editor@ or viewer@{{domain}} to try a role.',
|
|
11
|
+
invalid: 'The email or password is incorrect.',
|
|
11
12
|
welcome: 'Welcome, {{name}}',
|
|
12
13
|
failed: 'Sign in failed',
|
|
13
14
|
passwordRequired: 'Enter your password',
|
|
@@ -10,7 +10,7 @@ export default {
|
|
|
10
10
|
description: 'How your company appears in the app and in emails.',
|
|
11
11
|
name: 'Company name',
|
|
12
12
|
legalName: 'Legal name',
|
|
13
|
-
legalNameHint: '
|
|
13
|
+
legalNameHint: 'The full name as officially registered.',
|
|
14
14
|
},
|
|
15
15
|
tax: {
|
|
16
16
|
title: 'Tax information',
|
|
@@ -203,6 +203,8 @@ export default {
|
|
|
203
203
|
},
|
|
204
204
|
},
|
|
205
205
|
modules: {
|
|
206
|
+
emptyTitle: 'No modules yet',
|
|
207
|
+
emptyDescription: 'Features you add to this panel will be listed here.',
|
|
206
208
|
title: 'Modules',
|
|
207
209
|
description: 'Feature areas available in this workspace.',
|
|
208
210
|
note: 'Read-only for now. Modules are switched on in <code>src/app.config.ts</code>; this page will be editable once the backend stores the setting.',
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/** Dashboard home. */
|
|
2
2
|
export default {
|
|
3
|
+
readyDescription: 'Your panel is ready.',
|
|
4
|
+
noModules: {
|
|
5
|
+
title: 'Nothing here yet',
|
|
6
|
+
description: 'Features you add to this panel will appear here and in the sidebar.',
|
|
7
|
+
},
|
|
3
8
|
greeting: {
|
|
4
9
|
morning: 'Good morning, {{name}}',
|
|
5
10
|
afternoon: 'Good afternoon, {{name}}',
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** First-run setup wizard. */
|
|
2
|
+
export default {
|
|
3
|
+
title: 'Set up your panel',
|
|
4
|
+
progress: 'Step {{step}} of {{total}}',
|
|
5
|
+
next: 'Continue',
|
|
6
|
+
finish: 'Finish setup',
|
|
7
|
+
failed: 'Setup failed',
|
|
8
|
+
insecure: 'Open the panel on https or localhost to create an account.',
|
|
9
|
+
localNote: 'No backend connected yet: this account and its data are saved only in this browser. Connect a backend before going live.',
|
|
10
|
+
company: {
|
|
11
|
+
title: 'Your company',
|
|
12
|
+
description: 'This name appears across the panel. You can change everything later in Settings.',
|
|
13
|
+
name: 'Company name',
|
|
14
|
+
language: 'Language',
|
|
15
|
+
timeZone: 'Time zone',
|
|
16
|
+
currency: 'Currency',
|
|
17
|
+
},
|
|
18
|
+
admin: {
|
|
19
|
+
title: 'Your admin account',
|
|
20
|
+
description: 'You will be the Owner, with full access to the panel.',
|
|
21
|
+
firstName: 'First name',
|
|
22
|
+
lastName: 'Last name',
|
|
23
|
+
email: 'Email',
|
|
24
|
+
password: 'Password',
|
|
25
|
+
confirmPassword: 'Confirm password',
|
|
26
|
+
passwordWeak: 'This password does not meet the rules below',
|
|
27
|
+
passwordMismatch: 'The passwords do not match',
|
|
28
|
+
},
|
|
29
|
+
branding: {
|
|
30
|
+
title: 'Your look',
|
|
31
|
+
description: 'Pick an accent color and add a logo. The logo is optional.',
|
|
32
|
+
color: 'Accent color',
|
|
33
|
+
logo: 'Logo',
|
|
34
|
+
},
|
|
35
|
+
review: {
|
|
36
|
+
title: 'Check the details',
|
|
37
|
+
description: 'Everything look right?',
|
|
38
|
+
admin: 'Owner',
|
|
39
|
+
},
|
|
40
|
+
done: {
|
|
41
|
+
title: 'You are all set',
|
|
42
|
+
description: '{{name}} is ready to use.',
|
|
43
|
+
open: 'Open the panel',
|
|
44
|
+
},
|
|
45
|
+
}
|
|
@@ -13,26 +13,26 @@ import companyEn from './en/company'
|
|
|
13
13
|
import commonEn from './en/common'
|
|
14
14
|
import dashboardEn from './en/dashboard'
|
|
15
15
|
import errorsEn from './en/errors'
|
|
16
|
-
import landingEn from './en/landing'
|
|
17
16
|
import notificationsEn from './en/notifications'
|
|
18
17
|
import settingsEn from './en/settings'
|
|
18
|
+
import setupEn from './en/setup'
|
|
19
19
|
import accountTr from './tr/account'
|
|
20
20
|
import authTr from './tr/auth'
|
|
21
21
|
import companyTr from './tr/company'
|
|
22
22
|
import commonTr from './tr/common'
|
|
23
23
|
import dashboardTr from './tr/dashboard'
|
|
24
24
|
import errorsTr from './tr/errors'
|
|
25
|
-
import landingTr from './tr/landing'
|
|
26
25
|
import notificationsTr from './tr/notifications'
|
|
27
26
|
import settingsTr from './tr/settings'
|
|
27
|
+
import setupTr from './tr/setup'
|
|
28
28
|
|
|
29
29
|
const en = {
|
|
30
30
|
common: commonEn,
|
|
31
31
|
auth: authEn,
|
|
32
|
-
landing: landingEn,
|
|
33
32
|
errors: errorsEn,
|
|
34
33
|
dashboard: dashboardEn,
|
|
35
34
|
settings: settingsEn,
|
|
35
|
+
setup: setupEn,
|
|
36
36
|
account: accountEn,
|
|
37
37
|
company: companyEn,
|
|
38
38
|
notifications: notificationsEn,
|
|
@@ -42,10 +42,10 @@ const en = {
|
|
|
42
42
|
const tr: typeof en = {
|
|
43
43
|
common: commonTr,
|
|
44
44
|
auth: authTr,
|
|
45
|
-
landing: landingTr,
|
|
46
45
|
errors: errorsTr,
|
|
47
46
|
dashboard: dashboardTr,
|
|
48
47
|
settings: settingsTr,
|
|
48
|
+
setup: setupTr,
|
|
49
49
|
account: accountTr,
|
|
50
50
|
company: companyTr,
|
|
51
51
|
notifications: notificationsTr,
|
|
@@ -9,7 +9,8 @@ const auth: Messages<typeof en> = {
|
|
|
9
9
|
email: 'E-posta',
|
|
10
10
|
password: 'Şifre',
|
|
11
11
|
submit: 'Devam et',
|
|
12
|
-
demo: 'Demo modu: her şifre çalışır. Bir rolü denemek için owner@, admin@, editor@ veya viewer@
|
|
12
|
+
demo: 'Demo modu: her şifre çalışır. Bir rolü denemek için owner@, admin@, editor@ veya viewer@{{domain}} ile oturum açın.',
|
|
13
|
+
invalid: 'E-posta veya şifre hatalı.',
|
|
13
14
|
welcome: 'Hoş geldiniz, {{name}}',
|
|
14
15
|
failed: 'Oturum açılamadı',
|
|
15
16
|
passwordRequired: 'Şifrenizi girin',
|
|
@@ -12,7 +12,7 @@ const company: Messages<typeof en> = {
|
|
|
12
12
|
description: 'Şirketinizin uygulamada ve e-postalarda nasıl görüneceği.',
|
|
13
13
|
name: 'Şirket adı',
|
|
14
14
|
legalName: 'Ticari unvan',
|
|
15
|
-
legalNameHint: '
|
|
15
|
+
legalNameHint: 'Resmî olarak tescil edildiği tam ad.',
|
|
16
16
|
},
|
|
17
17
|
tax: {
|
|
18
18
|
title: 'Vergi bilgileri',
|
|
@@ -205,6 +205,8 @@ const company: Messages<typeof en> = {
|
|
|
205
205
|
},
|
|
206
206
|
},
|
|
207
207
|
modules: {
|
|
208
|
+
emptyTitle: 'Henüz modül yok',
|
|
209
|
+
emptyDescription: 'Bu panele eklediğiniz özellikler burada listelenecek.',
|
|
208
210
|
title: 'Modüller',
|
|
209
211
|
description: 'Bu çalışma alanında kullanılabilen özellik alanları.',
|
|
210
212
|
note: 'Şimdilik salt okunur. Modüller <code>src/app.config.ts</code> içinde açılır; arka uç bu ayarı sakladığında bu sayfa düzenlenebilir olacak.',
|
|
@@ -2,6 +2,11 @@ import type { Messages } from '@/core/i18n/types'
|
|
|
2
2
|
import type en from '../en/dashboard'
|
|
3
3
|
|
|
4
4
|
const dashboard: Messages<typeof en> = {
|
|
5
|
+
readyDescription: 'Paneliniz hazır.',
|
|
6
|
+
noModules: {
|
|
7
|
+
title: 'Henüz burada bir şey yok',
|
|
8
|
+
description: 'Bu panele eklediğiniz özellikler burada ve kenar çubuğunda görünecek.',
|
|
9
|
+
},
|
|
5
10
|
greeting: {
|
|
6
11
|
morning: 'Günaydın, {{name}}',
|
|
7
12
|
afternoon: 'İyi günler, {{name}}',
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Messages } from '@/core/i18n/types'
|
|
2
|
+
import type en from '../en/setup'
|
|
3
|
+
|
|
4
|
+
const tr: Messages<typeof en> = {
|
|
5
|
+
title: 'Panelinizi kurun',
|
|
6
|
+
progress: 'Adım {{step}} / {{total}}',
|
|
7
|
+
next: 'Devam et',
|
|
8
|
+
finish: 'Kurulumu tamamla',
|
|
9
|
+
failed: 'Kurulum tamamlanamadı',
|
|
10
|
+
insecure: 'Hesap oluşturmak için paneli https veya localhost üzerinden açın.',
|
|
11
|
+
localNote: 'Henüz bir sunucu bağlı değil: bu hesap ve verileri yalnızca bu tarayıcıda saklanır. Yayına almadan önce bir sunucu bağlayın.',
|
|
12
|
+
company: {
|
|
13
|
+
title: 'Şirketiniz',
|
|
14
|
+
description: 'Bu ad panelin her yerinde görünür. Her şeyi daha sonra Ayarlar’dan değiştirebilirsiniz.',
|
|
15
|
+
name: 'Şirket adı',
|
|
16
|
+
language: 'Dil',
|
|
17
|
+
timeZone: 'Saat dilimi',
|
|
18
|
+
currency: 'Para birimi',
|
|
19
|
+
},
|
|
20
|
+
admin: {
|
|
21
|
+
title: 'Yönetici hesabınız',
|
|
22
|
+
description: 'Panele tam erişimi olan Sahip siz olacaksınız.',
|
|
23
|
+
firstName: 'Ad',
|
|
24
|
+
lastName: 'Soyad',
|
|
25
|
+
email: 'E-posta',
|
|
26
|
+
password: 'Şifre',
|
|
27
|
+
confirmPassword: 'Şifreyi doğrulayın',
|
|
28
|
+
passwordWeak: 'Bu şifre aşağıdaki kuralları karşılamıyor',
|
|
29
|
+
passwordMismatch: 'Şifreler eşleşmiyor',
|
|
30
|
+
},
|
|
31
|
+
branding: {
|
|
32
|
+
title: 'Görünümünüz',
|
|
33
|
+
description: 'Bir vurgu rengi seçin ve logo ekleyin. Logo isteğe bağlıdır.',
|
|
34
|
+
color: 'Vurgu rengi',
|
|
35
|
+
logo: 'Logo',
|
|
36
|
+
},
|
|
37
|
+
review: {
|
|
38
|
+
title: 'Bilgileri kontrol edin',
|
|
39
|
+
description: 'Her şey doğru mu?',
|
|
40
|
+
admin: 'Sahip',
|
|
41
|
+
},
|
|
42
|
+
done: {
|
|
43
|
+
title: 'Her şey hazır',
|
|
44
|
+
description: '{{name}} kullanıma hazır.',
|
|
45
|
+
open: 'Paneli aç',
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default tr
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import productsEn from './products/locales/en'
|
|
2
|
-
import productsTr from './products/locales/tr'
|
|
3
1
|
// fp:locale-imports
|
|
4
2
|
|
|
5
3
|
/** Translations owned by modules; each key is the module's namespace. */
|
|
6
4
|
export const moduleLocalesEn = {
|
|
7
|
-
products: productsEn,
|
|
8
5
|
// fp:locale-en
|
|
9
6
|
}
|
|
10
7
|
|
|
11
8
|
export const moduleLocalesTr: typeof moduleLocalesEn = {
|
|
12
|
-
products: productsTr,
|
|
13
9
|
// fp:locale-tr
|
|
14
10
|
}
|
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import { appConfig } from '@/app.config'
|
|
2
|
-
import type { AppModule } from '@/core/modules/types'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
.filter((id) => appConfig.modules[id])
|
|
19
|
-
.map((id) => moduleRegistry[id])
|
|
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])
|