diff --git a/public/PolygonCampaign.png b/public/PolygonCampaign.png new file mode 100644 index 00000000..7fa325ce Binary files /dev/null and b/public/PolygonCampaign.png differ diff --git a/public/success-image.png b/public/success-image.png new file mode 100644 index 00000000..1fbd4b85 Binary files /dev/null and b/public/success-image.png differ diff --git a/src/Navigator.tsx b/src/Navigator.tsx index b2faa4b3..14193226 100644 --- a/src/Navigator.tsx +++ b/src/Navigator.tsx @@ -21,6 +21,13 @@ const RewardsScreen = lazy( })), ) +const FeedbackScreen = lazy( + async () => + await import('./components/screens/FeedbackScreen/FeedbackScreen').then(module => ({ + default: module.FeedbackScreen, + })), +) + export const Navigator = () => { const [user, setUser] = useState(null) @@ -56,6 +63,14 @@ export const Navigator = () => { } /> + }> + + + } + /> } /> } /> diff --git a/src/components/cards/PoolCard/swapExecution/executeDeposit.ts b/src/components/cards/PoolCard/swapExecution/executeDeposit.ts index 84568ce2..ee598a58 100644 --- a/src/components/cards/PoolCard/swapExecution/executeDeposit.ts +++ b/src/components/cards/PoolCard/swapExecution/executeDeposit.ts @@ -128,7 +128,7 @@ const handleDepositTransaction = async ( }) const depositRequestId = decodedLog.args.requestId - await sleep(35_000) + await sleep(25_000) await completeDeposit(swapState, swapDispatch, depositRequestId, walletClient, publicClient) } diff --git a/src/components/rewards/Quests/QuestModal/QuestModal.tsx b/src/components/rewards/Quests/QuestModal/QuestModal.tsx index 25c24eb8..5de594bb 100644 --- a/src/components/rewards/Quests/QuestModal/QuestModal.tsx +++ b/src/components/rewards/Quests/QuestModal/QuestModal.tsx @@ -95,16 +95,18 @@ export const QuestModal = ({
- {steps.map(step => ( - - ))} + {steps.map(step => { + return ( + + ) + })}
) diff --git a/src/components/rewards/Quests/QuestsGroup/QuestsGroup.tsx b/src/components/rewards/Quests/QuestsGroup/QuestsGroup.tsx index 4eecd5b2..764ad582 100644 --- a/src/components/rewards/Quests/QuestsGroup/QuestsGroup.tsx +++ b/src/components/rewards/Quests/QuestsGroup/QuestsGroup.tsx @@ -6,19 +6,26 @@ import type { IUser } from '../../../../api/concero/user/userType' import { QuestCard } from '../QuestCard/QuestCard' import { type IUserAction } from '../../../../api/concero/userActions/userActionType' import { fetchUserQuestActions } from '../../../../api/concero/userActions/fetchUserQuestActions' +import { useAccount } from 'wagmi' interface QuestsCardProps { user: IUser | null | undefined } export const QuestsGroup = ({ user }: QuestsCardProps) => { + const { address } = useAccount() const [quests, setQuests] = useState([]) const handleGetQuests = async () => { let userQuestActions: IUserAction[] = [] - if (user) { - userQuestActions = await fetchUserQuestActions(user.address) + + if (!user && !address) { + setQuests(await fetchQuests()) } + + if (!user) return + + userQuestActions = await fetchUserQuestActions(user.address) const fetchedQuests = await fetchQuests() const newQuests = fetchedQuests.map(quest => { diff --git a/src/components/screens/FeedbackScreen/Button/Button.module.pcss b/src/components/screens/FeedbackScreen/Button/Button.module.pcss new file mode 100644 index 00000000..f51c54ab --- /dev/null +++ b/src/components/screens/FeedbackScreen/Button/Button.module.pcss @@ -0,0 +1,229 @@ +.button { + flex: none; + align-items: center; + border: 1px solid var(--color-primary-350); + border-radius: var(--st-br-md); + color: var(--color-text-primary); + cursor: pointer; + display: flex; + flex-direction: row; + gap: var(--sp-sm); + justify-content: center; + padding: var(--sp-md) var(--sp-md); + transform-origin: center; + transition: + transform 0.2s, + 0.25s, + border-color 0.25s, + color 0.25s; + font-weight: 400; +} + +.button:hover { + background-color: var(--color-primary-500); + border-color: #8f5df9; + transform: translateY(-2px); + box-shadow: 0 4px 18px 0 rgb(115 53 247 / 30%); +} + +.button:active { + background-color: var(--color-primary-600); + border-color: var(--color-primary-500); + transform: scale(0.98); +} + +.xs { + font-size: 0.875rem; + gap: var(--sp-xs); + padding: var(--sp-xs) var(--sp-sm); + min-height: 28px; +} + +.sm { + font-size: 0.875rem; + gap: var(--sp-sm); + padding: var(--sp-sm) var(--sp-sm); + min-height: 36px; +} + +.md { + font-size: 20px; + gap: var(--sp-md); + min-height: 44px; + border-radius: var(--st-br-md); +} + +.lg { + font-size: 20px; + padding: var(--sp-md) var(--sp-lg); + border-radius: var(--st-br-lg); + min-height: 56px; +} + +.xl { + font-size: 2rem; + padding: var(--sp-sm) var(--sp-sm); +} + +.sq-xs { + border-radius: var(--st-br-sm); + padding: var(--sp-xs) var(--sp-xs); +} + +.sq-sm { + padding: var(--sp-sm) var(--sp-sm); +} + +.sq-md { + padding: var(--sp-md) var(--sp-md); +} + +.sq-lg { + padding: var(--sp-lg) var(--sp-lg); +} + +.primary { + background: #7335f7; + border-color: transparent; + color: var(--color-base-white); +} + +.secondary { + background-color: var(--color-primary-700); + border-color: var(--color-primary-650); + color: var(--color-primary-400); +} + +.subtle { + background-color: var(--color-grey-650); + border: 1px solid var(--color-grey-600); + color: var(--color-text-secondary); +} + +.subtle:hover { + background-color: var(--color-grey-600); + border-color: var(--color-grey-550); +} + +.subtle:active { + background-color: var(--color-grey-650); +} + +.isLoading { + background-color: var(--color-primary-650); + border-color: var(--color-primary-700); + cursor: wait; +} + +.isLoading:hover { + background-color: var(--color-primary-650); + border-color: var(--color-primary-700); + cursor: wait; +} + +.isDisabled { + background: none; + background-color: var(--color-primary-750); + border-color: var(--color-primary-650); + color: var(--color-primary-400); + cursor: not-allowed; + pointer-events: none; +} + +.isDisabled:hover { + background: none; + background-color: var(--color-primary-750); + border-color: var(--color-primary-650); +} + +.isDisabled.subtle { + background-color: var(--color-grey-650); + border-color: var(--color-grey-600); + color: var(--color-grey-550); +} + +.pressable-down:active { + transform: translateY(2px); +} + +.black { + background-color: transparent; + border: 1px solid transparent; + color: var(--color-text-secondary); +} + +.black:hover { + background-color: var(--color-grey-650); + border: 1px solid transparent; + box-shadow: unset; + transform: unset; +} + +.black:active { + background-color: var(--color-grey-600); + border: 1px solid transparent; +} + +.filled { + background-color: var(--color-primary-500); + border: none; + color: var(--color-text-primary); +} + +.secondary:hover { + background-color: var(--color-primary-650); + border-color: var(--color-primary-500); +} + +.light { + border-radius: var(--st-br-xl); + border: 1px solid rgb(from var(--color-primary-700) r g b / 20%); + background: var(--color-primary-750); + color: var(--color-primary-550); +} + +.light:hover { + background: var(--color-primary-750); + border: 1px solid rgb(from var(--color-primary-700) r g b / 20%); + border-radius: var(--st-br-xl); +} + +.convex { + border-radius: var(--st-br-md); + border: 1px solid var(--color-grey-600); + background: var(--color-base-white); + color: var(--color-text-secondary); +} + +:global(.dark) .convex { + background: var(--color-grey-600); +} + +:global(.dark) .isDisabled.convex { + background: var(--color-base-black); +} + +.convex:hover { + background: var(--color-base-white); + border: 1px solid var(--color-grey-600); + color: var(--color-text-secondary); + border-radius: var(--st-br-md); + box-shadow: 0 4px 12px 0 rgb(0 0 0 / 5%); + transform: translateY(-2px); +} + +:global(.dark) .convex:hover { + background: var(--color-grey-625); +} + +.transparent { + background-color: transparent; + border: solid 1px var(--color-base-black); +} + +.transparent:hover { + background-color: transparent; + border: solid 1px var(--color-base-black); + transform: unset; + box-shadow: none; +} diff --git a/src/components/screens/FeedbackScreen/Button/Button.tsx b/src/components/screens/FeedbackScreen/Button/Button.tsx new file mode 100644 index 00000000..90469411 --- /dev/null +++ b/src/components/screens/FeedbackScreen/Button/Button.tsx @@ -0,0 +1,31 @@ +import { type FC } from 'react' +import { type ButtonProps } from './types' +import { getButtonClasses } from './getButtonClasses' +import classNames from './Button.module.pcss' + +export const Button: FC = ({ + size = 'md', + variant = 'primary', + leftIcon, + rightIcon, + isLoading, + isDisabled, + children, + onClick, + className, +}) => { + const buttonClasses = getButtonClasses(size, variant, isLoading, isDisabled, className) + + return ( + + ) +} diff --git a/src/components/screens/FeedbackScreen/Button/getButtonClasses.tsx b/src/components/screens/FeedbackScreen/Button/getButtonClasses.tsx new file mode 100644 index 00000000..4364f106 --- /dev/null +++ b/src/components/screens/FeedbackScreen/Button/getButtonClasses.tsx @@ -0,0 +1,17 @@ +import { type ButtonProps } from './types' +import styles from './Button.module.pcss' + +export function getButtonClasses( + size: ButtonProps['size'], + variant: ButtonProps['variant'], + isLoading: ButtonProps['isLoading'], + isDisabled: ButtonProps['isDisabled'], + className: ButtonProps['className'], +) { + const baseClasses = [styles.button] + const sizeClass = size ? styles[size] : '' + const variantClass = variant ? styles[variant] : '' + const isLoadingClass = isLoading ? styles.isLoading : '' + const isDisabledClass = isDisabled ? styles.isDisabled : '' + return baseClasses.concat(sizeClass, variantClass, isLoadingClass, isDisabledClass, className).join(' ') +} diff --git a/src/components/screens/FeedbackScreen/Button/types.ts b/src/components/screens/FeedbackScreen/Button/types.ts new file mode 100644 index 00000000..d2050734 --- /dev/null +++ b/src/components/screens/FeedbackScreen/Button/types.ts @@ -0,0 +1,13 @@ +import { type MouseEventHandler, type ReactNode } from 'react' + +export interface ButtonProps { + size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'sq-xs' | 'sq-sm' | 'sq-md' | 'sq-lg' | 'sq-xl' + variant?: 'primary' | 'secondary' | 'filled' | 'subtle' | 'black' | 'light' | 'convex' | 'transparent' + leftIcon?: ReactNode + rightIcon?: ReactNode + onClick?: MouseEventHandler | undefined + isLoading?: boolean + isDisabled?: boolean + className?: string + children?: ReactNode +} diff --git a/src/components/screens/FeedbackScreen/FeedbackScreen.module.pcss b/src/components/screens/FeedbackScreen/FeedbackScreen.module.pcss new file mode 100644 index 00000000..a4db51d2 --- /dev/null +++ b/src/components/screens/FeedbackScreen/FeedbackScreen.module.pcss @@ -0,0 +1,194 @@ +.feedbackScreenContainer { + width: 100%; + height: 100%; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 0 var(--sp-xl) var(--sp-xl) var(--sp-xl); +} + +.formContainer { + padding: var(--sp-xxl); +} + +.cardHeadingContainer { + padding-bottom: var(--sp-md); +} + +.titleContainer { + h2 { + color: var(--color-grey-700); + font-size: var(--sp-xl); + font-weight: 500; + } +} + +p { + font-size: 14px; + color: var(--color-grey-500); +} + +.cersHighlighting { + color:; +} + +.inputsContainer { + display: flex; + flex-direction: column; + gap: 16px; + padding: var(--sp-lg) 0 0 0; +} + +.card { + padding: var(--sp-xl); + min-width: 441px; + min-height: 654px; + display: flex; + border-radius: var(--sp-xl); +} + +.titleContainer { + gap: var(--sp-sm); + + p { + max-width: 500px; + } +} + +.inputContainer { + display: flex; + flex-direction: column; + padding-top: var(--sp-xxl); + gap: var(--sp-xxl); +} + +.productSelectContainer { + display: flex; + gap: 8px; + + p { + z-index: 1; + color: var(--color-grey-700); + font-size: 14px; + font-weight: 500; + max-width: 393px; + } +} + +.errorIcon { + margin-right: 8px; + vertical-align: middle; +} + +.errorMessage { + display: flex; + flex-direction: row; + //align-items:; + color: var(--color-danger-700); + font-size: 14px; +} + +.inputError { + border: 1px solid var(--color-danger-500); +} + +.messageInput { + background-color: var(--color-grey-100); + border-radius: var(--st-br-md); + padding: var(--sp-md) var(--sp-lg); + font-size: var(--st-br-lg); + color: var(--color-grey-700); + min-height: 128px; + .placeholder { + color: var(--color-grey-500); + text-align: right; + } +} + +.hintContainer { + max-width: 393px; + font-size: 14px; + font-weight: 400; +} + +.submitButton { + background: var(--color-primary-600); + color: var(--color-base-white); + padding: 15px 24px; +} + +.imageContainer { + height: 100%; + width: 40%; + overflow: hidden; + //border-radius: var(--st-br-card); + background: linear-gradient(211deg, #0f1c2a 3.64%, #120a24 30.75%, #131216 71.22%); + position: relative; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } +} + +.buttonTitle { + width: 100%; + align-items: center; + justify-content: center; + color: var(--color-base-white); +} + +.closeButton { + position: absolute; + top: var(--sp-lg); + right: var(--sp-lg); + cursor: pointer; +} + +.closeButton:hover { + background-color: transparent; +} + +@media (width <= 768px) { + .container { + flex-direction: column-reverse; + justify-content: unset; + gap: var(--sp-xl); + overflow-y: scroll; + } + + .feedbackScreenContainer { + height: 100vh; + align-items: start; + min-height: 816px; + } + + .cardHeadingContainer { + max-width: 343px; + } + + .card { + max-width: 343px; + min-width: auto; + margin-top: var(--sp-lg); + } + + .inputsContainer { + max-width: 343px; + } + + .formContainer { + padding: 0; + } + + .imageContainer { + width: 100%; + height: 250px; + } + + .inputContainer { + gap: var(--sp-lg); + padding-top: var(--sp-lg); + } +} diff --git a/src/components/screens/FeedbackScreen/FeedbackScreen.tsx b/src/components/screens/FeedbackScreen/FeedbackScreen.tsx new file mode 100644 index 00000000..0140e11e --- /dev/null +++ b/src/components/screens/FeedbackScreen/FeedbackScreen.tsx @@ -0,0 +1,154 @@ +import { useState } from 'react' +import axios from 'axios' +import { Card } from '../../cards/Card/Card' +import ProductSelect from '../FeedbackScreen/ProductSelect/ProductSelect' +import { Input } from './Input/Input' +import classNames from './FeedbackScreen.module.pcss' +import { Button } from '../../buttons/Button/Button' +import { SuccessModal } from './SuccessModal/SuccessModal' + +interface ContactModalProps { + show: boolean + title: string + body?: string + isMessageNeeded?: boolean +} + +const products = [{ title: 'Lanca' }, { title: 'CERP (Rewards)' }, { title: 'CELP (Pools)' }] + +export function FeedbackScreen({ show, title, body, isMessageNeeded = true }: ContactModalProps) { + const [inputs, setInputs] = useState({ + email: '', + walletAddress: '', + message: '', + }) + const [errors, setErrors] = useState({ + email: false, + walletAddress: false, + message: false, + product: false, + }) + + const [activeItem, setActiveItem] = useState<{ title: string } | null>(null) + const [isModalVisible, setIsModalVisible] = useState(false) + + async function handleSubmit(e: any) { + e.preventDefault() + + const newErrors = { + email: !inputs.email, + walletAddress: !inputs.walletAddress, + message: !inputs.message || inputs.message.length < 100, + product: !activeItem || activeItem.title === 'Select a product', + } + + setErrors(newErrors) + + if (Object.values(newErrors).some(Boolean)) { + return + } + + try { + const response = await axios({ + method: 'POST', + url: 'http://localhost:4000/api/submit_feedback', + data: { + email: inputs.email, + walletAddress: inputs.walletAddress, + product: activeItem?.title, + message: inputs.message, + }, + }) + + console.log(response.data) + + setIsModalVisible(true) + } catch (err) { + console.error('Error submitting form:', err) + // setIsModalVisible(true) + } finally { + setInputs({ + email: '', + walletAddress: '', + message: '', + }) + setActiveItem(null) + } + } + + return ( +
+ +
+
+

Leave Feedback

+
+
+
+ { + setInputs({ ...inputs, email: e.target.value }) + }} + value={inputs.email} + error={errors.email ? 'Invalid email' : ''} + className={errors.email ? classNames.inputError : ''} + /> + { + setInputs({ ...inputs, walletAddress: e.target.value }) + }} + value={inputs.walletAddress} + error={errors.walletAddress ? 'Invalid wallet address' : ''} + className={errors.walletAddress ? classNames.inputError : ''} + /> +
+

Product

+ +
+ { + setInputs({ ...inputs, message: e.target.value }) + }} + value={inputs.message} + error={errors.message ? 'Message is too short. Minimum count is 100 symbols' : ''} + className={errors.message ? classNames.inputError : ''} + style={{ paddingTop: '16px', height: '128px', textAlign: 'left' }} + /> + {errors.message ? ( + errors.message + ) : ( +
+

+ Describe your problem or suggestion. Don’t worry to be very detailed. Most relevant and + helpful feedbacks will be awarded by 25 CERs +

+
+ )} + +
+
+ { + setIsModalVisible(false) + }} + /> +
+ ) +} diff --git a/src/components/screens/FeedbackScreen/Input/Input.module.pcss b/src/components/screens/FeedbackScreen/Input/Input.module.pcss new file mode 100644 index 00000000..86bb6f0a --- /dev/null +++ b/src/components/screens/FeedbackScreen/Input/Input.module.pcss @@ -0,0 +1,70 @@ +.container { + position: relative; + width: 100%; + + p { + z-index: 1; + color: var(--color-grey-700); + font-size: 14px; + padding: 0 0 var(--sp-sm); + } +} + +.input { + flex: 1; + background-color: var(--color-grey-100); + border-radius: var(--st-br-md); + padding: var(--sp-md) var(--sp-lg); + font-size: var(--st-br-lg); + color: var(--color-grey-700); + max-width: 393px; +} + +input::placeholder { + color: var(--color-grey-500); + font-size: 16px; + font-weight: 400; +} + +::placeholder { + color: var(--color-grey-500); + font-size: 16px; + font-weight: 400; +} + +.textarea { + flex: 1; + min-height: 128px; + border: none; + background-color: var(--color-grey-100); + border-radius: var(--st-br-md); + padding: var(--sp-md) var(--sp-lg); + font-size: var(--st-br-lg); + color: var(--color-grey-700); + max-width: 393px; + .placeholder { + color: var(--color-grey-500); + } +} + +.title { + z-index: 1; + color: var(--color-grey-700); + font-size: 14px; + font-weight: 500; +} + +.errorMessage { + color: var(--color-danger-700); + font-size: 14px; + margin-top: var(--st-br-sm); + font-weight: 400; + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; +} + +.inputError { + border: 1px solid var(--color-danger-700); +} diff --git a/src/components/screens/FeedbackScreen/Input/Input.tsx b/src/components/screens/FeedbackScreen/Input/Input.tsx new file mode 100644 index 00000000..9b4845b5 --- /dev/null +++ b/src/components/screens/FeedbackScreen/Input/Input.tsx @@ -0,0 +1,48 @@ +import classNames from './Input.module.pcss' +import { type FC } from 'react' +import { IconAlertCircle } from '@tabler/icons-react' + +interface InputProps { + type: 'text' | 'email' | 'password' + title?: string + placeholder: string + value?: string + onChange?: (e: React.ChangeEvent) => void + inputType?: 'input' | 'textarea' + className?: string + error?: string +} + +export const Input: FC = ({ + type, + placeholder, + value, + onChange, + title, + inputType = 'input', + className, + error, +}) => { + return ( +
+ {title ?

{title}

: null} + {inputType === 'textarea' ? ( +