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,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
|
|
10
|
-
import
|
|
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
|
|
58
|
-
const showStartPage = showStartPageCookie?.value !== 'false'
|
|
10
|
+
const startPageDismissed = cookieStore.get('start_page_dismissed')?.value === '1'
|
|
59
11
|
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
78
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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 (
|