Skip to content

Commit b815e1f

Browse files
committed
feat: add Subscription Page Builder to dashboard sidebar and routing
1 parent e188c50 commit b815e1f

File tree

22 files changed

+1557
-1
lines changed

22 files changed

+1557
-1
lines changed

src/app/layouts/dashboard/sidebar/menu-sections.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ export const useMenuSections = (): MenuItem[] => {
117117
name: t('constants.happ-routing-builder'),
118118
href: ROUTES.DASHBOARD.UTILS.HAPP_ROUTING_BUILDER,
119119
icon: PiLinkDuotone
120+
},
121+
{
122+
name: 'Sub Page Builder',
123+
href: ROUTES.DASHBOARD.UTILS.SUBSCRIPTION_PAGE_BUILDER,
124+
icon: PiLinkDuotone
120125
}
121126
]
122127
},

src/app/router/router.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from 'react-router-dom'
88
import { SUBSCRIPTION_TEMPLATE_TYPE } from '@remnawave/backend-contract'
99

10+
import { SubscriptionPageBuilderConnector } from '@pages/dashboard/utils/subscription-page-builder/ui/connectors/subscription-page-builder.page.connector'
1011
import { HappRoutingBuilderPageConnector } from '@pages/dashboard/utils/happ-routing-builder/ui/connectors/happ-routing-builder.page.connector'
1112
import { TemplateBasePageConnector } from '@pages/dashboard/templates/ui/connectors/template-base-page.connector'
1213
import { NodesBandwidthTablePageConnector } from '@pages/dashboard/nodes-bandwidth-table/ui/connectors'
@@ -124,6 +125,10 @@ const router = createBrowserRouter(
124125
element={<HappRoutingBuilderPageConnector />}
125126
path={ROUTES.DASHBOARD.UTILS.HAPP_ROUTING_BUILDER}
126127
/>
128+
<Route
129+
element={<SubscriptionPageBuilderConnector />}
130+
path={ROUTES.DASHBOARD.UTILS.SUBSCRIPTION_PAGE_BUILDER}
131+
/>
127132
<Route
128133
element={<SubscriptionSettingsConnector />}
129134
path={ROUTES.DASHBOARD.SUBSCRIPTION_SETTINGS}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { AppConfig, LocalizedText, PlatformConfig, Step, TitleStep } from './types'
2+
3+
/**
4+
* Пустые шаблоны для создания новых объектов
5+
*/
6+
7+
export const emptyLocalizedText: LocalizedText = {
8+
en: '',
9+
fa: '',
10+
ru: ''
11+
}
12+
13+
export const emptyStep: Step = {
14+
description: { ...emptyLocalizedText }
15+
}
16+
17+
export const emptyTitleStep: TitleStep = {
18+
description: { ...emptyLocalizedText },
19+
title: { ...emptyLocalizedText },
20+
buttons: []
21+
}
22+
23+
/**
24+
* Создает пустое приложение для указанной платформы
25+
*/
26+
export const createEmptyApp = (platform: 'android' | 'ios' | 'pc'): AppConfig => ({
27+
id: `new-app-${platform}-${Date.now()}`.toLowerCase() as `${Lowercase<string>}`,
28+
name: 'New App',
29+
isFeatured: false,
30+
urlScheme: '',
31+
installationStep: {
32+
buttons: [],
33+
description: { ...emptyLocalizedText }
34+
},
35+
addSubscriptionStep: { ...emptyStep },
36+
connectAndUseStep: { ...emptyStep }
37+
})
38+
39+
/**
40+
* Пустая конфигурация платформ
41+
*/
42+
export const emptyConfig: PlatformConfig = {
43+
ios: [],
44+
android: [],
45+
pc: []
46+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/* eslint-disable no-use-before-define */
2+
3+
export interface AppConfig {
4+
additionalAfterAddSubscriptionStep?: TitleStep
5+
additionalBeforeAddSubscriptionStep?: TitleStep
6+
addSubscriptionStep: Step
7+
connectAndUseStep: Step
8+
id: `${Lowercase<string>}`
9+
installationStep: {
10+
buttons: Button[]
11+
description: LocalizedText
12+
}
13+
isFeatured: boolean
14+
isNeedBase64Encoding?: boolean
15+
name: string
16+
urlScheme: string
17+
viewPosition?: number
18+
}
19+
20+
export interface Button {
21+
buttonLink: string
22+
buttonText: LocalizedText
23+
}
24+
25+
export interface LocalizedText {
26+
en: string
27+
fa: string
28+
ru: string
29+
}
30+
31+
export interface PlatformConfig {
32+
android: AppConfig[]
33+
ios: AppConfig[]
34+
pc: AppConfig[]
35+
}
36+
37+
export interface Step {
38+
description: LocalizedText
39+
}
40+
41+
export interface TitleStep extends Step {
42+
buttons: Button[]
43+
title: LocalizedText
44+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any, no-use-before-define */
2+
3+
import { PlatformConfig } from './types'
4+
5+
interface ValidationResult {
6+
errors: string[]
7+
valid: boolean
8+
}
9+
10+
export function isPlatformConfig(obj: any): obj is PlatformConfig {
11+
const result = validatePlatformConfig(obj)
12+
return result.valid
13+
}
14+
15+
export function validatePlatformConfig(config: any): ValidationResult {
16+
const errors: string[] = []
17+
18+
if (!config || typeof config !== 'object') {
19+
return { valid: false, errors: ['Configuration must be an object'] }
20+
}
21+
22+
if (!Array.isArray(config.ios)) {
23+
errors.push('config.ios: must be an array')
24+
} else {
25+
config.ios.forEach((app: any, index: number) => {
26+
errors.push(...validateAppConfig(app, `config.ios[${index}]`))
27+
})
28+
}
29+
30+
if (!Array.isArray(config.android)) {
31+
errors.push('config.android: must be an array of applications')
32+
} else {
33+
config.android.forEach((app: any, index: number) => {
34+
errors.push(...validateAppConfig(app, `config.android[${index}]`))
35+
})
36+
}
37+
38+
if (!Array.isArray(config.pc)) {
39+
errors.push('config.pc: must be an array of applications')
40+
} else {
41+
config.pc.forEach((app: any, index: number) => {
42+
errors.push(...validateAppConfig(app, `config.pc[${index}]`))
43+
})
44+
}
45+
46+
return {
47+
valid: errors.length === 0,
48+
errors
49+
}
50+
}
51+
52+
function validateAppConfig(app: any, path: string): string[] {
53+
const errors: string[] = []
54+
55+
if (!app || typeof app !== 'object') {
56+
return [`${path}: must be an object`]
57+
}
58+
59+
if (typeof app.id !== 'string') {
60+
errors.push(`${path}.id: must be a lowercase string`)
61+
} else if (!/^[a-z]/.test(app.id)) {
62+
errors.push(`${path}.id: must start with a lowercase letter`)
63+
}
64+
65+
if (typeof app.name !== 'string') errors.push(`${path}.name: must be a string`)
66+
if (typeof app.isFeatured !== 'boolean') errors.push(`${path}.isFeatured: must be a boolean`)
67+
if (typeof app.urlScheme !== 'string') errors.push(`${path}.urlScheme: must be a string`)
68+
69+
if (app.isNeedBase64Encoding !== undefined && typeof app.isNeedBase64Encoding !== 'boolean') {
70+
errors.push(`${path}.isNeedBase64Encoding: must be a boolean`)
71+
}
72+
73+
if (!app.installationStep || typeof app.installationStep !== 'object') {
74+
errors.push(`${path}.installationStep: must be an object`)
75+
} else {
76+
errors.push(
77+
...validateLocalizedText(
78+
app.installationStep.description,
79+
`${path}.installationStep.description`
80+
)
81+
)
82+
errors.push(
83+
...validateButtons(app.installationStep.buttons, `${path}.installationStep.buttons`)
84+
)
85+
}
86+
87+
if (!app.addSubscriptionStep) {
88+
errors.push(`${path}.addSubscriptionStep: required field is missing`)
89+
} else {
90+
errors.push(...validateStep(app.addSubscriptionStep, `${path}.addSubscriptionStep`))
91+
}
92+
93+
if (!app.connectAndUseStep) {
94+
errors.push(`${path}.connectAndUseStep: required field is missing`)
95+
} else {
96+
errors.push(...validateStep(app.connectAndUseStep, `${path}.connectAndUseStep`))
97+
}
98+
99+
if (app.additionalBeforeAddSubscriptionStep) {
100+
errors.push(
101+
...validateTitleStep(
102+
app.additionalBeforeAddSubscriptionStep,
103+
`${path}.additionalBeforeAddSubscriptionStep`
104+
)
105+
)
106+
}
107+
108+
if (app.additionalAfterAddSubscriptionStep) {
109+
errors.push(
110+
...validateTitleStep(
111+
app.additionalAfterAddSubscriptionStep,
112+
`${path}.additionalAfterAddSubscriptionStep`
113+
)
114+
)
115+
}
116+
117+
return errors
118+
}
119+
120+
function validateButton(button: any, path: string): string[] {
121+
const errors: string[] = []
122+
123+
if (!button || typeof button !== 'object') {
124+
return [`${path}: button must be an object`]
125+
}
126+
127+
if (typeof button.buttonLink !== 'string') {
128+
errors.push(`${path}.buttonLink: must be a string`)
129+
} else if (button.buttonLink === '') {
130+
errors.push(`${path}.buttonLink: can't be empty`)
131+
}
132+
133+
errors.push(...validateLocalizedText(button.buttonText, `${path}.buttonText`))
134+
135+
return errors
136+
}
137+
138+
function validateButtons(buttons: any, path: string): string[] {
139+
const errors: string[] = []
140+
141+
if (!Array.isArray(buttons)) {
142+
return [`${path}: must be an array of buttons`]
143+
}
144+
145+
buttons.forEach((button, index) => {
146+
errors.push(...validateButton(button, `${path}[${index}]`))
147+
})
148+
149+
return errors
150+
}
151+
152+
function validateLocalizedText(text: any, path: string): string[] {
153+
const errors: string[] = []
154+
155+
if (!text || typeof text !== 'object') {
156+
return [`${path}: must be an object with translations`]
157+
}
158+
159+
if (typeof text.en !== 'string') errors.push(`${path}.en: must be a string`)
160+
if (typeof text.fa !== 'string') errors.push(`${path}.fa: must be a string`)
161+
if (typeof text.ru !== 'string') errors.push(`${path}.ru: must be a string`)
162+
163+
if (typeof text.en === 'string' && text.en === '') errors.push(`${path}.en: can't be empty`)
164+
if (typeof text.fa === 'string' && text.fa === '') errors.push(`${path}.fa: can't be empty`)
165+
if (typeof text.ru === 'string' && text.ru === '') errors.push(`${path}.ru: can't be empty`)
166+
167+
return errors
168+
}
169+
170+
function validateStep(step: any, path: string): string[] {
171+
const errors: string[] = []
172+
173+
if (!step || typeof step !== 'object') {
174+
return [`${path}: must be an object`]
175+
}
176+
177+
errors.push(...validateLocalizedText(step.description, `${path}.description`))
178+
179+
return errors
180+
}
181+
182+
function validateTitleStep(step: any, path: string): string[] {
183+
const errors: string[] = []
184+
185+
if (!step || typeof step !== 'object') {
186+
return [`${path}: must be an object`]
187+
}
188+
189+
errors.push(...validateLocalizedText(step.title, `${path}.title`))
190+
errors.push(...validateLocalizedText(step.description, `${path}.description`))
191+
errors.push(...validateButtons(step.buttons, `${path}.buttons`))
192+
193+
return errors
194+
}

0 commit comments

Comments
 (0)