From 99c2bc11cb8aea12d793ae2bc7a6765e2a12d81a Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Mon, 31 Jul 2023 00:02:10 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20Add=20board=20customization=20pa?= =?UTF-8?q?ge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Appearance/AppearanceCustomization.tsx | 208 ++++++++++++++++++ .../Gridstack/GridstackCustomization.tsx | 37 ++++ .../Customize/Layout/LayoutCustomization.tsx | 43 ++++ .../Board/Customize/Layout/LayoutPreview.tsx | 124 +++++++++++ .../PageMetadataCustomization.tsx | 37 ++++ src/components/Board/Customize/form.ts | 43 ++++ src/pages/board/[slug]/customize.tsx | 135 ++++++++++++ src/pages/index.tsx | 4 +- 8 files changed, 629 insertions(+), 2 deletions(-) create mode 100644 src/components/Board/Customize/Appearance/AppearanceCustomization.tsx create mode 100644 src/components/Board/Customize/Gridstack/GridstackCustomization.tsx create mode 100644 src/components/Board/Customize/Layout/LayoutCustomization.tsx create mode 100644 src/components/Board/Customize/Layout/LayoutPreview.tsx create mode 100644 src/components/Board/Customize/PageMetadata/PageMetadataCustomization.tsx create mode 100644 src/components/Board/Customize/form.ts create mode 100644 src/pages/board/[slug]/customize.tsx diff --git a/src/components/Board/Customize/Appearance/AppearanceCustomization.tsx b/src/components/Board/Customize/Appearance/AppearanceCustomization.tsx new file mode 100644 index 000000000..eb1109131 --- /dev/null +++ b/src/components/Board/Customize/Appearance/AppearanceCustomization.tsx @@ -0,0 +1,208 @@ +import { + CheckIcon, + ColorSwatch, + Group, + Input, + Slider, + Stack, + Text, + TextInput, + createStyles, + rem, + useMantineTheme, +} from '@mantine/core'; +import { useTranslation } from 'next-i18next'; +import { highlight, languages } from 'prismjs'; +import Editor from 'react-simple-code-editor'; + +import { useBoardCustomizationFormContext } from '../form'; + +export const AppearanceCustomization = () => { + const { t } = useTranslation([ + 'settings/customization/page-appearance', + 'settings/customization/color-selector', + ]); + const theme = useMantineTheme(); + const form = useBoardCustomizationFormContext(); + + return ( + + + + + + + + + ); +}; + +type ColorSelectorProps = { + type: 'primaryColor' | 'secondaryColor'; +}; +const ColorSelector = ({ type }: ColorSelectorProps) => { + const { t } = useTranslation('settings/customization/color-selector'); + const theme = useMantineTheme(); + const form = useBoardCustomizationFormContext(); + + const colors = Object.keys(theme.colors).map((color) => ({ + swatch: theme.colors[color][6], + color, + })); + + return ( + + + {colors.map(({ color, swatch }) => ( + form.getInputProps(`appearance.${type}`).onChange(color)} + color={swatch} + style={{ cursor: 'pointer' }} + > + {color === form.values.appearance[type] && } + + ))} + + + ); +}; + +const ShadeSelector = () => { + const form = useBoardCustomizationFormContext(); + const theme = useMantineTheme(); + + const primaryColor = form.values.appearance.primaryColor; + const primaryShades = theme.colors[primaryColor].map((_, shade) => ({ + swatch: theme.colors[primaryColor][shade], + shade, + })); + + return ( + + + {primaryShades.map(({ shade, swatch }) => ( + form.getInputProps(`appearance.shade`).onChange(shade)} + color={swatch} + style={{ cursor: 'pointer' }} + > + {shade === form.values.appearance.shade && } + + ))} + + + ); +}; + +const OpacitySlider = () => { + const { t } = useTranslation('settings/customization/opacity-selector'); + const form = useBoardCustomizationFormContext(); + + return ( + + + + ); +}; + +const opacityMarks = [ + { value: 10, label: '10%' }, + { value: 20, label: '20%' }, + { value: 30, label: '30%' }, + { value: 40, label: '40%' }, + { value: 50, label: '50%' }, + { value: 60, label: '60%' }, + { value: 70, label: '70%' }, + { value: 80, label: '80%' }, + { value: 90, label: '90%' }, + { value: 100, label: '100%' }, +]; + +const CustomCssInput = () => { + const { t } = useTranslation('settings/customization/page-appearance'); + const { classes } = useStyles(); + const form = useBoardCustomizationFormContext(); + + return ( + +
+ form.getInputProps('appearance.customCss').onChange(code)} + highlight={(code) => highlight(code, languages.extend('css', {}), 'css')} + padding={10} + style={{ + fontFamily: '"Fira code", "Fira Mono", monospace', + fontSize: 12, + minHeight: 250, + }} + /> +
+
+ ); +}; + +const useStyles = createStyles(({ colors, colorScheme, radius }) => ({ + codeEditorFooter: { + borderBottomLeftRadius: radius.sm, + borderBottomRightRadius: radius.sm, + backgroundColor: colorScheme === 'dark' ? colors.dark[7] : undefined, + }, + codeEditorRoot: { + marginTop: 4, + borderColor: colorScheme === 'dark' ? colors.dark[4] : colors.gray[4], + borderWidth: 1, + borderStyle: 'solid', + borderRadius: radius.sm, + }, + codeEditor: { + backgroundColor: colorScheme === 'dark' ? colors.dark[6] : 'white', + fontSize: 12, + + '& ::placeholder': { + color: colorScheme === 'dark' ? colors.dark[3] : colors.gray[5], + }, + }, +})); + +/* + + + + {t('settings/customization/color-selector:colors')} + + + + + + + + + + + + + + + +*/ diff --git a/src/components/Board/Customize/Gridstack/GridstackCustomization.tsx b/src/components/Board/Customize/Gridstack/GridstackCustomization.tsx new file mode 100644 index 000000000..4497b3b50 --- /dev/null +++ b/src/components/Board/Customize/Gridstack/GridstackCustomization.tsx @@ -0,0 +1,37 @@ +import { Input, Slider } from '@mantine/core'; +import { useTranslation } from 'next-i18next'; +import { GridstackBreakpoints } from '~/constants/gridstack-breakpoints'; + +import { useBoardCustomizationFormContext } from '../form'; + +export const GridstackCustomization = () => { + const { t } = useTranslation('settings/customization/gridstack'); + const form = useBoardCustomizationFormContext(); + + return ( + <> + + + + + + + + + + + ); +}; diff --git a/src/components/Board/Customize/Layout/LayoutCustomization.tsx b/src/components/Board/Customize/Layout/LayoutCustomization.tsx new file mode 100644 index 000000000..f6a172679 --- /dev/null +++ b/src/components/Board/Customize/Layout/LayoutCustomization.tsx @@ -0,0 +1,43 @@ +import { Button, Checkbox, Grid, Stack } from '@mantine/core'; +import { IconDeviceFloppy } from '@tabler/icons-react'; +import { useTranslation } from 'next-i18next'; + +import { useBoardCustomizationFormContext } from '../form'; +import { LayoutPreview } from './LayoutPreview'; + +export const LayoutCustomization = () => { + const { t } = useTranslation('settings/common'); + const form = useBoardCustomizationFormContext(); + + return ( + + + + + + + + + + + + + + + ); +}; diff --git a/src/components/Board/Customize/Layout/LayoutPreview.tsx b/src/components/Board/Customize/Layout/LayoutPreview.tsx new file mode 100644 index 000000000..5a843fbba --- /dev/null +++ b/src/components/Board/Customize/Layout/LayoutPreview.tsx @@ -0,0 +1,124 @@ +import { Flex, Group, Indicator, Paper, Stack, createStyles } from '@mantine/core'; +import { Logo } from '~/components/layout/Logo'; +import { createDummyArray } from '~/tools/client/arrays'; + +type LayoutPreviewProps = { + showLeftSidebar: boolean; + showRightSidebar: boolean; + showPings: boolean; +}; +export const LayoutPreview = ({ + showLeftSidebar, + showRightSidebar, + showPings, +}: LayoutPreviewProps) => { + const { classes } = useStyles(); + + return ( + + + +
+ +
+ + + + +
+
+ + + {showLeftSidebar && ( + + + {createDummyArray(5).map((_item, index) => ( + + ))} + + + )} + + + + {createDummyArray(10).map((_item, index) => ( + + ))} + + + + {showRightSidebar && ( + + + {createDummyArray(5).map((_item, index) => ( + + ))} + + + )} + +
+ ); +}; + +const useStyles = createStyles((theme) => ({ + primaryWrapper: { + flexGrow: 2, + }, + secondaryWrapper: { + flexGrow: 1, + maxWidth: 100, + }, +})); + +const BaseElement = ({ height, width }: { height: number; width: number }) => ( + ({ + backgroundColor: theme.colorScheme === 'dark' ? theme.colors.gray[8] : theme.colors.gray[1], + })} + h={height} + p={2} + w={width} + /> +); + +type PlaceholderElementProps = { + height: number; + width: number; + showPing: boolean; + index: number; +}; +const PlaceholderElement = ({ height, width, showPing, index }: PlaceholderElementProps) => { + if (showPing) { + return ( + + + + ); + } + + return ; +}; diff --git a/src/components/Board/Customize/PageMetadata/PageMetadataCustomization.tsx b/src/components/Board/Customize/PageMetadata/PageMetadataCustomization.tsx new file mode 100644 index 000000000..a2f657123 --- /dev/null +++ b/src/components/Board/Customize/PageMetadata/PageMetadataCustomization.tsx @@ -0,0 +1,37 @@ +import { Stack, TextInput } from '@mantine/core'; +import { useTranslation } from 'next-i18next'; + +import { useBoardCustomizationFormContext } from '../form'; + +export const PageMetadataCustomization = () => { + const { t } = useTranslation('settings/customization/page-appearance'); + const form = useBoardCustomizationFormContext(); + return ( + + + + + + + ); +}; diff --git a/src/components/Board/Customize/form.ts b/src/components/Board/Customize/form.ts new file mode 100644 index 000000000..b65463dbc --- /dev/null +++ b/src/components/Board/Customize/form.ts @@ -0,0 +1,43 @@ +import { DEFAULT_THEME, MANTINE_COLORS, MantineColor } from '@mantine/core'; +import { createFormContext } from '@mantine/form'; +import { z } from 'zod'; + +const schema = z.object({ + layout: z.object({ + leftSidebarEnabled: z.boolean(), + rightSidebarEnabled: z.boolean(), + pingsEnabled: z.boolean(), + }), + gridstack: z.object({ + sm: z.number().min(1).max(8), + md: z.number().min(3).max(16), + lg: z.number().min(5).max(20), + }), + pageMetadata: z.object({ + pageTitle: z.string(), + metaTitle: z.string(), + logoSrc: z.string(), + faviconSrc: z.string(), + }), + appearance: z.object({ + backgroundSrc: z.string(), + primaryColor: z.custom( + (value) => typeof value === 'string' && MANTINE_COLORS.includes(value) + ), + secondaryColor: z.custom( + (value) => typeof value === 'string' && MANTINE_COLORS.includes(value) + ), + shade: z + .number() + .min(0) + .max(DEFAULT_THEME.colors['blue'].length - 1), + opacity: z.number().min(10).max(100), + customCss: z.string(), + }), +}); + +export const [ + BoardCustomizationFormProvider, + useBoardCustomizationFormContext, + useBoardCustomizationForm, +] = createFormContext>(); diff --git a/src/pages/board/[slug]/customize.tsx b/src/pages/board/[slug]/customize.tsx new file mode 100644 index 000000000..a3edd6a94 --- /dev/null +++ b/src/pages/board/[slug]/customize.tsx @@ -0,0 +1,135 @@ +import { Button, Container, Group, Paper, Stack, Text, Title } from '@mantine/core'; +import { + IconArrowLeft, + IconBrush, + IconChartCandle, + IconDragDrop, + IconLayout, + TablerIconsProps, +} from '@tabler/icons-react'; +import { GetServerSideProps } from 'next'; +import { useTranslation } from 'next-i18next'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { ReactNode } from 'react'; +import { AppearanceCustomization } from '~/components/Board/Customize/Appearance/AppearanceCustomization'; +import { GridstackCustomization } from '~/components/Board/Customize/Gridstack/GridstackCustomization'; +import { LayoutCustomization } from '~/components/Board/Customize/Layout/LayoutCustomization'; +import { PageMetadataCustomization } from '~/components/Board/Customize/PageMetadata/PageMetadataCustomization'; +import { + BoardCustomizationFormProvider, + useBoardCustomizationForm, +} from '~/components/Board/Customize/form'; +import { MainLayout } from '~/components/layout/main'; +import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; +import { dashboardNamespaces } from '~/tools/server/translation-namespaces'; + +export default function CustomizationPage() { + const query = useRouter().query as { slug: string }; + const { t } = useTranslation([ + 'settings/customization/general', + 'settings/customization/color-selector', + ]); + const form = useBoardCustomizationForm({ + initialValues: { + layout: { + leftSidebarEnabled: false, + rightSidebarEnabled: false, + pingsEnabled: false, + }, + appearance: { + backgroundSrc: '', + primaryColor: 'red', + secondaryColor: 'orange', + shade: 8, + opacity: 50, + customCss: '', + }, + gridstack: { + sm: 3, + md: 6, + lg: 12, + }, + pageMetadata: { + pageTitle: '', + metaTitle: '', + logoSrc: '', + faviconSrc: '', + }, + }, + }); + + return ( + + + + + + Customization for {query.slug} Board + + + +
+ + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ ); +} + +type SectionTitleProps = { + type: 'layout' | 'gridstack' | 'pageMetadata' | 'appereance'; + icon: (props: TablerIconsProps) => ReactNode; +}; + +const SectionTitle = ({ type, icon: Icon }: SectionTitleProps) => { + const { t } = useTranslation('settings/customization/general'); + + return ( + + + + {t(`accordeon.${type}.name`)} + + {t(`accordeon.${type}.description`)} + + ); +}; + +export const getServerSideProps: GetServerSideProps = async ({ req, res, locale }) => { + const translations = await getServerSideTranslations(dashboardNamespaces, locale, req, res); + + return { + props: { + ...translations, + }, + }; +}; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 80e415deb..bfa1eaca8 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -63,9 +63,9 @@ export default function HomePage({ config: initialConfig }: DashboardServerSideP useInitConfig(initialConfig); return ( - + - + ); }