Skip to content
Merged
424 changes: 132 additions & 292 deletions package-lock.json

Large diffs are not rendered by default.

354 changes: 59 additions & 295 deletions pages/claim/[type]/[code].tsx

Large diffs are not rendered by default.

134 changes: 134 additions & 0 deletions pages/profile/redeem/[code].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { useRouter } from 'next/router';
import { useState, useEffect, useContext, FC } from 'react';
import LandingSection from '../../../src/features/common/Layout/LandingSection';
import i18next from './../../../i18n';
import { UserPropsContext } from '../../../src/features/common/Layout/UserPropsContext';
import { ErrorHandlingContext } from '../../../src/features/common/Layout/ErrorHandlingContext';
import { postAuthenticatedRequest } from '../../../src/utils/apiRequests/api';
import { RedeemedCodeData } from '../../../src/features/common/types/redeem';
import {
InputRedeemCode,
SuccessfullyRedeemed,
RedeemCodeFailed,
} from '../../../src/features/common/RedeemMicro/RedeemCode';
import { ClaimCode1 } from '../../claim/[type]/[code]';

const { useTranslation } = i18next;
const ReedemCode: FC = () => {
const { t, ready } = useTranslation(['redeem']);
const { user, contextLoaded, token } = useContext(UserPropsContext);
const { handleError } = useContext(ErrorHandlingContext);

const [code, setCode] = useState<string | string[] | null>('');
const [inputCode, setInputCode] = useState<ClaimCode1>('');
const [errorMessage, setErrorMessage] = useState<ClaimCode1>('');
const [redeemedCodeData, setRedeemedCodeData] = useState<
RedeemedCodeData | undefined
>(undefined);

const router = useRouter();

const closeRedeem = () => {
if (typeof window !== 'undefined') {
router.push('/profile');
}
};

const handleCode = () => {
router.push(`/profile/redeem/${code}?inputCode=${true}`);
setErrorMessage('');
setRedeemedCodeData(undefined);
setInputCode('');
};

const changeRouteCode = () => {
router.push(`/profile/redeem/${inputCode}?inputCode=${false}`);

const codeFromUrl = router.query.code;
redeemingCode(codeFromUrl);
};

useEffect(() => {
if (contextLoaded) {
if (!user) {
localStorage.setItem(
'redirectLink',
`profile/redeem/${router.query.code}`
);
router.push(`/login`);
}
}
}, [contextLoaded, user, router]);

useEffect(() => {
if (router && router.query.code) {
setCode(router.query.code);
}
}, [router]);

useEffect(() => {
if (contextLoaded && user && router && router.query.code) {
redeemingCode(router.query.code);
}
}, [user, contextLoaded, router.query.code]);

async function redeemingCode(data: string | string[]): Promise<void> {
const submitData = {
code: data,
};

if (contextLoaded && user) {
postAuthenticatedRequest(
`/app/redeem`,
submitData,
token,
handleError
).then((res) => {
if (res.error_code === 'invalid_code') {
setErrorMessage(t('redeem:invalidCode'));
} else if (res.error_code === 'already_redeemed') {
setErrorMessage(t('redeem:alreadyRedeemed'));
} else if (res.status === 'redeemed') {
setRedeemedCodeData(res);
}
});
}
}

return ready ? (
router.query.inputCode === 'true' ? (
// to input redeem code
<LandingSection>
<InputRedeemCode
setInputCode={setInputCode}
inputCode={inputCode}
changeRouteCode={changeRouteCode}
closeRedeem={closeRedeem}
/>
</LandingSection>
) : (
//after successful redeem
<LandingSection>
{redeemedCodeData ? (
<SuccessfullyRedeemed
redeemedCodeData={redeemedCodeData}
redeemAnotherCode={handleCode}
closeRedeem={closeRedeem}
/>
) : (
// if redeem code is invalid and redeem process failed
<RedeemCodeFailed
errorMessage={errorMessage}
code={code}
redeemAnotherCode={handleCode}
closeRedeem={closeRedeem}
/>
)}
</LandingSection>
)
) : (
<></>
);
};

export default ReedemCode;
1 change: 0 additions & 1 deletion public/static/locales/en/me.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
"registerTrees": "Register Trees",
"registerTreesDescription": "Register trees that you have planted yourself, e.g. in your garden.",
"redeem": "Redeem",
"redeemDescription": "Redeem your tree voucher and other codes.",
"myForest": "My Forest",
"giftToGiftee": "Gift to {{gifteeName}}",
"settingManageProject": "Manage Projects",
Expand Down
14 changes: 8 additions & 6 deletions public/static/locales/en/redeem.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
"addToMyTrees": "Add to my trees",
"validateCode": "Validate Code",
"enterRedeemCode": "Please enter a code to redeem",
"validating": "Validating",
"invalidType": "Invalid type, please check the link again",
"successfullyRedeemed": "Successfully Redeemed",
"exitToProfile": "Exit to Profile",
"redeemCode": "Redeem Code",
"redeeming": "Redeeming:",
"invalidCode": "This code is invalid.",
"alreadyRedeemed": "The code has already been redeemed",
"invalidCode": "This code is invalid",
"redeemAnotherCode": "Redeem Another Code",
"redeeming": "Redeeming:"
"redeemCode":"Redeem Code",
"redeem": "Redeem",
"redeemDescription": "Redeem your tree voucher and other codes.",
"successfullyRedeemed": "Successfully Redeemed",
"redeemAnotherCode": "Redeem Another Code"
}

163 changes: 163 additions & 0 deletions src/features/common/RedeemMicro/RedeemCode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { ReactElement } from 'react';
import { useForm } from 'react-hook-form';
import { getFormattedNumber } from '../../../utils/getFormattedNumber';
import CancelIcon from '../../../../public/assets/images/icons/CancelIcon';
import styles from '../../../../src/features/user/Profile/styles/RedeemModal.module.scss';
import i18next from '../../../../i18n';
import MaterialTextField from '../InputTypes/MaterialTextField';
import CircularProgress from '@mui/material/CircularProgress';
import { RedeemedCodeData } from '../types/redeem';

const { useTranslation } = i18next;

export interface InputRedeemCode {
setInputCode: React.Dispatch<React.SetStateAction<string | null>>;
inputCode: string | null;
changeRouteCode: () => void;
closeRedeem: () => void;
}

export interface RedeemCodeFailed {
errorMessage: string | null;
code: string | string[] | null;
redeemAnotherCode: () => void;
closeRedeem: () => void;
}

export interface SuccessfullyRedeemed {
redeemedCodeData: RedeemedCodeData | undefined;
redeemAnotherCode: () => void;
closeRedeem: () => void;
}

export const InputRedeemCode = ({
setInputCode,
inputCode,
changeRouteCode,
closeRedeem,
}: InputRedeemCode): ReactElement => {
const { register, errors } = useForm({ mode: 'onBlur' });
const { t } = useTranslation(['redeem']);

return (
<div className={styles.routeRedeemModal}>
<div className={styles.crossDiv}>
<button className={styles.crossWidth} onClick={closeRedeem}>
<CancelIcon />
</button>
</div>

<div style={{ fontWeight: 'bold' }}>{t('redeem:redeem')}</div>
<div className={styles.note}>{t('redeem:redeemDescription')}</div>
<div className={styles.inputField}>
<MaterialTextField
inputRef={register({
required: {
value: true,
message: t('redeem:enterRedeemCode'),
},
})}
onChange={(event) => {
setInputCode(event.target.value);
}}
value={inputCode}
name={'code'}
placeholder="XAD-1SA-5F1-A"
label=""
variant="outlined"
/>
</div>

{errors.code && (
<span className={styles.formErrors}>{errors.code.message}</span>
)}

<div style={{ paddingTop: '30px' }}>
<button className={'primaryButton'} onClick={changeRouteCode}>
{t('redeem:redeemCode')}
</button>
</div>
</div>
);
};

export const RedeemCodeFailed = ({
errorMessage,
code,
redeemAnotherCode,
closeRedeem,
}: RedeemCodeFailed): ReactElement => {
const { t } = useTranslation(['redeem']);

return (
<div className={styles.routeRedeemModal}>
<div className={styles.crossDiv}>
<button className={styles.crossWidth} onClick={closeRedeem}>
<CancelIcon />
</button>
</div>
{errorMessage ? (
<div className={styles.RedeemTitle}>{code}</div>
) : (
<div className={styles.RedeemTitle}>
{t('redeem:redeeming')} {code}
</div>
)}

{errorMessage && (
<div>
<span className={styles.formErrors}>{errorMessage}</span>
</div>
)}

{!errorMessage ? (
<div className={styles.redeemAnotherCodeDiv}>
<CircularProgress />
</div>
) : (
<div className={styles.redeemAnotherCodeDiv}>
<button className="primaryButton" onClick={redeemAnotherCode}>
{t('redeem:redeemAnotherCode')}
</button>
</div>
)}
</div>
);
};

export const SuccessfullyRedeemed = ({
redeemedCodeData,
redeemAnotherCode,
closeRedeem,
}: SuccessfullyRedeemed): ReactElement => {
const { t, i18n } = useTranslation(['common', 'redeem']);

return (
<div className={styles.routeRedeemModal}>
<div className={styles.crossDiv}>
<button className={styles.crossWidth} onClick={closeRedeem}>
<CancelIcon />
</button>
</div>

<div className={styles.codeTreeCount}>
{getFormattedNumber(i18n.language, Number(redeemedCodeData?.units))}
<span>
{t('common:tree', {
count: Number(redeemedCodeData?.units),
})}
</span>
</div>

<div className={styles.codeTreeCount}>
<span>{t('redeem:successfullyRedeemed')}</span>
</div>

<div className={styles.redeemAnotherCodeDiv}>
<button className="primaryButton" onClick={redeemAnotherCode}>
{t('redeem:redeemAnotherCode')}
</button>
</div>
</div>
);
};
24 changes: 24 additions & 0 deletions src/features/common/types/redeem.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export interface Organization {
name: string
slug: string
}
export interface Project {
id: string
name: string
slug: string
country: string
location: string
coordinates: number[]
organization: Organization
}
export interface RedeemedCodeData {
id: string
type: string
code: string
units: number
status: string
project: Project
}



2 changes: 1 addition & 1 deletion src/features/user/Profile/components/RedeemModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export default function RedeemModal({
<>
<h4 style={{ fontWeight: '700' }}>{t('me:redeem')}</h4>
<div className={styles.note}>
<p>{t('me:redeemDescription')}</p>
<p>{t('redeem:redeemDescription')}</p>
</div>
{!errorMessage && (
<div className={styles.inputField}>
Expand Down
Loading