create-mercato-app 0.6.6-develop.6364.1.b26488823f → 0.6.6-develop.6366.1.fba69a7362

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mercato-app",
3
- "version": "0.6.6-develop.6364.1.b26488823f",
3
+ "version": "0.6.6-develop.6366.1.fba69a7362",
4
4
  "type": "module",
5
5
  "description": "Create a new Open Mercato application",
6
6
  "main": "./dist/index.js",
@@ -1,165 +1,18 @@
1
- import { modules } from '@/.mercato/generated/modules.app.generated'
2
- import { frontendRoutes } from '@/.mercato/generated/frontend-routes.generated'
3
- import { backendRoutes } from '@/.mercato/generated/backend-routes.generated'
4
- import { apiRoutes } from '@/.mercato/generated/api-routes.generated'
5
- import { StartPageContent } from '@/components/StartPageContent'
6
- import type { Metadata } from 'next'
7
- import { resolveLocalizedAppMetadata } from '@/lib/metadata'
8
1
  import { cookies } from 'next/headers'
9
- import Image from 'next/image'
10
- import Link from 'next/link'
11
- import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
12
- import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
13
- import type { EntityManager } from '@mikro-orm/postgresql'
14
- import { User } from '@open-mercato/core/modules/auth/data/entities'
15
- import { Tenant, Organization } from '@open-mercato/core/modules/directory/data/entities'
16
- import { buildHomeQuickLinks } from '@/lib/homeQuickLinks'
17
- import { Fragment } from 'react'
18
-
19
- function FeatureBadge({ label }: { label: string }) {
20
- return (
21
- <span className="inline-flex items-center rounded border px-2 py-0.5 text-xs text-muted-foreground">
22
- {label}
23
- </span>
24
- )
25
- }
26
-
27
- export async function generateMetadata(): Promise<Metadata> {
28
- return resolveLocalizedAppMetadata()
29
- }
30
-
31
- const routeCountsByModule = modules.reduce((map, module) => {
32
- map.set(module.id, { frontend: 0, backend: 0, api: 0 })
33
- return map
34
- }, new Map<string, { frontend: number; backend: number; api: number }>())
35
-
36
- for (const route of frontendRoutes) {
37
- const entry = routeCountsByModule.get(route.moduleId)
38
- if (entry) entry.frontend += 1
39
- }
40
-
41
- for (const route of backendRoutes) {
42
- const entry = routeCountsByModule.get(route.moduleId)
43
- if (entry) entry.backend += 1
44
- }
45
-
46
- for (const route of apiRoutes) {
47
- const entry = routeCountsByModule.get(route.moduleId)
48
- if (entry) entry.api += route.methods.length
49
- }
2
+ import { redirect } from 'next/navigation'
3
+ import { getAuthFromCookies } from '@open-mercato/shared/lib/auth/server'
50
4
 
5
+ // The home route is a pure router: it never renders. Unless the visitor has
6
+ // dismissed the start page, it sends them there; otherwise into the app
7
+ // (backend when authenticated, login otherwise).
51
8
  export default async function Home() {
52
- const { t } = await resolveTranslations()
53
- const quickLinks = buildHomeQuickLinks(modules)
54
-
55
- // Check if user wants to see the start page
56
9
  const cookieStore = await cookies()
57
- const showStartPageCookie = cookieStore.get('show_start_page')
58
- const showStartPage = showStartPageCookie?.value !== 'false'
10
+ const startPageDismissed = cookieStore.get('start_page_dismissed')?.value === '1'
59
11
 
60
- // Database status and counts
61
- let dbStatus = t('app.page.dbStatus.unknown', 'Unknown')
62
- let usersCount = 0
63
- let tenantsCount = 0
64
- let orgsCount = 0
65
- try {
66
- const container = await createRequestContainer()
67
- const em = container.resolve<EntityManager>('em')
68
- usersCount = await em.count(User, {})
69
- tenantsCount = await em.count(Tenant, {})
70
- orgsCount = await em.count(Organization, {})
71
- dbStatus = t('app.page.dbStatus.connected', 'Connected')
72
- } catch (error: unknown) {
73
- const message = error instanceof Error ? error.message : t('app.page.dbStatus.noConnection', 'no connection')
74
- dbStatus = t('app.page.dbStatus.error', 'Error: {message}', { message })
12
+ if (!startPageDismissed) {
13
+ redirect('/start')
75
14
  }
76
15
 
77
- const onboardingAvailable =
78
- process.env.SELF_SERVICE_ONBOARDING_ENABLED === 'true' &&
79
- Boolean(process.env.RESEND_API_KEY && process.env.RESEND_API_KEY.trim()) &&
80
- Boolean(process.env.APP_URL && process.env.APP_URL.trim())
81
-
82
- return (
83
- <main className="min-h-svh w-full p-8 flex flex-col gap-8">
84
- <header className="flex flex-col md:flex-row md:items-center gap-4 md:gap-6">
85
- <Image
86
- src="/open-mercato.svg"
87
- alt={t('app.page.logoAlt', 'Open Mercato')}
88
- width={40}
89
- height={40}
90
- priority
91
- />
92
- <div className="flex-1">
93
- <h1 className="text-3xl font-semibold tracking-tight">{t('app.page.title', 'Open Mercato')}</h1>
94
- <p className="text-sm text-muted-foreground">{t('app.page.subtitle', 'AI‑supportive, modular ERP foundation for product & service companies')}</p>
95
- </div>
96
- </header>
97
-
98
- <StartPageContent showStartPage={showStartPage} showOnboardingCta={onboardingAvailable} />
99
-
100
- <section className="grid grid-cols-1 md:grid-cols-3 gap-6">
101
- <div className="rounded-lg border bg-card p-4">
102
- <div className="text-sm font-medium mb-2">{t('app.page.dbStatus.title', 'Database Status')}</div>
103
- <div className="text-sm text-muted-foreground">{t('app.page.dbStatus.label', 'Status:')} <span className="font-medium text-foreground">{dbStatus}</span></div>
104
- <div className="mt-3 space-y-1.5 text-sm">
105
- <div className="flex justify-between">
106
- <span className="text-muted-foreground">{t('app.page.dbStatus.users', 'Users:')}</span>
107
- <span className="font-mono font-medium">{usersCount}</span>
108
- </div>
109
- <div className="flex justify-between">
110
- <span className="text-muted-foreground">{t('app.page.dbStatus.tenants', 'Tenants:')}</span>
111
- <span className="font-mono font-medium">{tenantsCount}</span>
112
- </div>
113
- <div className="flex justify-between">
114
- <span className="text-muted-foreground">{t('app.page.dbStatus.organizations', 'Organizations:')}</span>
115
- <span className="font-mono font-medium">{orgsCount}</span>
116
- </div>
117
- </div>
118
- </div>
119
-
120
- <div className="rounded-lg border bg-card p-4 md:col-span-2">
121
- <div className="text-sm font-medium mb-3">{t('app.page.activeModules.title', 'Active Modules')}</div>
122
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 max-h-[200px] overflow-y-auto pr-2">
123
- {modules.map((m) => {
124
- const counts = routeCountsByModule.get(m.id) ?? { frontend: 0, backend: 0, api: 0 }
125
- const fe = counts.frontend
126
- const be = counts.backend
127
- const api = counts.api
128
- const i18n = m.translations ? Object.keys(m.translations).length : 0
129
- return (
130
- <div key={m.id} className="rounded border p-3 bg-background">
131
- <div className="text-sm font-medium">{m.info?.title || m.id}{m.info?.version ? <span className="ml-2 text-xs text-muted-foreground">v{m.info.version}</span> : null}</div>
132
- {m.info?.description ? <div className="text-xs text-muted-foreground mt-1 line-clamp-2">{m.info.description}</div> : null}
133
- <div className="mt-2 flex flex-wrap gap-1">
134
- {fe ? <FeatureBadge label={`FE:${fe}`} /> : null}
135
- {be ? <FeatureBadge label={`BE:${be}`} /> : null}
136
- {api ? <FeatureBadge label={`API:${api}`} /> : null}
137
- {i18n ? <FeatureBadge label={`i18n:${i18n}`} /> : null}
138
- </div>
139
- </div>
140
- )
141
- })}
142
- </div>
143
- </div>
144
- </section>
145
-
146
- <section className="rounded-lg border bg-card p-4">
147
- <div className="text-sm font-medium mb-2">{t('app.page.quickLinks.title', 'Quick Links')}</div>
148
- <div className="flex flex-wrap items-center gap-3 text-sm">
149
- {quickLinks.map((link, index) => (
150
- <Fragment key={link.href}>
151
- {index > 0 ? <span className="text-muted-foreground">·</span> : null}
152
- <Link className="underline hover:text-primary transition-colors" href={link.href}>
153
- {t(link.translationKey, link.fallbackLabel)}
154
- </Link>
155
- </Fragment>
156
- ))}
157
- </div>
158
- </section>
159
-
160
- <footer className="text-xs text-muted-foreground text-center">
161
- {t('app.page.footer', 'Built with Next.js, MikroORM, and Awilix — modular by design.')}
162
- </footer>
163
- </main>
164
- )
16
+ const auth = await getAuthFromCookies()
17
+ redirect(auth ? '/backend' : '/login')
165
18
  }
@@ -0,0 +1,165 @@
1
+ import { modules } from '@/.mercato/generated/modules.app.generated'
2
+ import { frontendRoutes } from '@/.mercato/generated/frontend-routes.generated'
3
+ import { backendRoutes } from '@/.mercato/generated/backend-routes.generated'
4
+ import { apiRoutes } from '@/.mercato/generated/api-routes.generated'
5
+ import { StartPageContent } from '@/components/StartPageContent'
6
+ import type { Metadata } from 'next'
7
+ import { resolveLocalizedAppMetadata } from '@/lib/metadata'
8
+ import { cookies } from 'next/headers'
9
+ import Image from 'next/image'
10
+ import Link from 'next/link'
11
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
12
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
13
+ import type { EntityManager } from '@mikro-orm/postgresql'
14
+ import { User } from '@open-mercato/core/modules/auth/data/entities'
15
+ import { Tenant, Organization } from '@open-mercato/core/modules/directory/data/entities'
16
+ import { buildHomeQuickLinks } from '@/lib/homeQuickLinks'
17
+ import { Fragment } from 'react'
18
+
19
+ function FeatureBadge({ label }: { label: string }) {
20
+ return (
21
+ <span className="inline-flex items-center rounded border px-2 py-0.5 text-xs text-muted-foreground">
22
+ {label}
23
+ </span>
24
+ )
25
+ }
26
+
27
+ export async function generateMetadata(): Promise<Metadata> {
28
+ return resolveLocalizedAppMetadata()
29
+ }
30
+
31
+ const routeCountsByModule = modules.reduce((map, module) => {
32
+ map.set(module.id, { frontend: 0, backend: 0, api: 0 })
33
+ return map
34
+ }, new Map<string, { frontend: number; backend: number; api: number }>())
35
+
36
+ for (const route of frontendRoutes) {
37
+ const entry = routeCountsByModule.get(route.moduleId)
38
+ if (entry) entry.frontend += 1
39
+ }
40
+
41
+ for (const route of backendRoutes) {
42
+ const entry = routeCountsByModule.get(route.moduleId)
43
+ if (entry) entry.backend += 1
44
+ }
45
+
46
+ for (const route of apiRoutes) {
47
+ const entry = routeCountsByModule.get(route.moduleId)
48
+ if (entry) entry.api += route.methods.length
49
+ }
50
+
51
+ export default async function StartPage() {
52
+ const { t } = await resolveTranslations()
53
+ const quickLinks = buildHomeQuickLinks(modules)
54
+
55
+ // The checkbox reflects whether this start page is the default landing (the
56
+ // `/` router honors the dismissal cookie); visiting /start always renders it.
57
+ const cookieStore = await cookies()
58
+ const showStartPage = cookieStore.get('start_page_dismissed')?.value !== '1'
59
+
60
+ // Database status and counts
61
+ let dbStatus = t('app.page.dbStatus.unknown', 'Unknown')
62
+ let usersCount = 0
63
+ let tenantsCount = 0
64
+ let orgsCount = 0
65
+ try {
66
+ const container = await createRequestContainer()
67
+ const em = container.resolve<EntityManager>('em')
68
+ usersCount = await em.count(User, {})
69
+ tenantsCount = await em.count(Tenant, {})
70
+ orgsCount = await em.count(Organization, {})
71
+ dbStatus = t('app.page.dbStatus.connected', 'Connected')
72
+ } catch (error: unknown) {
73
+ const message = error instanceof Error ? error.message : t('app.page.dbStatus.noConnection', 'no connection')
74
+ dbStatus = t('app.page.dbStatus.error', 'Error: {message}', { message })
75
+ }
76
+
77
+ const onboardingAvailable =
78
+ process.env.SELF_SERVICE_ONBOARDING_ENABLED === 'true' &&
79
+ Boolean(process.env.RESEND_API_KEY && process.env.RESEND_API_KEY.trim()) &&
80
+ Boolean(process.env.APP_URL && process.env.APP_URL.trim())
81
+
82
+ return (
83
+ <main className="min-h-svh w-full p-8 flex flex-col gap-8">
84
+ <header className="flex flex-col md:flex-row md:items-center gap-4 md:gap-6">
85
+ <Image
86
+ src="/open-mercato.svg"
87
+ alt={t('app.page.logoAlt', 'Open Mercato')}
88
+ width={40}
89
+ height={40}
90
+ priority
91
+ />
92
+ <div className="flex-1">
93
+ <h1 className="text-3xl font-semibold tracking-tight">{t('app.page.title', 'Open Mercato')}</h1>
94
+ <p className="text-sm text-muted-foreground">{t('app.page.subtitle', 'AI‑supportive, modular ERP foundation for product & service companies')}</p>
95
+ </div>
96
+ </header>
97
+
98
+ <StartPageContent showStartPage={showStartPage} showOnboardingCta={onboardingAvailable} />
99
+
100
+ <section className="grid grid-cols-1 md:grid-cols-3 gap-6">
101
+ <div className="rounded-lg border bg-card p-4">
102
+ <div className="text-sm font-medium mb-2">{t('app.page.dbStatus.title', 'Database Status')}</div>
103
+ <div className="text-sm text-muted-foreground">{t('app.page.dbStatus.label', 'Status:')} <span className="font-medium text-foreground">{dbStatus}</span></div>
104
+ <div className="mt-3 space-y-1.5 text-sm">
105
+ <div className="flex justify-between">
106
+ <span className="text-muted-foreground">{t('app.page.dbStatus.users', 'Users:')}</span>
107
+ <span className="font-mono font-medium">{usersCount}</span>
108
+ </div>
109
+ <div className="flex justify-between">
110
+ <span className="text-muted-foreground">{t('app.page.dbStatus.tenants', 'Tenants:')}</span>
111
+ <span className="font-mono font-medium">{tenantsCount}</span>
112
+ </div>
113
+ <div className="flex justify-between">
114
+ <span className="text-muted-foreground">{t('app.page.dbStatus.organizations', 'Organizations:')}</span>
115
+ <span className="font-mono font-medium">{orgsCount}</span>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <div className="rounded-lg border bg-card p-4 md:col-span-2">
121
+ <div className="text-sm font-medium mb-3">{t('app.page.activeModules.title', 'Active Modules')}</div>
122
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 max-h-[200px] overflow-y-auto pr-2">
123
+ {modules.map((m) => {
124
+ const counts = routeCountsByModule.get(m.id) ?? { frontend: 0, backend: 0, api: 0 }
125
+ const fe = counts.frontend
126
+ const be = counts.backend
127
+ const api = counts.api
128
+ const i18n = m.translations ? Object.keys(m.translations).length : 0
129
+ return (
130
+ <div key={m.id} className="rounded border p-3 bg-background">
131
+ <div className="text-sm font-medium">{m.info?.title || m.id}{m.info?.version ? <span className="ml-2 text-xs text-muted-foreground">v{m.info.version}</span> : null}</div>
132
+ {m.info?.description ? <div className="text-xs text-muted-foreground mt-1 line-clamp-2">{m.info.description}</div> : null}
133
+ <div className="mt-2 flex flex-wrap gap-1">
134
+ {fe ? <FeatureBadge label={`FE:${fe}`} /> : null}
135
+ {be ? <FeatureBadge label={`BE:${be}`} /> : null}
136
+ {api ? <FeatureBadge label={`API:${api}`} /> : null}
137
+ {i18n ? <FeatureBadge label={`i18n:${i18n}`} /> : null}
138
+ </div>
139
+ </div>
140
+ )
141
+ })}
142
+ </div>
143
+ </div>
144
+ </section>
145
+
146
+ <section className="rounded-lg border bg-card p-4">
147
+ <div className="text-sm font-medium mb-2">{t('app.page.quickLinks.title', 'Quick Links')}</div>
148
+ <div className="flex flex-wrap items-center gap-3 text-sm">
149
+ {quickLinks.map((link, index) => (
150
+ <Fragment key={link.href}>
151
+ {index > 0 ? <span className="text-muted-foreground">·</span> : null}
152
+ <Link className="underline hover:text-primary transition-colors" href={link.href}>
153
+ {t(link.translationKey, link.fallbackLabel)}
154
+ </Link>
155
+ </Fragment>
156
+ ))}
157
+ </div>
158
+ </section>
159
+
160
+ <footer className="text-xs text-muted-foreground text-center">
161
+ {t('app.page.footer', 'Built with Next.js, MikroORM, and Awilix — modular by design.')}
162
+ </footer>
163
+ </main>
164
+ )
165
+ }
@@ -92,8 +92,15 @@ export function StartPageContent({ showStartPage: initialShowStartPage, showOnbo
92
92
 
93
93
  const handleCheckboxChange = (checked: boolean) => {
94
94
  setShowStartPage(checked)
95
- // Set cookie to remember preference
96
- document.cookie = `show_start_page=${checked}; path=/; max-age=${365 * 24 * 60 * 60}; SameSite=Lax`
95
+ if (checked) {
96
+ // Re-enable: clear the dismissal so `/` routes back here.
97
+ document.cookie = 'start_page_dismissed=; path=/; max-age=0; SameSite=Lax'
98
+ } else {
99
+ // Dismiss and leave now — `/` routes to the backend (authenticated) or
100
+ // login from here on. The `/` router reads this cookie server-side.
101
+ document.cookie = `start_page_dismissed=1; path=/; max-age=${365 * 24 * 60 * 60}; SameSite=Lax`
102
+ window.location.assign('/')
103
+ }
97
104
  }
98
105
 
99
106
  return (