Skip to content

Commit 7e780df

Browse files
committed
feat: enhance subpage config editor
1 parent cf9b367 commit 7e780df

File tree

13 files changed

+311
-202
lines changed

13 files changed

+311
-202
lines changed

package-lock.json

Lines changed: 54 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"i18next": "^25.7.2",
8686
"i18next-browser-languagedetector": "^8.2.0",
8787
"i18next-http-backend": "^3.0.2",
88+
"is-svg": "^6.1.0",
8889
"json-edit-react": "^1.29.0",
8990
"lodash": "^4.17.21",
9091
"lottie-react": "^2.4.1",
@@ -196,4 +197,4 @@
196197
"inquirer": "9.3.5"
197198
}
198199
}
199-
}
200+
}

public/locales/en/remnawave.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1813,7 +1813,10 @@
18131813
"only-latin-characters-allowed": "Only latin characters allowed",
18141814
"icon-key": "Icon Key",
18151815
"svg-code": "SVG Code",
1816-
"preview": "Preview"
1816+
"preview": "Preview",
1817+
"where-to-find-icons": "Where to find icons?",
1818+
"you-can-find-beautiful-icons-at": "You can find beautiful icons at",
1819+
"where-to-find-icons-description": "Сlick on any icon, select \"Copy SVG\" and paste it here. Make sure to choose \"Outline\" or \"Filled\" style."
18171820
}
18181821
},
18191822
"external-squads-subpage-config": {

src/pages/dashboard/subpage-config/ui/components/subpage-config-editor-page.component..tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { GetSubscriptionPageConfigCommand } from '@remnawave/backend-contract'
22
import { ActionIcon, CopyButton, Group, Tooltip } from '@mantine/core'
3-
import { TbArrowBack, TbFile } from 'react-icons/tb'
3+
import { TbArrowBack, TbDownload, TbFile } from 'react-icons/tb'
44
import { PiCheck, PiCopy } from 'react-icons/pi'
55
import { useNavigate } from 'react-router-dom'
66
import { useTranslation } from 'react-i18next'
@@ -18,6 +18,19 @@ export const SubpageConfigEditorPageComponent = (props: Props) => {
1818
const { t } = useTranslation()
1919
const navigate = useNavigate()
2020

21+
const handleDownloadConfig = () => {
22+
const json = JSON.stringify(config.config, null, 2)
23+
const blob = new Blob([json], { type: 'application/json' })
24+
const url = URL.createObjectURL(blob)
25+
const a = document.createElement('a')
26+
a.href = url
27+
a.download = `subpage-${config.uuid}.json`
28+
document.body.appendChild(a)
29+
a.click()
30+
document.body.removeChild(a)
31+
URL.revokeObjectURL(url)
32+
}
33+
2134
// TODO: Help Article
2235
return (
2336
<Page title={config.name}>
@@ -29,6 +42,15 @@ export const SubpageConfigEditorPageComponent = (props: Props) => {
2942
screen="EDITOR_TEMPLATES_XRAY_JSON"
3043
/> */}
3144

45+
<ActionIcon
46+
color="gray"
47+
onClick={handleDownloadConfig}
48+
size="input-md"
49+
variant="light"
50+
>
51+
<TbDownload size={24} />
52+
</ActionIcon>
53+
3254
<CopyButton timeout={2000} value={config.uuid}>
3355
{({ copied, copy }) => (
3456
<Tooltip label={t('common.copy-uuid')}>
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
.drawerContent {
1+
/* .drawerContent {
22
background-color: #161b23;
33
background-image:
44
linear-gradient(to right, rgba(128, 128, 128, 0.03) 1px, transparent 1px),
55
linear-gradient(to bottom, rgba(128, 128, 128, 0.03) 1px, transparent 1px);
66
background-size: 40px 40px;
7-
}
7+
} */
88

99
.drawerHeader {
1010
background: rgba(22, 27, 35, 0.95);
@@ -14,5 +14,4 @@
1414

1515
.drawerBody {
1616
padding: var(--mantine-spacing-lg);
17-
background: transparent;
1817
}

src/shared/constants/theme/overrides/drawer/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ export default {
66
Drawer: Drawer.extend({
77
classNames: {
88
header: classes.drawerHeader,
9-
body: classes.drawerBody,
10-
content: classes.drawerContent
9+
body: classes.drawerBody
1110
},
1211
defaultProps: {
1312
radius: 'md'

src/shared/ui/overlays/base-overlay-header/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const BaseOverlayHeader = (props: IProps) => {
2222
} = props
2323

2424
return (
25-
<Group gap="sm">
25+
<Group gap="sm" wrap="nowrap">
2626
<ThemeIcon size="lg" variant={iconVariant} {...actionIconProps}>
2727
<IconComponent size={iconSize} />
2828
</ThemeIcon>

src/widgets/dashboard/subpage-configs/subpage-config-editor/editor-components/localized-text-editor.component.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,19 @@ export function LocalizedTextEditor(props: IProps) {
5353
<>
5454
<UnstyledButton className={styles.localizedTextButton} onClick={open} w="100%">
5555
<Group gap="sm" justify="space-between" wrap="nowrap">
56-
<Group gap="xs" wrap="nowrap">
57-
<IconLanguage className={styles.localizedTextIcon} size={16} />
58-
<div style={{ minWidth: 0 }}>
59-
<Text c="dimmed" size="xs">
56+
<Group gap="xs" style={{ flex: 1, minWidth: 0 }} wrap="nowrap">
57+
<IconLanguage
58+
className={styles.localizedTextIcon}
59+
size={16}
60+
style={{ flexShrink: 0 }}
61+
/>
62+
<div style={{ minWidth: 0, flex: 1, overflow: 'hidden' }}>
63+
<Text c="dimmed" size="xs" truncate>
6064
{label}
6165
</Text>
6266
<Text
6367
c="white"
6468
className={styles.localizedTextPreview}
65-
lineClamp={1}
66-
maw="30ch"
6769
size="sm"
6870
truncate="end"
6971
>
@@ -102,9 +104,10 @@ export function LocalizedTextEditor(props: IProps) {
102104
}
103105
key={locale}
104106
label={LOCALE_LABELS[locale]}
105-
minRows={multiline ? 3 : undefined}
107+
minRows={multiline ? 4 : undefined}
106108
onChange={(e) => handleChange(locale, e.target.value)}
107109
placeholder={`Enter ${label.toLowerCase()}...`}
110+
resize={multiline ? 'vertical' : undefined}
108111
value={value[locale] || ''}
109112
/>
110113
))}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Text } from '@mantine/core'
2+
3+
export function RequiredAsterisk() {
4+
return (
5+
<Text c="red" component="span" fz="inherit" inherit ml={4}>
6+
*
7+
</Text>
8+
)
9+
}

src/widgets/dashboard/subpage-configs/subpage-config-editor/editor-components/svg-icon-select.component.tsx

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { IconCheck, IconPhoto } from '@tabler/icons-react'
44
import { useDisclosure } from '@mantine/hooks'
55
import { useTranslation } from 'react-i18next'
66
import { useMemo } from 'react'
7+
import isSvg from 'is-svg'
78

89
import styles from '../subpage-config-visual-editor.module.css'
10+
import { RequiredAsterisk } from './required-asterisk'
911

1012
interface IProps {
1113
label?: string
@@ -23,8 +25,7 @@ export function SvgIconSelect(props: IProps) {
2325
const entries = useMemo(() => Object.entries(svgLibrary), [svgLibrary])
2426

2527
const selectedSvg = svgLibrary[value]
26-
const isValidSvg =
27-
selectedSvg && selectedSvg.trim().startsWith('<svg') && selectedSvg.includes('</svg>')
28+
const isValidSelectedSvg = isSvg(selectedSvg ?? '')
2829

2930
const handleSelect = (key: string) => {
3031
onChange(key)
@@ -36,6 +37,7 @@ export function SvgIconSelect(props: IProps) {
3637
{label && (
3738
<Text c="dimmed" fw={500} mb={4} size="sm">
3839
{label}
40+
<RequiredAsterisk />
3941
</Text>
4042
)}
4143
<Popover
@@ -50,7 +52,7 @@ export function SvgIconSelect(props: IProps) {
5052
<Box className={styles.iconSelectTrigger} onClick={toggle}>
5153
<Group gap="sm" wrap="nowrap">
5254
<Box className={styles.iconSelectPreview}>
53-
{isValidSvg ? (
55+
{isValidSelectedSvg ? (
5456
<Box
5557
className={styles.iconSelectSvg}
5658
dangerouslySetInnerHTML={{ __html: selectedSvg }}
@@ -79,8 +81,6 @@ export function SvgIconSelect(props: IProps) {
7981
) : (
8082
<SimpleGrid cols={5} spacing={6}>
8183
{entries.map(([key, svg]) => {
82-
const isValid =
83-
svg && svg.trim().startsWith('<svg') && svg.includes('</svg>')
8484
const isSelected = key === value
8585

8686
return (
@@ -98,17 +98,11 @@ export function SvgIconSelect(props: IProps) {
9898
size={52}
9999
variant="subtle"
100100
>
101-
{isValid ? (
102-
<Box
103-
className={styles.iconSelectItemSvg}
104-
dangerouslySetInnerHTML={{ __html: svg }}
105-
/>
106-
) : (
107-
<IconPhoto
108-
color="var(--mantine-color-dimmed)"
109-
size={18}
110-
/>
111-
)}
101+
<Box
102+
className={styles.iconSelectItemSvg}
103+
dangerouslySetInnerHTML={{ __html: svg }}
104+
/>
105+
112106
{isSelected && (
113107
<Box className={styles.iconSelectCheck}>
114108
<IconCheck size={12} />

0 commit comments

Comments
 (0)