diff --git a/public/locales/bg/campaigns.json b/public/locales/bg/campaigns.json index 6baf3f31b..2a8af78fd 100644 --- a/public/locales/bg/campaigns.json +++ b/public/locales/bg/campaigns.json @@ -196,5 +196,34 @@ "disabled": "Деактивирана", "error": "Грешна", "deleted": "Изтрита" - } + }, + "create-campaign": "Създайте кампания", + "steps": { + "step1": "Стъпка 1", + "step1-type": "Информация за кампания", + "step1-description": "на тази стъпка трябва посочите името на вашата кампания, нейния вид, бенефициент на кампанията, необходима сума и срок на кампанията", + "step2": "Стъпка 2", + "step2-type": "Организатор", + "step2-description": "тук ще се изискат ваши лични данни, както и е необходимо да се запознаете и съгласите с “Политиката за защита на лични данни” на Подкрепи.бг", + "step3": "Стъпка 3", + "step3-type": "Описание на кампания", + "step3-description": "на този етап ще опишете подоробно целта на вашата кампания, какво е правено до момента (ако има извършени дейности), да посочите свои лични истории, да посочите свой/свои гаранти.", + "step4": "Стъпка 4", + "step4-type": "Снимки/Документи", + "step4-description-part1": "свалете основния пакет от", + "step4-description-link": "необходими документи", + "step4-description-part2": "на Вашия компютър, попълнете, подпишете. На тази стъпка прикачете файловете . Тук също ще може да прикачите снимки и видео, свързани с вашата кампания и нейната конкретна цел.", + "step5": "Стъпка 5", + "step5-type": "Преглед на кампания", + "step5-description": "на тази стъпка ще можете да прегледате своята кампания" + }, + "here": "тук", + "note": "След преглед на попълнените и изпратени данни ще бъде изискан и допълнителен пакет от документи, който е съобразен изцяло с вида на вашата кампания. За повече информация относно кандидатстване по кампания, моля вижте", + "type-of-organization": "Тип на вашата организация", + "please-select-organization": "Моля, посочете типа на вашата организация:", + "individual": "Физическо лице", + "organization": "Организация", + "back": "Назад", + "next": "Продължете", + "save-and-next": "Запазете и продължете" } diff --git a/public/locales/en/campaigns.json b/public/locales/en/campaigns.json index 08fa1639b..48c221cc3 100644 --- a/public/locales/en/campaigns.json +++ b/public/locales/en/campaigns.json @@ -190,5 +190,34 @@ "disabled": "Disabled", "error": "Error", "deleted": "Deleted" - } + }, + "create-campaign": "Create campaign", + "steps": { + "step1": "Step 1", + "step1-type": "Campaign information", + "step1-description": "at this step you must specify the name of your campaign, its type, beneficiary of the campaign, required amount and duration of the campaign", + "step2": "Step 2", + "step2-type": "Organizer", + "step2-description": "here your personal data will be required, as well as it is necessary to familiarize yourself with and agree to the 'Privacy policy' of Podkrepi.bg", + "step3": "Step 3", + "step3-type": "Campaign description", + "step3-description": "at this step you will describe in detail the purpose of your campaign, what has been done so far (if any activities have been carried out), indicate your personal stories, indicate your guarantor(s).", + "step4": "Step 4", + "step4-type": "Photos/Documents", + "step4-description-part1": "download the core package from", + "step4-description-link": "necessary documents", + "step4-description-part2": "on your computer, fill in, sign. At this step, attach the files . Here you will also be able to attach photos and video related to your campaign and its specific purpose.", + "step5": "Step 5", + "step5-type": "Campaign overview", + "step5-description": "at this step you will be able to review your campaign" + }, + "here": "here", + "note": "After reviewing the completed and sent data, an additional package of documents will be requested, which is fully tailored to the type of your campaign. For more information on applying by campaign, please click", + "type-of-organization": "Type of your organization", + "please-select-organization": "Please select your organization type:", + "individual": "Individual", + "organization": "Organization", + "back": "Back", + "next": "Continue", + "save-and-next": "Save & Continue" } diff --git a/src/common/routes.ts b/src/common/routes.ts index 0e0a01b33..6dfd05885 100644 --- a/src/common/routes.ts +++ b/src/common/routes.ts @@ -78,6 +78,7 @@ export const routes = { campaigns: { index: '/campaigns', create: '/campaigns/create', + steps: '/campaigns/create/steps', viewCampaignBySlug: (slug: string) => `/campaigns/${slug}`, viewExpenses: (slug: string) => `/campaigns/${slug}/expenses`, oneTimeDonation: (slug: string) => `/campaigns/donation/${slug}`, diff --git a/src/common/theme.ts b/src/common/theme.ts index 8641fef99..1e9d837b7 100644 --- a/src/common/theme.ts +++ b/src/common/theme.ts @@ -1,203 +1,214 @@ -import { - createTheme, - darken, - lighten, - responsiveFontSizes, - Theme, - ThemeOptions, -} from '@mui/material/styles' - -import { Montserrat } from '@next/font/google' - -export const montserrat = Montserrat({ - display: 'auto', - subsets: ['latin', 'cyrillic'], -}) - -const colors = { - blue: { - light: '#4AC3FF', - main: '#32A9FE', - mainDark: darken('#32A9FE', 0.2), - dark: '#294E85', - }, - yellow: { - main: '#FFCB57', - dark: '#F6992B', - }, - gray: { - main: '#F5F5F5', - background: '#FAFAFA', - }, - white: { - main: '#ffffff', - }, -} - -const borders = { - dark: colors.blue.dark, - light: colors.blue.main, - round: '60px', - semiRound: '20px', -} - -export const themeOptions: ThemeOptions = { - palette: { - mode: 'light', - primary: { - light: colors.blue.light, - main: colors.blue.main, - dark: colors.blue.dark, - }, - secondary: { - main: colors.yellow.main, - light: colors.gray.main, - }, - background: { - default: colors.white.main, - }, - info: { - main: colors.blue.dark, - light: colors.blue.mainDark, - dark: darken(colors.blue.dark, 0.2), - }, - }, - shape: { - borderRadius: 3, - }, - components: { - MuiLink: { - defaultProps: { - underline: 'none', - }, - }, - MuiButton: { - defaultProps: { - disableElevation: true, - }, - styleOverrides: { - root: { - lineHeight: 2, - borderRadius: '25px', - borderWidth: 2, - '&:hover': { - borderWidth: 2, - }, - }, - textPrimary: { - color: colors.blue.dark, - '&:hover': { - color: colors.blue.mainDark, - }, - }, - outlined: { - backgroundColor: colors.white.main, - }, - outlinedPrimary: { - color: colors.blue.dark, - '&:hover': { - backgroundColor: lighten(colors.blue.main, 0.85), - }, - }, - outlinedSecondary: { - color: darken(colors.yellow.dark, 0.4), - borderColor: colors.yellow.main, - '&:hover': { - backgroundColor: lighten(colors.yellow.main, 0.85), - borderColor: darken(colors.yellow.main, 0.15), - }, - }, - containedPrimary: { - backgroundColor: colors.blue.main, - '&:hover': { - backgroundColor: darken(colors.blue.main, 0.15), - }, - }, - containedSecondary: { - backgroundColor: colors.yellow.main, - '&:hover': { - backgroundColor: darken(colors.yellow.main, 0.15), - }, - }, - }, - }, - MuiButtonBase: { - defaultProps: { - disableRipple: true, - }, - }, - MuiInputBase: { - styleOverrides: { - root: { - fontSize: '1rem', - borderRadius: borders.round, - }, - multiline: { - borderRadius: borders.semiRound, - }, - }, - }, - MuiFilledInput: { - styleOverrides: { - root: { - borderRadius: borders.round, - }, - multiline: { - borderRadius: borders.semiRound, - }, - }, - }, - MuiOutlinedInput: { - styleOverrides: { - root: { - borderRadius: borders.round, - }, - multiline: { - borderRadius: borders.semiRound, - }, - }, - }, - MuiInputLabel: { - styleOverrides: { - root: { - fontSize: '1rem', - }, - }, - }, - MuiAppBar: { - styleOverrides: { - root: { - paddingLeft: 15, - paddingRight: 15, - }, - }, - }, - - MuiMenuItem: { - defaultProps: { - sx: { py: 1.5 }, - }, - }, - }, - - typography: { - fontFamily: montserrat.style.fontFamily, - h3: { color: colors.blue.dark }, - - body1: { - fontSize: '0.875rem', - lineHeight: '1.43', - letterSpacing: '0.01071em', - }, - button: { textTransform: 'initial' }, - }, -} - -const theme: Theme = createTheme(themeOptions) -const materialTheme = responsiveFontSizes(theme) -const podkrepiTheme = { - borders, - ...materialTheme, -} - -export default podkrepiTheme +import { + createTheme, + darken, + lighten, + responsiveFontSizes, + Theme, + ThemeOptions, +} from '@mui/material/styles' + +import { Montserrat, Raleway, Lato } from '@next/font/google' + +export const montserrat = Montserrat({ + display: 'auto', + subsets: ['latin', 'cyrillic'], +}) + +export const raleway = Raleway({ + display: 'auto', + subsets: ['latin', 'cyrillic'], +}) + +export const lato = Lato({ + display: 'auto', + subsets: ['latin'], + weight: '400', +}) + +const colors = { + blue: { + light: '#4AC3FF', + main: '#32A9FE', + mainDark: darken('#32A9FE', 0.2), + dark: '#294E85', + }, + yellow: { + main: '#FFCB57', + dark: '#F6992B', + }, + gray: { + main: '#F5F5F5', + background: '#FAFAFA', + }, + white: { + main: '#ffffff', + }, +} + +const borders = { + dark: colors.blue.dark, + light: colors.blue.main, + round: '60px', + semiRound: '20px', +} + +export const themeOptions: ThemeOptions = { + palette: { + mode: 'light', + primary: { + light: colors.blue.light, + main: colors.blue.main, + dark: colors.blue.dark, + }, + secondary: { + main: colors.yellow.main, + light: colors.gray.main, + }, + background: { + default: colors.white.main, + }, + info: { + main: colors.blue.dark, + light: colors.blue.mainDark, + dark: darken(colors.blue.dark, 0.2), + }, + }, + shape: { + borderRadius: 3, + }, + components: { + MuiLink: { + defaultProps: { + underline: 'none', + }, + }, + MuiButton: { + defaultProps: { + disableElevation: true, + }, + styleOverrides: { + root: { + lineHeight: 2, + borderRadius: '25px', + borderWidth: 2, + '&:hover': { + borderWidth: 2, + }, + }, + textPrimary: { + color: colors.blue.dark, + '&:hover': { + color: colors.blue.mainDark, + }, + }, + outlined: { + backgroundColor: colors.white.main, + }, + outlinedPrimary: { + color: colors.blue.dark, + '&:hover': { + backgroundColor: lighten(colors.blue.main, 0.85), + }, + }, + outlinedSecondary: { + color: darken(colors.yellow.dark, 0.4), + borderColor: colors.yellow.main, + '&:hover': { + backgroundColor: lighten(colors.yellow.main, 0.85), + borderColor: darken(colors.yellow.main, 0.15), + }, + }, + containedPrimary: { + backgroundColor: colors.blue.main, + '&:hover': { + backgroundColor: darken(colors.blue.main, 0.15), + }, + }, + containedSecondary: { + backgroundColor: colors.yellow.main, + '&:hover': { + backgroundColor: darken(colors.yellow.main, 0.15), + }, + }, + }, + }, + MuiButtonBase: { + defaultProps: { + disableRipple: true, + }, + }, + MuiInputBase: { + styleOverrides: { + root: { + fontSize: '1rem', + borderRadius: borders.round, + }, + multiline: { + borderRadius: borders.semiRound, + }, + }, + }, + MuiFilledInput: { + styleOverrides: { + root: { + borderRadius: borders.round, + }, + multiline: { + borderRadius: borders.semiRound, + }, + }, + }, + MuiOutlinedInput: { + styleOverrides: { + root: { + borderRadius: borders.round, + }, + multiline: { + borderRadius: borders.semiRound, + }, + }, + }, + MuiInputLabel: { + styleOverrides: { + root: { + fontSize: '1rem', + }, + }, + }, + MuiAppBar: { + styleOverrides: { + root: { + paddingLeft: 15, + paddingRight: 15, + }, + }, + }, + + MuiMenuItem: { + defaultProps: { + sx: { py: 1.5 }, + }, + }, + }, + + typography: { + fontFamily: montserrat.style.fontFamily, + h3: { color: colors.blue.dark }, + + body1: { + fontSize: '0.875rem', + lineHeight: '1.43', + letterSpacing: '0.01071em', + }, + button: { textTransform: 'initial' }, + }, +} + +const theme: Theme = createTheme(themeOptions) +const materialTheme = responsiveFontSizes(theme) +const podkrepiTheme = { + borders, + ...materialTheme, +} + +export default podkrepiTheme diff --git a/src/components/client/campaigns/CampaignFilter.tsx b/src/components/client/campaigns/CampaignFilter.tsx index 2c533f613..f42945e3b 100644 --- a/src/components/client/campaigns/CampaignFilter.tsx +++ b/src/components/client/campaigns/CampaignFilter.tsx @@ -1,10 +1,10 @@ import React, { useMemo, useState } from 'react' -import { styled } from '@mui/material/styles' -import { Box, CircularProgress, IconButton, ImageList, Typography } from '@mui/material' +import { Box, CircularProgress } from '@mui/material' import { useCampaignList } from 'common/hooks/campaigns' import CampaignsList from './CampaignsList' import { CampaignResponse } from 'gql/campaigns' import { CampaignTypeCategory } from 'components/common/campaign-types/categories' +import { CategoryType } from 'gql/types' import { useTranslation } from 'next-i18next' import { Apartment, @@ -20,42 +20,11 @@ import { TheaterComedy, VolunteerActivism, } from '@mui/icons-material' -import useMobile from 'common/hooks/useMobile' - -const PREFIX = 'CampaignFilter' - -const classes = { - filterButtons: `${PREFIX}-filterButtons`, -} -const Root = styled('div')(() => ({ - [`& .${classes.filterButtons}`]: { - display: 'block', - height: '80px', - borderRadius: 0, - borderBottom: '1px solid transparent', - padding: 1, - '&:active': { - color: '#4AC3FF', - borderBottom: '5px solid #4AC3FF', - }, - '&:hover': { - backgroundColor: 'white', - borderBottom: '5px solid #4AC3FF', - color: '#4AC3FF', - }, - '&:focus': { - color: '#4AC3FF', - borderBottom: '5px solid #4AC3FF', - }, - '&:selected': { - color: '#4AC3FF', - borderBottom: '5px solid #4AC3FF', - }, - }, -})) +import useMobile from 'common/hooks/useMobile' +import ListIconButtons from './ListIconButtons' -const categories: { +const categoryIcons: { [key in CampaignTypeCategory]: { icon?: React.ReactElement } } = { medical: { icon: }, @@ -68,63 +37,87 @@ const categories: { art: { icon: }, animals: { icon: }, nature: { icon: }, - others: {}, + others: { icon: }, + all: { icon: }, +} + +type Props = { + showCampaigns?: boolean + selected: CampaignTypeCategory + onClick: (item: CategoryType) => void + styles: React.CSSProperties + styleItem?: React.CSSProperties } -export default function CampaignFilter() { +export default function CampaignFilter({ + selected, + styles, + styleItem, + showCampaigns = true, + onClick, +}: Props) { const { t } = useTranslation() const { mobile } = useMobile() const { data: campaigns, isLoading } = useCampaignList() - const [selectedCategory, setSelectedCategory] = useState('ALL') + // TODO: add filters&sorting of campaigns so people can select based on personal preferences const campaignToShow = useMemo(() => { const filteredCampaigns = campaigns?.filter((campaign) => { - if (selectedCategory != 'ALL') { - return campaign.campaignType.category === selectedCategory + if (selected != 'all') { + return campaign.campaignType.category === selected } return campaign }) ?? [] return filteredCampaigns - }, [campaigns, selectedCategory]) + }, [campaigns, selected]) + + const categories = useMemo(() => { + const computedCategories = Object.values(CampaignTypeCategory).map((category) => { + const count = + campaigns?.filter((campaign) => campaign.campaignType.category === category).length ?? 0 + + if (category === 'all') { + return { + type: 'all', + text: t(`campaigns:filters.${category}`), + count: campaigns?.length, + icon: categoryIcons[category].icon, + isDisabled: false, + } + } + + return { + type: category, + text: t(`campaigns:filters.${category}`), + count: count, + icon: categoryIcons[category].icon, + isDisabled: !count, + } + }) + + return computedCategories + }, [campaigns]) return ( - - - {Object.values(CampaignTypeCategory).map((category) => { - const count = - campaigns?.filter((campaign) => campaign.campaignType.category === category).length ?? 0 - return ( - setSelectedCategory(category)}> - {categories[category].icon ?? } - - {t(`campaigns:filters.${category}`)} ({count}) - - - ) - })} - setSelectedCategory('ALL')}> - - - {t(`campaigns:filters.all`)} ({campaigns?.length ?? 0}) - - - + <> + + + + {isLoading ? ( ) : ( - + showCampaigns && )} - + ) } diff --git a/src/components/client/campaigns/CampaignsPage.tsx b/src/components/client/campaigns/CampaignsPage.tsx index 63623cffb..653b620ff 100644 --- a/src/components/client/campaigns/CampaignsPage.tsx +++ b/src/components/client/campaigns/CampaignsPage.tsx @@ -1,13 +1,14 @@ -import React from 'react' +import React, { useState } from 'react' import { useTranslation } from 'next-i18next' import { Grid, Typography } from '@mui/material' +import { CategoryType } from 'gql/types' +import { styled } from '@mui/material/styles' +import { CampaignTypeCategory } from 'components/common/campaign-types/categories' import CampaignFilter from './CampaignFilter' import Layout from 'components/client/layout/Layout' -import { styled } from '@mui/material/styles' - const PREFIX = 'CampaignsPage' const classes = { @@ -77,6 +78,14 @@ const Root = styled(Layout)(({ theme }) => ({ export default function CampaignsPage() { const { t } = useTranslation() + const [selectedCategory, setSelectedCategory] = useState( + CampaignTypeCategory.medical, + ) + + const selectCategoryHandler = (category: CategoryType) => { + setSelectedCategory(category?.type) + } + return ( @@ -86,7 +95,11 @@ export default function CampaignsPage() { {t('campaigns:cta.support-cause-today')} - + ) diff --git a/src/components/client/campaigns/CreateCampaignSteps.tsx b/src/components/client/campaigns/CreateCampaignSteps.tsx new file mode 100644 index 000000000..770b2164f --- /dev/null +++ b/src/components/client/campaigns/CreateCampaignSteps.tsx @@ -0,0 +1,89 @@ +import { Heading } from './campaigns.styled' +import { styled } from '@mui/system' +import theme, { lato } from 'common/theme' +import { useTranslation } from 'next-i18next' + +import ExternalLink from 'components/common/ExternalLink' + +// TODO: Ask for the URLs + +const StepHeading = styled('strong')(() => ({ + fontWeight: 700, + fontFamily: lato.style.fontFamily, + fontSize: theme.typography.pxToRem(16), +})) + +const Paragraph = styled('p')(() => ({ + fontFamily: lato.style.fontFamily, + fontSize: theme.typography.pxToRem(14), + margin: 0, + + '& a': { + fontStyle: 'normal', + textDecoration: 'underline', + }, +})) + +const Note = styled('p')(() => ({ + fontFamily: 'Raleway, sans-serif', + fontStyle: 'italic', + fontSize: theme.typography.pxToRem(14), + lineHeight: theme.typography.pxToRem(24), + letterSpacing: '-0.009em', + + '& a': { + fontStyle: 'normal', + textDecoration: 'underline', + }, +})) + +export default function CreateCampaignSteps() { + const { t } = useTranslation('campaigns') + + return ( + <> + {t('create-campaign')} + + + + {t('steps.step1')} "{t('steps.step1-type')}" + + - {t('steps.step1-description')} + + + + + {t('steps.step2')} "{t('steps.step2-type')}" + {' '} + -{t('steps.step2-description')} + + + + + {t('steps.step3')} "{t('steps.step3-type')}" + {' '} + -{t('steps.step3-description')} + + + + + {t('steps.step4')} "{t('steps.step4-type')}" + {' '} + -{t('steps.step4-description-part1')} +   {t('steps.step4-description-link')}  + {t('steps.step4-description-part2')} + + + + + {t('steps.step5')} "{t('steps.step5-type')}" + {' '} + -{t('steps.step5-description')} + + + + {t('note')} {t('here')}. + + + ) +} diff --git a/src/components/client/campaigns/CreateCampaignUserType.tsx b/src/components/client/campaigns/CreateCampaignUserType.tsx new file mode 100644 index 000000000..bf3732c42 --- /dev/null +++ b/src/components/client/campaigns/CreateCampaignUserType.tsx @@ -0,0 +1,38 @@ +import { Heading } from './campaigns.styled' +import { useTranslation } from 'next-i18next' +import { Typography } from '@mui/material' +import { CategoryType } from 'gql/types' + +import PersonIcon from '@mui/icons-material/Person' +import PeopleOutlinedIcon from '@mui/icons-material/PeopleOutlined' +import ListIconButtons from './ListIconButtons' + +type Props = { + onClick: (item: CategoryType) => void +} + +export default function CreateCampaignUserType({ onClick }: Props) { + const { t } = useTranslation('campaigns') + + const users = [ + { + type: 'individual', + text: t('individual'), + icon: , + }, + { + type: 'organization', + text: t('organization'), + icon: , + }, + ] + + return ( + <> + {t('type-of-organization')} + {t('please-select-organization')} + + + + ) +} diff --git a/src/components/client/campaigns/CustomHorizontalStepper.tsx b/src/components/client/campaigns/CustomHorizontalStepper.tsx new file mode 100644 index 000000000..af473be10 --- /dev/null +++ b/src/components/client/campaigns/CustomHorizontalStepper.tsx @@ -0,0 +1,62 @@ +import React, { useContext } from 'react' +import { Box, Grid, Button, Stepper, Step, StepLabel, Typography } from '@mui/material' +import { CampaignContext } from 'context/create-campaign' + +import HSCreateForm from 'components/client/campaigns/stepOne/HSCreateForm' +// TODO: Fix the Typescript errors +export default function CustomHorizontalStepper() { + const ctx = useContext(CampaignContext) + + return ( + + + {ctx.steps.map((step, index) => { + const stepProps: { completed?: boolean } = {} + const labelProps: { + optional?: React.ReactNode + } = {} + if (ctx.isStepOptional && ctx.isStepOptional(index)) { + labelProps.optional = Optional + } + if (ctx.isStepSkipped && ctx.isStepSkipped(index)) { + stepProps.completed = false + } + return ( + + {step.title} + + ) + })} + + + {ctx.activeStep === ctx.steps.length ? ( + <> + All steps completed - you're finished + + ) : ( + <> + {ctx.activeStep === 0 && } + {ctx.activeStep === 1 && Page {ctx.activeStep}} + {ctx.activeStep === 2 && Page {ctx.activeStep}} + {ctx.activeStep === 3 && Page {ctx.activeStep}} + {ctx.activeStep === 4 && Page {ctx.activeStep}} + + {/* Action Buttons */} + {/* + + + + + + + + */} + + )} + + ) +} diff --git a/src/components/client/campaigns/HSCreateCampaignPage.tsx b/src/components/client/campaigns/HSCreateCampaignPage.tsx new file mode 100644 index 000000000..333c3133b --- /dev/null +++ b/src/components/client/campaigns/HSCreateCampaignPage.tsx @@ -0,0 +1,61 @@ +import { Box, Grid, Button } from '@mui/material' +import { CategoryType } from 'gql/types' +import { useTranslation } from 'next-i18next' +import { useRouter } from 'next/router' +import { useState, useEffect } from 'react' + +import React from 'react' +import Layout from 'components/client/layout/Layout' +import CreateCampaignSteps from './CreateCampaignSteps' +import CreateCampaignUserType from './CreateCampaignUserType' +import ChevronRightIcon from '@mui/icons-material/ChevronRight' + +// TODO: MOBILE VIEW +// TODO add User type + +// type User = {} + +export default function CreateCampaignPage() { + const { t } = useTranslation('campaigns') + const router = useRouter() + // const [user, setUser] = useState({}) + const [user, setUser] = useState(null) + + const userTypeHandler = (selectedUser: CategoryType) => { + // TODO: store the selection on redirect + setUser(selectedUser) //it shouldn't be null + } + + const goToNextPage = () => router.push('/campaigns/create/steps') + const goBackToPrevPage = () => router.back() + + return ( + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/components/client/campaigns/HSCreateCampaignStepsPage.tsx b/src/components/client/campaigns/HSCreateCampaignStepsPage.tsx new file mode 100644 index 000000000..1f151e31a --- /dev/null +++ b/src/components/client/campaigns/HSCreateCampaignStepsPage.tsx @@ -0,0 +1,22 @@ +import { Box } from '@mui/material' +import { useTranslation } from 'next-i18next' + +import React from 'react' +import Layout from 'components/client/layout/Layout' +import CustomHorizontalStepper from './CustomHorizontalStepper' +import HSCreateForm from './stepOne/HSCreateForm' + +import { CampaignContext, CampaignProvider } from 'context/create-campaign' +import CampaignStepper from './CampaignStepper' + +export default function HSCreateCampaignStepsPage() { + return ( + + + + + + + + ) +} diff --git a/src/components/client/campaigns/ListIconButtons.tsx b/src/components/client/campaigns/ListIconButtons.tsx new file mode 100644 index 000000000..40a269877 --- /dev/null +++ b/src/components/client/campaigns/ListIconButtons.tsx @@ -0,0 +1,87 @@ +import { styled } from '@mui/material/styles' +import { IconButton, ImageList, ImageListItem, Typography } from '@mui/material' +import { Category } from '@mui/icons-material' +import { CategoryType } from 'gql/types' + +const Root = styled('div')(() => ({ + [`& .iconButton`]: { + display: 'block', + height: '90px', + borderRadius: 0, + borderBottom: '1px solid transparent', + position: 'relative', + '&:before': { + content: '""', + position: 'absolute', + bottom: '0', + left: '0', + width: '100%', + height: '0', + backgroundColor: '#4AC3FF', + transition: 'height .2s', + }, + + '&:hover': { + backgroundColor: 'white', + color: '#4AC3FF', + }, + '&:hover:before': { + height: '5px', + }, + '&:focus': { + color: '#4AC3FF', + }, + '&:focus:before': { + height: '5px', + }, + }, +})) + +const IconButtonText = styled(Typography)(() => ({ + fontFamily: 'Raleway, sans-serif', +})) + +type Props = { + data: CategoryType[] + onClick: (item: CategoryType) => void + cols?: number + rowHeight?: number + gap?: number + style?: React.CSSProperties + styleItem?: React.CSSProperties +} + +export default function ListIconButtons({ + data, + onClick, + cols, + rowHeight, + gap, + style, + styleItem = { display: 'inline', margin: '0 auto' }, +}: Props) { + return ( + + + {data.map((item) => { + const hasCountProperty = Object.keys(item).includes('count') + + return ( + + onClick(item)}> + {item.icon ?? } + + + {item.text} {hasCountProperty && `(${item.count})`} + + + + ) + })} + + + ) +} diff --git a/src/components/client/campaigns/campaigns.styled.tsx b/src/components/client/campaigns/campaigns.styled.tsx new file mode 100644 index 000000000..b62e47963 --- /dev/null +++ b/src/components/client/campaigns/campaigns.styled.tsx @@ -0,0 +1,19 @@ +import { styled } from '@mui/system' +import { Typography } from '@mui/material' + +import theme from 'common/theme' + +export const Heading = styled(Typography)(() => ({ + fontWeight: 500, + marginBottom: theme.spacing(2), + fontFamily: 'Montserrat, sans-serif', + fontSize: theme.typography.pxToRem(25), +})) + +export const SectionHeading = styled(Typography)(() => ({ + fontWeight: 500, + marginBottom: theme.spacing(2), + fontFamily: 'Montserrat, sans-serif', + fontSize: theme.typography.pxToRem(35), + lineHeight: theme.typography.pxToRem(45), +})) diff --git a/src/components/client/campaigns/stepOne/BeneficiarySelect.tsx b/src/components/client/campaigns/stepOne/BeneficiarySelect.tsx new file mode 100644 index 000000000..fecfe1c35 --- /dev/null +++ b/src/components/client/campaigns/stepOne/BeneficiarySelect.tsx @@ -0,0 +1,35 @@ +import { FormControl, FormHelperText, InputLabel, MenuItem, Select } from '@mui/material' +import { TranslatableField, translateError } from 'common/form/validation' +import { useField } from 'formik' +import { useTranslation } from 'react-i18next' +import { useBeneficiariesList } from 'service/beneficiary' + +export default function BeneficiarySelect({ name = 'beneficiaryId' }) { + const { t } = useTranslation() + const { data } = useBeneficiariesList() + const [field, meta] = useField(name) + + const helperText = meta.touched ? translateError(meta.error as TranslatableField, t) : '' + return ( + + {t('campaigns:beneficiary')} + + {helperText && {helperText}} + + ) +} diff --git a/src/components/client/campaigns/stepOne/CoordinatorSelect.tsx b/src/components/client/campaigns/stepOne/CoordinatorSelect.tsx new file mode 100644 index 000000000..f63dc8361 --- /dev/null +++ b/src/components/client/campaigns/stepOne/CoordinatorSelect.tsx @@ -0,0 +1,33 @@ +import { FormControl, FormHelperText, InputLabel, MenuItem, Select } from '@mui/material' +import { TranslatableField, translateError } from 'common/form/validation' +import { useCoordinatorsList } from 'common/hooks/coordinators' +import { useField } from 'formik' +import { useTranslation } from 'react-i18next' + +export default function CoordinatorSelect({ name = 'coordinatorId' }) { + const { t } = useTranslation() + const { data } = useCoordinatorsList() + const [field, meta] = useField(name) + + const helperText = meta.touched ? translateError(meta.error as TranslatableField, t) : '' + return ( + + {t('campaigns:coordinator')} + + {helperText && {helperText}} + + ) +} diff --git a/src/components/client/campaigns/stepOne/HSCreateForm.tsx b/src/components/client/campaigns/stepOne/HSCreateForm.tsx new file mode 100644 index 000000000..7221c3b17 --- /dev/null +++ b/src/components/client/campaigns/stepOne/HSCreateForm.tsx @@ -0,0 +1,241 @@ +/** + * TODO: + * - fix the typescript errors on the functions + * - fix the beneficiary data in the context + * - fix the validations + * - store the data in localStorage in case the user refreshes the page + */ + +import { useState, useContext } from 'react' +import { useTranslation } from 'next-i18next' +import { Button, Grid, FormControl, RadioGroup, FormControlLabel, Radio } from '@mui/material' +import { Heading, SectionHeading } from '../campaigns.styled' +import { CampaignState } from '../../../client/campaigns/helpers/campaign.enums' +import ChevronRightIcon from '@mui/icons-material/ChevronRight' + +// Validations +import * as yup from 'yup' +import { parse, isDate } from 'date-fns' + +// Types +import { + // CampaignResponse, + // CampaignInput, + // CampaignUploadImage, + CampaignAdminCreateFormData, +} from 'gql/campaigns' +import { Currency } from 'gql/currency' +import { CategoryType } from 'gql/types' +import { CampaignTypeCategory } from 'components/common/campaign-types/categories' + +// Components +import GenericForm from 'components/common/form/GenericForm' +import FormTextField from 'components/common/form/FormTextField' +import CampaignFilter from '../CampaignFilter' + +import { CampaignContext } from 'context/create-campaign' + +// Validation helpers +const formatString = 'yyyy-MM-dd' + +const parseDateString = (value: string, originalValue: string) => { + const parsedDate = isDate(originalValue) + ? originalValue + : parse(originalValue, formatString, new Date()) + + return parsedDate +} + +// Campaigns + +export default function CampaignForm() { + const { t } = useTranslation() + + const ctx = useContext(CampaignContext) + const campaignInfo = ctx.campaignData.info + + const onSubmit = () => { + console.log('Form submitted') + } + + const initialValues = {} + + // Validations + // TODO: ADD validation for type of campaign + const validationSchema: yup.SchemaOf = yup + .object() + .defined() + .shape({ + title: yup.string().trim().min(10).max(200).required(), + slug: yup.string().trim().min(10).max(200).optional(), + description: yup.string().trim().min(50).max(60000).required(), + targetAmount: yup.number().integer().positive().required(), + allowDonationOnComplete: yup.bool().optional(), + campaignTypeId: yup.string().uuid().required(), + beneficiaryId: yup.string().uuid().required(), + coordinatorId: yup.string().uuid().required(), + organizerId: yup.string().uuid().required(), + startDate: yup.date().transform(parseDateString).required(), + state: yup.mixed().oneOf(Object.values(CampaignState)).required(), + endDate: yup + .date() + .transform(parseDateString) + .min(yup.ref('startDate'), `end date can't be before start date`), + terms: yup.bool().required().oneOf([true], 'validation:terms-of-use'), + gdpr: yup.bool().required().oneOf([true], 'validation:terms-of-service'), + currency: yup.mixed().oneOf(Object.values(Currency)).required(), + }) + + // type of campaign + const [selectedCampaignType, setSelectedCampaignType] = useState( + CampaignTypeCategory.all, + ) + const selectedCampaignTypeHandler = (type: CategoryType) => { + setSelectedCampaignType(type) + } + + return ( + + + {t('campaigns:steps.step1-type')} + + + {/* FORM */} + + + {/* Campaign Name */} + + + {t('campaigns:steps.step1-type')} + + + + {/* Campaign Type */} + + {t('campaigns:campaignType')} + + + {/* Campaign Beneficiary */} + + Бенфициент на кампанията + + + } label="Кампанията е лична" /> + } + label="Кампанията се организира за друг бенефициент - физическо лице" + /> + + + + + + + + {/* Amount */} + + + {t('campaigns:campaign.amount')} + + + + + + {/* End of the campaign */} + + + Желана крайна дата на кампанията: + + + } + label="До събиране на необходимите средства" + /> + } + label="Ежемесечна кампания" + /> + } label="До дата" /> + + + + {/* TODO: Show this field if 'specific-date' is selected */} + + + + {/* Action Buttons */} + + + + + + + + + + + + + ) +} diff --git a/src/components/client/campaigns/stepOne/OrganizerSelect.tsx b/src/components/client/campaigns/stepOne/OrganizerSelect.tsx new file mode 100644 index 000000000..40a6503fe --- /dev/null +++ b/src/components/client/campaigns/stepOne/OrganizerSelect.tsx @@ -0,0 +1,37 @@ +import { FormControl, FormHelperText, InputLabel, MenuItem, Select } from '@mui/material' +import { TranslatableField, translateError } from 'common/form/validation' +import { useOrganizersList } from 'common/hooks/organizer' +import { useField } from 'formik' +import { useTranslation } from 'react-i18next' + +export default function OrganizerSelect({ name = 'organizerId', label = 'campaigns:organizer' }) { + const { t } = useTranslation() + const { data } = useOrganizersList() + const [field, meta] = useField(name) + + const helperText = meta.touched ? translateError(meta.error as TranslatableField, t) : '' + if (!data) { + return null + } + + return ( + + {t(label)} + + {helperText && {helperText}} + + ) +} diff --git a/src/components/client/layout/Layout.tsx b/src/components/client/layout/Layout.tsx index fbb64ebf8..3e5ebb98d 100644 --- a/src/components/client/layout/Layout.tsx +++ b/src/components/client/layout/Layout.tsx @@ -28,6 +28,7 @@ type LayoutProps = React.PropsWithChildren< boxProps?: BoxProps metaTitle?: string metaDescription?: string + minHeight?: string profilePage?: boolean canonicalUrl?: string prevPage?: string @@ -40,6 +41,7 @@ export default function Layout({ ogImage, children, maxWidth = 'lg', + minHeight = '100vh', disableOffset = false, hideFooter = false, canonicalUrl, @@ -65,7 +67,7 @@ export default function Layout({ disableGutters sx={{ backgroundColor: profilePage ? '#E9F6FF' : '' }}> diff --git a/src/components/client/layout/nav/DonationMenu.tsx b/src/components/client/layout/nav/DonationMenu.tsx index 1892df88b..2831155b9 100644 --- a/src/components/client/layout/nav/DonationMenu.tsx +++ b/src/components/client/layout/nav/DonationMenu.tsx @@ -40,7 +40,8 @@ const allNavItems: NavItem[] = [ label: 'nav.campaigns.all-campaigns', }, { - href: routes.faq_campaigns, //temporarily lead to FAQ + href: routes.campaigns.create, //temporarily lead to FAQ + label: 'nav.campaigns.create', }, ] diff --git a/src/components/client/one-time-donation/Steps.tsx b/src/components/client/one-time-donation/Steps.tsx index 1b154e0a2..8cb7ff634 100644 --- a/src/components/client/one-time-donation/Steps.tsx +++ b/src/components/client/one-time-donation/Steps.tsx @@ -63,10 +63,6 @@ export default function DonationStepper({ onStepChange }: DonationStepperProps) if (isLoading || !data) return const { campaign } = data - function isLogged() { - return session && session.accessToken ? true : false - } - initialValues.isRecurring = false const userEmail = session?.user?.email diff --git a/src/components/common/campaign-types/categories.ts b/src/components/common/campaign-types/categories.ts index bd2597ccc..14d5fa3b2 100644 --- a/src/components/common/campaign-types/categories.ts +++ b/src/components/common/campaign-types/categories.ts @@ -10,4 +10,5 @@ export enum CampaignTypeCategory { animals = 'animals', nature = 'nature', others = 'others', + all = 'all', } diff --git a/src/context/create-campaign.tsx b/src/context/create-campaign.tsx new file mode 100644 index 000000000..8b52c990b --- /dev/null +++ b/src/context/create-campaign.tsx @@ -0,0 +1,259 @@ +import React, { useState, createContext } from 'react' +import { useTranslation } from 'react-i18next' + +import type { CampaignTypeCategory } from 'components/common/campaign-types/categories' +import { UUID } from 'gql/types' +import { BeneficiaryType } from '../components/admin/beneficiary/BeneficiaryTypes' +import { CampaignFile } from 'gql/campaigns' + +// TODO: Extract the different types +type CampaignDataType = { + info: { + name: string + category: CampaignTypeCategory | '' + beneficiary: { + id: UUID + type: BeneficiaryType + name: string + } + targetAmount: number + // endDate?: Date + endDate?: 'one-time' | 'each-month' | 'specific-date' //temporary + } + organizer: { + person: { + id: UUID + firstName: string + middleName: string + lastName: string + pin: string + phoneNumber: string + } + company: { + id: UUID + name: string + address: string + bulstatUIC: string + phoneNumber: string + representative: { + firstName: string + middleName: string + lastName: string + } + } + } + about: { + organizer: string + campaign: string + completed: string + personalStories: string + guarantor?: { + name: string + profession: string + avatar?: string // not sure + info: string + } + webpage: string + links: string + facebook: string + } + documents: { + video: string //not sure + pictures: string //not sure + files: CampaignFile[] + } +} + +type Props = { + children: React.ReactNode +} + +type Label = { title: string } + +type StepsCampaignContext = { + activeStep: number + steps: Label[] + nextPage?: (event: React.MouseEvent) => void + prevPage?: (event: React.MouseEvent) => void + campaignData: CampaignDataType + isStepOptional?: (step: number) => boolean + isStepSkipped?: (step: number) => boolean + handleChange?: (event: React.MouseEvent) => void + setCampaignInfo?: (event: React.MouseEvent) => void + setOrganizer?: (event: React.MouseEvent) => void + setAboutCampaign?: (event: React.MouseEvent) => void + setDocuments?: (event: React.MouseEvent) => void +} + +export const CampaignContext = createContext({} as StepsCampaignContext) + +export const CampaignProvider = (props: Props) => { + const { t } = useTranslation() + const [campaignData, setCampaignData] = useState({ + info: { + name: '', + category: 'all', + beneficiary: { + id: '', + type: '', + name: '', + }, + targetAmount: 0, + endDate: new Date(), + }, + organizer: { + person: { + id: '', + firstName: '', + middleName: '', + lastName: '', + pin: '', + phoneNumber: '', + }, + campany: { + id: '', + name: '', + address: '', + bulstatUIC: '', + phoneNumber: '', + representative: { + firstName: '', + middleName: '', + lastName: '', + }, + }, + }, + about: { + organizer: '', + campaign: '', + completed: '', + personalStories: '', + guarantor: { + name: '', + profession: '', + avatar: '', + info: '', + }, + webpage: '', + links: '', + facebook: '', + }, + documents: { + video: '', + pictures: '', + files: '', + }, + }) + + const [activeStep, setActiveStep] = useState(0) + const [skipped, setSkipped] = useState(new Set()) + + const steps = [ + { title: t('campaigns:steps.step1-type') }, + { title: t('campaigns:steps.step2-type') }, + { title: t('campaigns:steps.step3-type') }, + { title: t('campaigns:steps.step4-type') }, + { title: t('campaigns:steps.step5-type') }, + ] + + /** + * Optional Steps / Skip + **/ + const optionalSteps: number[] = [] + + const isStepOptional = (step: number) => { + return optionalSteps.includes(step) + } + + const isStepSkipped = (step: number) => { + return skipped.has(step) + } + + const handleSkip = () => { + if (!isStepOptional(activeStep)) { + throw new Error("You can't skip a step that isn't optional.") + } + + setActiveStep((prevActiveStep) => prevActiveStep + 1) + + setSkipped((prevSkipped) => { + const newSkipped = new Set(prevSkipped.values()) + newSkipped.add(activeStep) + return newSkipped + }) + } + + /** + * Prev / Next Actions + **/ + const nextPage = () => { + let newSkipped = skipped + + if (isStepSkipped(activeStep)) { + newSkipped = new Set(newSkipped.values()) + newSkipped.delete(activeStep) + } + + setActiveStep((prevActiveStep) => prevActiveStep + 1) + setSkipped(newSkipped) + + console.log('next page: ', campaignData.info) + } + + const prevPage = () => { + setActiveStep((prevActiveStep) => prevActiveStep - 1) + } + + /** + * Campaign Data Actions + **/ + const handleChange = (prop: string) => (event: React.ChangeEvent) => { + setCampaignData({ ...campaignData, [prop]: event.target.value }) + } + + const setCampaignInfo = (prop: string) => (event: React.ChangeEvent) => { + setCampaignData({ + ...campaignData, + info: { ...campaignData.info, [prop]: event.target.value }, + }) + } + const setOrganizer = (prop: string) => (event: React.ChangeEvent) => { + setCampaignData({ + ...campaignData, + organizer: { ...campaignData.organizer, [prop]: event.target.value }, + }) + } + const setAboutCampaign = (prop: string) => (event: React.ChangeEvent) => { + setCampaignData({ + ...campaignData, + about: { ...campaignData.about, [prop]: event.target.value }, + }) + } + const setDocuments = (prop: string) => (event: React.ChangeEvent) => { + setCampaignData({ + ...campaignData, + documents: { ...campaignData.documents, [prop]: event.target.value }, + }) + } + + return ( + + {props.children} + + ) +} diff --git a/src/gql/types.d.ts b/src/gql/types.d.ts index 760a0ec5a..c56a2b9eb 100644 --- a/src/gql/types.d.ts +++ b/src/gql/types.d.ts @@ -13,3 +13,11 @@ export type FilterData = { maxAmount: number sortBy: string } + +export type CategoryType = { + text: string + type: CampaignTypeCategory + count?: number + isDisabled?: boolean + icon: ReactElement +} diff --git a/src/pages/campaigns/create.tsx b/src/pages/campaigns/create/index.tsx similarity index 72% rename from src/pages/campaigns/create.tsx rename to src/pages/campaigns/create/index.tsx index 1a6ec0e12..1a2edda62 100644 --- a/src/pages/campaigns/create.tsx +++ b/src/pages/campaigns/create/index.tsx @@ -1,5 +1,6 @@ import { GetServerSideProps } from 'next' -import CreateCampaignPage from 'components/client/campaigns/CreateCampaignPage' +import HSCreateCampaignPage from 'components/client/campaigns/HSCreateCampaignPage' + import { securedPropsWithTranslation } from 'middleware/auth/securedProps' import { routes } from 'common/routes' @@ -8,4 +9,4 @@ export const getServerSideProps: GetServerSideProps = securedPropsWithTranslatio routes.campaigns.create, ) -export default CreateCampaignPage +export default HSCreateCampaignPage diff --git a/src/pages/campaigns/create/steps.tsx b/src/pages/campaigns/create/steps.tsx new file mode 100644 index 000000000..8db20775c --- /dev/null +++ b/src/pages/campaigns/create/steps.tsx @@ -0,0 +1,12 @@ +import { GetServerSideProps } from 'next' +import HSCreateCampaignStepsPage from 'components/client/campaigns/HSCreateCampaignStepsPage' + +import { securedPropsWithTranslation } from 'middleware/auth/securedProps' +import { routes } from 'common/routes' + +export const getServerSideProps: GetServerSideProps = securedPropsWithTranslation( + ['common', 'auth', 'validation', 'campaigns'], + routes.campaigns.steps, +) + +export default HSCreateCampaignStepsPage