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.
Files changed (79) hide show
  1. package/README.md +24 -23
  2. package/bin/create-freakpunch.mjs +6 -6
  3. package/files/README.md +19 -15
  4. package/files/app.config.ts +50 -58
  5. package/files/locales.ts +10 -0
  6. package/files/registry.ts +17 -0
  7. package/package.json +23 -23
  8. package/template/BACKEND.md +108 -86
  9. package/template/CLAUDE.md +15 -10
  10. package/template/design/COMPONENTS.md +1 -1
  11. package/template/design/DESIGN.md +3 -3
  12. package/template/design/PATTERNS.md +5 -5
  13. package/template/index.html +1 -1
  14. package/template/src/app.config.ts +50 -61
  15. package/template/src/app.roles.ts +24 -24
  16. package/template/src/core/api/demo.ts +8 -0
  17. package/template/src/core/audit/audit.service.ts +3 -34
  18. package/template/src/core/auth/auth.service.ts +25 -14
  19. package/template/src/core/auth/local-credentials.ts +46 -0
  20. package/template/src/core/auth/permissions.ts +1 -1
  21. package/template/src/core/company/company.service.ts +14 -12
  22. package/template/src/core/company/use-app-name.ts +7 -0
  23. package/template/src/core/components/page.tsx +4 -3
  24. package/template/src/core/config/runtime.ts +16 -14
  25. package/template/src/core/config/types.ts +3 -2
  26. package/template/src/core/layout/app-sidebar.tsx +19 -2
  27. package/template/src/core/notifications/notifications.service.ts +10 -115
  28. package/template/src/core/notifications/types.ts +5 -0
  29. package/template/src/core/setup/setup-gate.tsx +20 -0
  30. package/template/src/core/setup/setup.service.ts +101 -0
  31. package/template/src/core/users/users.service.ts +2 -2
  32. package/template/src/locales/en/auth.ts +2 -1
  33. package/template/src/locales/en/common.ts +1 -0
  34. package/template/src/locales/en/company.ts +3 -1
  35. package/template/src/locales/en/dashboard.ts +5 -0
  36. package/template/src/locales/en/setup.ts +45 -0
  37. package/template/src/locales/index.ts +4 -4
  38. package/template/src/locales/tr/auth.ts +2 -1
  39. package/template/src/locales/tr/common.ts +1 -0
  40. package/template/src/locales/tr/company.ts +3 -1
  41. package/template/src/locales/tr/dashboard.ts +5 -0
  42. package/template/src/locales/tr/setup.ts +49 -0
  43. package/template/src/modules/locales.ts +0 -4
  44. package/template/src/modules/registry.ts +17 -19
  45. package/template/src/pages/dashboard/dashboard-page.tsx +28 -243
  46. package/template/src/pages/dashboard/quick-actions.tsx +37 -0
  47. package/template/src/pages/login-page.tsx +26 -14
  48. package/template/src/pages/not-found-page.tsx +1 -2
  49. package/template/src/pages/settings/account/account.service.ts +22 -41
  50. package/template/src/pages/settings/account/notifications-page.tsx +3 -3
  51. package/template/src/pages/settings/company/general-page.tsx +6 -4
  52. package/template/src/pages/settings/company/integrations/integrations.service.ts +8 -28
  53. package/template/src/pages/settings/company/modules-page.tsx +10 -2
  54. package/template/src/pages/settings/company/roles-page.tsx +2 -1
  55. package/template/src/pages/setup-page.tsx +387 -0
  56. package/template/src/router.tsx +9 -4
  57. package/template/tools/new-module.mjs +90 -90
  58. package/template/tools/templates/module/index.ts.tpl +44 -37
  59. package/template/tools/templates/module/locales/en.ts.tpl +51 -45
  60. package/template/tools/templates/module/locales/tr.ts.tpl +55 -49
  61. package/template/{src/modules/products/pages/product-detail-page.tsx → tools/templates/module/pages/__id__-detail-page.tsx.tpl} +103 -108
  62. package/template/tools/templates/module/pages/__id__-form-page.tsx.tpl +121 -121
  63. package/template/tools/templates/module/pages/__id__-list-page.tsx.tpl +234 -237
  64. package/template/vite.config.ts +6 -3
  65. package/template/src/core/users/users.mock.ts +0 -24
  66. package/template/src/locales/en/landing.ts +0 -15
  67. package/template/src/locales/tr/landing.ts +0 -19
  68. package/template/src/modules/products/api/products.mock.ts +0 -46
  69. package/template/src/modules/products/api/products.service.ts +0 -84
  70. package/template/src/modules/products/components/product-status.tsx +0 -21
  71. package/template/src/modules/products/hooks.ts +0 -73
  72. package/template/src/modules/products/index.ts +0 -50
  73. package/template/src/modules/products/locales/en.ts +0 -78
  74. package/template/src/modules/products/locales/tr.ts +0 -82
  75. package/template/src/modules/products/pages/product-form-page.tsx +0 -211
  76. package/template/src/modules/products/pages/products-list-page.tsx +0 -286
  77. package/template/src/modules/products/types.ts +0 -61
  78. package/template/src/pages/dashboard/dashboard.service.ts +0 -92
  79. package/template/src/pages/landing-page.tsx +0 -79
@@ -1,237 +1,234 @@
1
- import { useMemo, useState } from 'react'
2
- import { useTranslation } from 'react-i18next'
3
- import { Link, useNavigate } from 'react-router'
4
- import { createColumnHelper, type RowSelectionState } from '@tanstack/react-table'
5
- import { Download, MoreHorizontal, Pencil, Plus, Trash2 } from 'lucide-react'
6
- import { toast } from 'sonner'
7
-
8
- import { Button } from '@/components/ui/button'
9
- import {
10
- DropdownMenu,
11
- DropdownMenuContent,
12
- DropdownMenuItem,
13
- DropdownMenuSeparator,
14
- DropdownMenuTrigger,
15
- } from '@/components/ui/dropdown-menu'
16
- import { Can, usePermissions } from '@/core/auth/can'
17
- import { ButtonLink } from '@/core/components/button-link'
18
- import { useConfirm } from '@/core/components/confirm-dialog'
19
- import { DataTable } from '@/core/components/data-table/data-table'
20
- import type { DataTableFeatures } from '@/core/components/data-table/features'
21
- import { ClearFiltersButton, FilterSelect, TableSearch } from '@/core/components/data-table/toolbar'
22
- import { useListUrlState } from '@/core/components/data-table/use-list-url-state'
23
- import { Page } from '@/core/components/page'
24
- import { EmptyState } from '@/core/components/states'
25
- import { StatusBadge, type StatusTone } from '@/core/components/status-badge'
26
- import { useFormat } from '@/core/i18n/use-format'
27
- import { downloadCsv } from '@/core/lib/csv'
28
- import { useDelete__Name__, useExport__Name__, use__Name__List } from '../hooks'
29
- import { __id__Statuses, type __Name__Record, type __Name__Status } from '../types'
30
-
31
- const FILTER_KEYS = ['status'] as const
32
- const DEFAULT_SORT = { id: 'updatedAt', desc: true }
33
- const helper = createColumnHelper<DataTableFeatures, __Name__Record>()
34
- const tones: Record<__Name__Status, StatusTone> = { active: 'success', draft: 'neutral', archived: 'warning' }
35
-
36
- export default function __Name__ListPage() {
37
- const list = useListUrlState({ filterKeys: FILTER_KEYS, defaultSort: DEFAULT_SORT })
38
- const query = use__Name__List(list.listParams)
39
- const exportRows = useExport__Name__(list.listParams)
40
- const { mutateAsync: deleteAsync } = useDelete__Name__()
41
- const confirm = useConfirm()
42
- const navigate = useNavigate()
43
- const { t } = useTranslation(['__id__', 'common'])
44
- const fmt = useFormat()
45
- const { can } = usePermissions()
46
- const [selected, setSelected] = useState<RowSelectionState>({})
47
-
48
- async function onExport() {
49
- downloadCsv(
50
- '__id__',
51
- [
52
- { header: t('list.columns.name'), value: (r: __Name__Record) => r.name },
53
- { header: t('list.columns.status'), value: (r: __Name__Record) => t(`status.${r.status}`) },
54
- { header: t('list.columns.updated'), value: (r: __Name__Record) => fmt.date(r.updatedAt) },
55
- ],
56
- await exportRows(),
57
- )
58
- }
59
-
60
- async function onBulkDelete() {
61
- const ids = Object.keys(selected).filter((id) => selected[id])
62
- const ok = await confirm({
63
- title: t('delete.bulkTitle', { count: ids.length }),
64
- description: t('delete.description'),
65
- confirmLabel: t('common:actions.delete'),
66
- destructive: true,
67
- })
68
- if (!ok) return
69
- toast.promise(Promise.all(ids.map((id) => deleteAsync(id))), {
70
- loading: t('delete.deleting'),
71
- success: () => {
72
- setSelected({})
73
- return t('delete.bulkDeleted', { count: ids.length })
74
- },
75
- error: (e: Error) => e.message,
76
- })
77
- }
78
-
79
- const columns = useMemo(() => {
80
- async function onDelete(record: __Name__Record) {
81
- const ok = await confirm({
82
- title: t('delete.title', { name: record.name }),
83
- description: t('delete.description'),
84
- confirmLabel: t('common:actions.delete'),
85
- destructive: true,
86
- })
87
- if (!ok) return
88
- toast.promise(deleteAsync(record.id), {
89
- loading: t('delete.deleting'),
90
- success: t('delete.deleted', { name: record.name }),
91
- error: (e: Error) => e.message,
92
- })
93
- }
94
-
95
- return helper.columns([
96
- helper.accessor('name', {
97
- header: t('list.columns.name'),
98
- cell: ({ row }) =>
99
- can('__id__.edit') ? (
100
- <Link to={`${row.original.id}/edit`} className="font-medium outline-none hover:underline focus-visible:underline">
101
- {row.original.name}
102
- </Link>
103
- ) : (
104
- <span className="font-medium">{row.original.name}</span>
105
- ),
106
- }),
107
- helper.accessor('status', {
108
- header: t('list.columns.status'),
109
- cell: ({ getValue }) => <StatusBadge tone={tones[getValue()]}>{t(`status.${getValue()}`)}</StatusBadge>,
110
- }),
111
- helper.accessor('updatedAt', {
112
- header: t('list.columns.updated'),
113
- meta: { hideOnMobile: true, className: 'text-muted-foreground' },
114
- cell: ({ getValue }) => fmt.date(getValue()),
115
- }),
116
- helper.display({
117
- id: 'actions',
118
- header: () => <span className="sr-only">{t('common:table.actions')}</span>,
119
- meta: { align: 'right', className: 'w-12' },
120
- cell: ({ row }) =>
121
- (can('__id__.edit') || can('__id__.delete')) && (
122
- <DropdownMenu>
123
- <DropdownMenuTrigger
124
- render={
125
- <Button variant="ghost" size="icon-sm" aria-label={t('common:table.actionsFor', { name: row.original.name })}>
126
- <MoreHorizontal />
127
- </Button>
128
- }
129
- />
130
- <DropdownMenuContent align="end" className="w-44">
131
- {can('__id__.edit') && (
132
- <DropdownMenuItem onClick={() => navigate(`${row.original.id}/edit`)}>
133
- <Pencil />
134
- {t('common:actions.edit')}
135
- </DropdownMenuItem>
136
- )}
137
- {can('__id__.delete') && (
138
- <>
139
- <DropdownMenuSeparator />
140
- <DropdownMenuItem variant="destructive" onClick={() => onDelete(row.original)}>
141
- <Trash2 />
142
- {t('common:actions.delete')}
143
- </DropdownMenuItem>
144
- </>
145
- )}
146
- </DropdownMenuContent>
147
- </DropdownMenu>
148
- ),
149
- }),
150
- ])
151
- }, [confirm, deleteAsync, navigate, t, fmt, can])
152
-
153
- const statusOptions = __id__Statuses.map((s) => ({ value: s, label: t(`status.${s}`) }))
154
-
155
- return (
156
- <Page
157
- title={t('list.title')}
158
- description={t('list.description')}
159
- actions={
160
- <>
161
- <Button variant="outline" onClick={onExport}>
162
- <Download data-icon="inline-start" />
163
- {t('list.export')}
164
- </Button>
165
- <Can permission="__id__.create">
166
- <ButtonLink to="new">
167
- <Plus data-icon="inline-start" />
168
- {t('list.new')}
169
- </ButtonLink>
170
- </Can>
171
- </>
172
- }
173
- >
174
- <DataTable
175
- columns={columns}
176
- data={query.data?.items}
177
- total={query.data?.total ?? 0}
178
- getRowId={(r) => r.id}
179
- pagination={list.pagination}
180
- onPaginationChange={list.onPaginationChange}
181
- sorting={list.sorting}
182
- onSortingChange={list.onSortingChange}
183
- isLoading={query.isPending}
184
- isFetching={query.isFetching}
185
- error={query.error}
186
- onRetry={() => query.refetch()}
187
- noun={t('noun')}
188
- selection={
189
- can('__id__.delete')
190
- ? {
191
- value: selected,
192
- onChange: setSelected,
193
- actions: (
194
- <Button variant="destructive" size="sm" onClick={onBulkDelete}>
195
- <Trash2 data-icon="inline-start" />
196
- {t('common:actions.delete')}
197
- </Button>
198
- ),
199
- }
200
- : undefined
201
- }
202
- toolbar={
203
- <>
204
- <TableSearch value={list.search} onChange={list.setSearch} placeholder={t('list.search')} />
205
- <FilterSelect
206
- label={t('list.filterStatus')}
207
- allLabel={t('list.allStatuses')}
208
- value={list.filters.status}
209
- options={statusOptions}
210
- onChange={(v) => list.setFilter('status', v)}
211
- />
212
- {list.hasActiveFilters && <ClearFiltersButton onClick={list.clearAll} />}
213
- </>
214
- }
215
- empty={
216
- list.hasActiveFilters ? (
217
- <EmptyState
218
- title={t('list.noMatchesTitle')}
219
- description={t('list.noMatchesDescription')}
220
- action={<ClearFiltersButton onClick={list.clearAll} />}
221
- />
222
- ) : (
223
- <EmptyState
224
- title={t('list.emptyTitle')}
225
- description={t('list.emptyDescription')}
226
- action={
227
- <Can permission="__id__.create">
228
- <ButtonLink to="new">{t('list.new')}</ButtonLink>
229
- </Can>
230
- }
231
- />
232
- )
233
- }
234
- />
235
- </Page>
236
- )
237
- }
1
+ import { useMemo, useState } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import { Link, useNavigate } from 'react-router'
4
+ import { createColumnHelper, type RowSelectionState } from '@tanstack/react-table'
5
+ import { Download, MoreHorizontal, Pencil, Plus, Trash2 } from 'lucide-react'
6
+ import { toast } from 'sonner'
7
+
8
+ import { Button } from '@/components/ui/button'
9
+ import {
10
+ DropdownMenu,
11
+ DropdownMenuContent,
12
+ DropdownMenuItem,
13
+ DropdownMenuSeparator,
14
+ DropdownMenuTrigger,
15
+ } from '@/components/ui/dropdown-menu'
16
+ import { Can, usePermissions } from '@/core/auth/can'
17
+ import { ButtonLink } from '@/core/components/button-link'
18
+ import { useConfirm } from '@/core/components/confirm-dialog'
19
+ import { DataTable } from '@/core/components/data-table/data-table'
20
+ import type { DataTableFeatures } from '@/core/components/data-table/features'
21
+ import { ClearFiltersButton, FilterSelect, TableSearch } from '@/core/components/data-table/toolbar'
22
+ import { useListUrlState } from '@/core/components/data-table/use-list-url-state'
23
+ import { Page } from '@/core/components/page'
24
+ import { EmptyState } from '@/core/components/states'
25
+ import { StatusBadge, type StatusTone } from '@/core/components/status-badge'
26
+ import { useFormat } from '@/core/i18n/use-format'
27
+ import { downloadCsv } from '@/core/lib/csv'
28
+ import { useDelete__Name__, useExport__Name__, use__Name__List } from '../hooks'
29
+ import { __id__Statuses, type __Name__Record, type __Name__Status } from '../types'
30
+
31
+ const FILTER_KEYS = ['status'] as const
32
+ const DEFAULT_SORT = { id: 'updatedAt', desc: true }
33
+ const helper = createColumnHelper<DataTableFeatures, __Name__Record>()
34
+ const tones: Record<__Name__Status, StatusTone> = { active: 'success', draft: 'neutral', archived: 'warning' }
35
+
36
+ export default function __Name__ListPage() {
37
+ const list = useListUrlState({ filterKeys: FILTER_KEYS, defaultSort: DEFAULT_SORT })
38
+ const query = use__Name__List(list.listParams)
39
+ const exportRows = useExport__Name__(list.listParams)
40
+ const { mutateAsync: deleteAsync } = useDelete__Name__()
41
+ const confirm = useConfirm()
42
+ const navigate = useNavigate()
43
+ const { t } = useTranslation(['__id__', 'common'])
44
+ const fmt = useFormat()
45
+ const { can } = usePermissions()
46
+ const [selected, setSelected] = useState<RowSelectionState>({})
47
+
48
+ async function onExport() {
49
+ downloadCsv(
50
+ '__id__',
51
+ [
52
+ { header: t('list.columns.name'), value: (r: __Name__Record) => r.name },
53
+ { header: t('list.columns.status'), value: (r: __Name__Record) => t(`status.${r.status}`) },
54
+ { header: t('list.columns.updated'), value: (r: __Name__Record) => fmt.date(r.updatedAt) },
55
+ ],
56
+ await exportRows(),
57
+ )
58
+ }
59
+
60
+ async function onBulkDelete() {
61
+ const ids = Object.keys(selected).filter((id) => selected[id])
62
+ const ok = await confirm({
63
+ title: t('delete.bulkTitle', { count: ids.length }),
64
+ description: t('delete.description'),
65
+ confirmLabel: t('common:actions.delete'),
66
+ destructive: true,
67
+ })
68
+ if (!ok) return
69
+ toast.promise(Promise.all(ids.map((id) => deleteAsync(id))), {
70
+ loading: t('delete.deleting'),
71
+ success: () => {
72
+ setSelected({})
73
+ return t('delete.bulkDeleted', { count: ids.length })
74
+ },
75
+ error: (e: Error) => e.message,
76
+ })
77
+ }
78
+
79
+ const columns = useMemo(() => {
80
+ async function onDelete(record: __Name__Record) {
81
+ const ok = await confirm({
82
+ title: t('delete.title', { name: record.name }),
83
+ description: t('delete.description'),
84
+ confirmLabel: t('common:actions.delete'),
85
+ destructive: true,
86
+ })
87
+ if (!ok) return
88
+ toast.promise(deleteAsync(record.id), {
89
+ loading: t('delete.deleting'),
90
+ success: t('delete.deleted', { name: record.name }),
91
+ error: (e: Error) => e.message,
92
+ })
93
+ }
94
+
95
+ return helper.columns([
96
+ helper.accessor('name', {
97
+ header: t('list.columns.name'),
98
+ cell: ({ row }) => (
99
+ <Link to={row.original.id} className="font-medium outline-none hover:underline focus-visible:underline">
100
+ {row.original.name}
101
+ </Link>
102
+ ),
103
+ }),
104
+ helper.accessor('status', {
105
+ header: t('list.columns.status'),
106
+ cell: ({ getValue }) => <StatusBadge tone={tones[getValue()]}>{t(`status.${getValue()}`)}</StatusBadge>,
107
+ }),
108
+ helper.accessor('updatedAt', {
109
+ header: t('list.columns.updated'),
110
+ meta: { hideOnMobile: true, className: 'text-muted-foreground' },
111
+ cell: ({ getValue }) => fmt.date(getValue()),
112
+ }),
113
+ helper.display({
114
+ id: 'actions',
115
+ header: () => <span className="sr-only">{t('common:table.actions')}</span>,
116
+ meta: { align: 'right', className: 'w-12' },
117
+ cell: ({ row }) =>
118
+ (can('__id__.edit') || can('__id__.delete')) && (
119
+ <DropdownMenu>
120
+ <DropdownMenuTrigger
121
+ render={
122
+ <Button variant="ghost" size="icon-sm" aria-label={t('common:table.actionsFor', { name: row.original.name })}>
123
+ <MoreHorizontal />
124
+ </Button>
125
+ }
126
+ />
127
+ <DropdownMenuContent align="end" className="w-44">
128
+ {can('__id__.edit') && (
129
+ <DropdownMenuItem onClick={() => navigate(`${row.original.id}/edit`)}>
130
+ <Pencil />
131
+ {t('common:actions.edit')}
132
+ </DropdownMenuItem>
133
+ )}
134
+ {can('__id__.delete') && (
135
+ <>
136
+ <DropdownMenuSeparator />
137
+ <DropdownMenuItem variant="destructive" onClick={() => onDelete(row.original)}>
138
+ <Trash2 />
139
+ {t('common:actions.delete')}
140
+ </DropdownMenuItem>
141
+ </>
142
+ )}
143
+ </DropdownMenuContent>
144
+ </DropdownMenu>
145
+ ),
146
+ }),
147
+ ])
148
+ }, [confirm, deleteAsync, navigate, t, fmt, can])
149
+
150
+ const statusOptions = __id__Statuses.map((s) => ({ value: s, label: t(`status.${s}`) }))
151
+
152
+ return (
153
+ <Page
154
+ title={t('list.title')}
155
+ description={t('list.description')}
156
+ actions={
157
+ <>
158
+ <Button variant="outline" onClick={onExport}>
159
+ <Download data-icon="inline-start" />
160
+ {t('list.export')}
161
+ </Button>
162
+ <Can permission="__id__.create">
163
+ <ButtonLink to="new">
164
+ <Plus data-icon="inline-start" />
165
+ {t('list.new')}
166
+ </ButtonLink>
167
+ </Can>
168
+ </>
169
+ }
170
+ >
171
+ <DataTable
172
+ columns={columns}
173
+ data={query.data?.items}
174
+ total={query.data?.total ?? 0}
175
+ getRowId={(r) => r.id}
176
+ pagination={list.pagination}
177
+ onPaginationChange={list.onPaginationChange}
178
+ sorting={list.sorting}
179
+ onSortingChange={list.onSortingChange}
180
+ isLoading={query.isPending}
181
+ isFetching={query.isFetching}
182
+ error={query.error}
183
+ onRetry={() => query.refetch()}
184
+ noun={t('noun')}
185
+ selection={
186
+ can('__id__.delete')
187
+ ? {
188
+ value: selected,
189
+ onChange: setSelected,
190
+ actions: (
191
+ <Button variant="destructive" size="sm" onClick={onBulkDelete}>
192
+ <Trash2 data-icon="inline-start" />
193
+ {t('common:actions.delete')}
194
+ </Button>
195
+ ),
196
+ }
197
+ : undefined
198
+ }
199
+ toolbar={
200
+ <>
201
+ <TableSearch value={list.search} onChange={list.setSearch} placeholder={t('list.search')} />
202
+ <FilterSelect
203
+ label={t('list.filterStatus')}
204
+ allLabel={t('list.allStatuses')}
205
+ value={list.filters.status}
206
+ options={statusOptions}
207
+ onChange={(v) => list.setFilter('status', v)}
208
+ />
209
+ {list.hasActiveFilters && <ClearFiltersButton onClick={list.clearAll} />}
210
+ </>
211
+ }
212
+ empty={
213
+ list.hasActiveFilters ? (
214
+ <EmptyState
215
+ title={t('list.noMatchesTitle')}
216
+ description={t('list.noMatchesDescription')}
217
+ action={<ClearFiltersButton onClick={list.clearAll} />}
218
+ />
219
+ ) : (
220
+ <EmptyState
221
+ title={t('list.emptyTitle')}
222
+ description={t('list.emptyDescription')}
223
+ action={
224
+ <Can permission="__id__.create">
225
+ <ButtonLink to="new">{t('list.new')}</ButtonLink>
226
+ </Can>
227
+ }
228
+ />
229
+ )
230
+ }
231
+ />
232
+ </Page>
233
+ )
234
+ }
@@ -19,9 +19,12 @@ export default defineConfig(({ mode }) => {
19
19
  base: env.VITE_BASE_PATH || '/',
20
20
  plugins: [react(), tailwindcss()],
21
21
  /** Dev only: forward API calls to the project's backend, e.g. VITE_DEV_API_PROXY=http://localhost:8000 */
22
- server: env.VITE_DEV_API_PROXY
23
- ? { proxy: { [env.VITE_API_URL || '/api']: { target: env.VITE_DEV_API_PROXY, changeOrigin: true } } }
24
- : undefined,
22
+ server: {
23
+ port: Number(env.VITE_DEV_PORT) || undefined,
24
+ proxy: env.VITE_DEV_API_PROXY
25
+ ? { [env.VITE_API_URL || '/api']: { target: env.VITE_DEV_API_PROXY, changeOrigin: true } }
26
+ : undefined,
27
+ },
25
28
  resolve: {
26
29
  alias: {
27
30
  '@': path.resolve(import.meta.dirname, './src'),
@@ -1,24 +0,0 @@
1
- import type { User } from './types'
2
-
3
- const day = 86_400_000
4
-
5
- /**
6
- * Mock team. The first four are the demo accounts, one per role:
7
- * owner@, admin@, editor@, viewer@freakpunch.site (any password).
8
- */
9
- export function createUserSeed(): User[] {
10
- const now = Date.now()
11
- const at = (daysAgo: number) => new Date(now - daysAgo * day).toISOString()
12
- return [
13
- { id: 'u_owner', firstName: 'Deniz', lastName: 'Aydın', email: 'owner@freakpunch.site', phone: '+905321234567', jobTitle: 'Founder', role: 'owner', status: 'active', createdAt: at(420), lastActiveAt: at(0) },
14
- { id: 'u_admin', firstName: 'Maya', lastName: 'Chen', email: 'admin@freakpunch.site', jobTitle: 'Operations lead', role: 'admin', status: 'active', createdAt: at(380), lastActiveAt: at(0.1) },
15
- { id: 'u_editor', firstName: 'Jonas', lastName: 'Weber', email: 'editor@freakpunch.site', jobTitle: 'Catalog editor', role: 'editor', status: 'active', createdAt: at(300), lastActiveAt: at(0.4) },
16
- { id: 'u_viewer', firstName: 'Ana', lastName: 'López', email: 'viewer@freakpunch.site', jobTitle: 'Analyst', role: 'viewer', status: 'active', createdAt: at(210), lastActiveAt: at(2) },
17
- { id: 'u_0005', firstName: 'Elif', lastName: 'Şahin', email: 'elif.sahin@freakpunch.site', jobTitle: 'Content editor', role: 'editor', status: 'active', createdAt: at(160), lastActiveAt: at(1) },
18
- { id: 'u_0006', firstName: 'Can', lastName: 'Yılmaz', email: 'can.yilmaz@freakpunch.site', jobTitle: 'Engineering', role: 'admin', status: 'active', createdAt: at(140), lastActiveAt: at(3) },
19
- { id: 'u_0007', firstName: 'Sofia', lastName: 'Rossi', email: 'sofia.rossi@freakpunch.site', jobTitle: 'Merchandiser', role: 'editor', status: 'deactivated', createdAt: at(120), lastActiveAt: at(45) },
20
- { id: 'u_0008', firstName: 'Burak', lastName: 'Öztürk', email: 'burak.ozturk@freakpunch.site', jobTitle: 'Support', role: 'viewer', status: 'active', createdAt: at(90), lastActiveAt: at(6) },
21
- { id: 'u_0009', firstName: '', lastName: '', email: 'mert.kaya@freakpunch.site', role: 'viewer', status: 'invited', createdAt: at(4) },
22
- { id: 'u_0010', firstName: '', lastName: '', email: 'lena.fischer@freakpunch.site', role: 'editor', status: 'invited', createdAt: at(1) },
23
- ]
24
- }
@@ -1,15 +0,0 @@
1
- /** Public landing page. */
2
- export default {
3
- headline: 'One admin panel.',
4
- headlineMuted: 'Every project.',
5
- lead: 'A calm, fast starting point for e-commerce, publishing and booking back offices.',
6
- signIn: 'Sign in',
7
- openApp: 'Open app',
8
- openDashboard: 'Open dashboard',
9
- features: {
10
- modular: { title: 'Modular', text: 'Switch modules on per project from one config file.' },
11
- tokens: { title: 'One token source', text: 'Colors, radius and shadows live in CSS variables.' },
12
- api: { title: 'API-ready', text: 'Typed services today return mocks, tomorrow your REST API.' },
13
- },
14
- copyright: '© {{year}} {{name}}',
15
- }
@@ -1,19 +0,0 @@
1
- import type { Messages } from '@/core/i18n/types'
2
- import type en from '../en/landing'
3
-
4
- const landing: Messages<typeof en> = {
5
- headline: 'Tek yönetim paneli.',
6
- headlineMuted: 'Her proje için.',
7
- lead: 'E-ticaret, yayıncılık ve rezervasyon arka ofisleri için sakin ve hızlı bir başlangıç noktası.',
8
- signIn: 'Oturum aç',
9
- openApp: 'Uygulamayı aç',
10
- openDashboard: 'Panoyu aç',
11
- features: {
12
- modular: { title: 'Modüler', text: 'Modülleri her proje için tek bir yapılandırma dosyasından açın.' },
13
- tokens: { title: 'Tek token kaynağı', text: 'Renkler, köşe yuvarlaklıkları ve gölgeler CSS değişkenlerinde.' },
14
- api: { title: 'API’ye hazır', text: 'Tipli servisler bugün örnek veri, yarın REST API’nizi döndürür.' },
15
- },
16
- copyright: '© {{year}} {{name}}',
17
- }
18
-
19
- export default landing
@@ -1,46 +0,0 @@
1
- import { seededRandom } from '@/core/api/mock'
2
- import { productCategories, type Product, type ProductStatus } from '../types'
3
-
4
- const names: Record<(typeof productCategories)[number], string[]> = {
5
- Lighting: ['Arc Floor Lamp', 'Halo Pendant', 'Dune Table Lamp', 'Orbit Wall Light', 'Cone Desk Lamp', 'Glow Strip'],
6
- Furniture: ['Oak Side Table', 'Linen Lounge Chair', 'Walnut Shelf', 'Fold Stool', 'Slab Coffee Table', 'Nook Bench'],
7
- Decor: ['Stoneware Vase', 'Brass Candle Holder', 'Woven Basket', 'Round Mirror', 'Clay Planter', 'Frame Set'],
8
- Kitchen: ['Ceramic Mug', 'Pour-over Kettle', 'Ash Cutting Board', 'Glass Carafe', 'Steel Knife Set', 'Salad Bowl'],
9
- Textiles: ['Wool Throw', 'Linen Cushion', 'Jute Rug', 'Cotton Napkins', 'Waffle Towel', 'Velvet Pillow'],
10
- }
11
-
12
- const variants = ['', ' Mini', ' Large', ' Natural', ' Black', ' Sand']
13
-
14
- /** 64 deterministic products. */
15
- export function createProductSeed(): Product[] {
16
- const rand = seededRandom(42)
17
- const now = Date.now()
18
- const products: Product[] = []
19
- let n = 1
20
-
21
- for (const variant of variants) {
22
- for (const category of productCategories) {
23
- for (const base of names[category]) {
24
- if (products.length >= 64) break
25
- if (rand() > 0.55) continue
26
- const statusRoll = rand()
27
- const status: ProductStatus = statusRoll > 0.85 ? 'archived' : statusRoll > 0.72 ? 'draft' : 'active'
28
- const created = now - Math.floor(rand() * 180) * 86_400_000
29
- products.push({
30
- id: `p_${String(n).padStart(4, '0')}`,
31
- name: base + variant,
32
- sku: `${category.slice(0, 3).toUpperCase()}-${String(1000 + n)}`,
33
- category,
34
- price: Math.round((9 + rand() * 480) * 100) / 100,
35
- stock: rand() > 0.85 ? Math.floor(rand() * 8) : Math.floor(rand() * 240),
36
- status,
37
- description: `${base}${variant} from our ${category.toLowerCase()} collection. Designed to be simple, durable and easy to live with.`,
38
- createdAt: new Date(created).toISOString(),
39
- updatedAt: new Date(created + Math.floor(rand() * (now - created))).toISOString(),
40
- })
41
- n++
42
- }
43
- }
44
- }
45
- return products
46
- }