mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
refactor(client): daily challenges to use path params (#61776)
This commit is contained in:
+19
-22
@@ -1,23 +1,21 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useParams } from '@gatsbyjs/reach-router';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
import ShowClassic from '../../templates/Challenges/classic/show';
|
import ShowClassic from '../templates/Challenges/classic/show';
|
||||||
import { Loader } from '../../components/helpers';
|
import { Loader } from '../components/helpers';
|
||||||
import {
|
import {
|
||||||
DailyCodingChallengeLanguages,
|
DailyCodingChallengeLanguages,
|
||||||
DailyCodingChallengeNode,
|
DailyCodingChallengeNode,
|
||||||
DailyCodingChallengePageContext
|
DailyCodingChallengePageContext
|
||||||
} from '../../redux/prop-types';
|
} from '../redux/prop-types';
|
||||||
import DailyCodingChallengeNotFound from '../../components/daily-coding-challenge/not-found';
|
import DailyCodingChallengeNotFound from '../components/daily-coding-challenge/not-found';
|
||||||
import FourOhFour from '../../components/FourOhFour';
|
import FourOhFour from '../components/FourOhFour';
|
||||||
import {
|
import { apiLocation, showDailyCodingChallenges } from '../../config/env.json';
|
||||||
apiLocation,
|
import { isValidDateString } from '../components/daily-coding-challenge/helpers';
|
||||||
showDailyCodingChallenges
|
|
||||||
} from '../../../config/env.json';
|
|
||||||
import { isValidDateParam } from '../../components/daily-coding-challenge/helpers';
|
|
||||||
import {
|
import {
|
||||||
validateDailyCodingChallengeSchema,
|
validateDailyCodingChallengeSchema,
|
||||||
type DailyCodingChallengeFromDb
|
type DailyCodingChallengeFromDb
|
||||||
} from '../../utils/daily-coding-challenge-validator';
|
} from '../utils/daily-coding-challenge-validator';
|
||||||
|
|
||||||
interface DailyCodingChallengeLanguageData {
|
interface DailyCodingChallengeLanguageData {
|
||||||
data: {
|
data: {
|
||||||
@@ -31,7 +29,7 @@ interface DailyCodingChallengeDataFormatted {
|
|||||||
python: DailyCodingChallengeLanguageData;
|
python: DailyCodingChallengeLanguageData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// These are not included in the data from the DB (Daily Challenge API) - so we add them in
|
// This is not included in the data from the DB (Daily Challenge API) - so we add it in
|
||||||
function formatDescription(str: string) {
|
function formatDescription(str: string) {
|
||||||
return `<section id="description">\n${str}\n</section>`;
|
return `<section id="description">\n${str}\n</section>`;
|
||||||
}
|
}
|
||||||
@@ -149,7 +147,9 @@ function formatChallengeData({
|
|||||||
return props;
|
return props;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DailyCodingChallenge(): JSX.Element {
|
function ShowDailyCodingChallenge(): JSX.Element {
|
||||||
|
const { date } = useParams<{ date?: string }>();
|
||||||
|
|
||||||
const initLanguage =
|
const initLanguage =
|
||||||
(store.get(
|
(store.get(
|
||||||
'dailyCodingChallengeLanguage'
|
'dailyCodingChallengeLanguage'
|
||||||
@@ -162,9 +162,6 @@ function DailyCodingChallenge(): JSX.Element {
|
|||||||
const [dailyCodingChallengeLanguage, setDailyCodingChallengeLanguage] =
|
const [dailyCodingChallengeLanguage, setDailyCodingChallengeLanguage] =
|
||||||
useState<DailyCodingChallengeLanguages>(initLanguage);
|
useState<DailyCodingChallengeLanguages>(initLanguage);
|
||||||
|
|
||||||
const dateParam =
|
|
||||||
new URLSearchParams(window.location.search).get('date') || '';
|
|
||||||
|
|
||||||
const fetchChallenge = async (date: string) => {
|
const fetchChallenge = async (date: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@@ -201,15 +198,15 @@ function DailyCodingChallenge(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If dateParam is invalid, stop loading/fetching and show the not found page
|
// If date is invalid, stop loading/fetching and show the not found page
|
||||||
if (!isValidDateParam(dateParam)) {
|
if (!date || !isValidDateString(date)) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setChallengeFound(false);
|
setChallengeFound(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void fetchChallenge(dateParam);
|
void fetchChallenge(date);
|
||||||
}, [dateParam]);
|
}, [date]);
|
||||||
|
|
||||||
if (!showDailyCodingChallenges) {
|
if (!showDailyCodingChallenges) {
|
||||||
return <FourOhFour />;
|
return <FourOhFour />;
|
||||||
@@ -230,6 +227,6 @@ function DailyCodingChallenge(): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DailyCodingChallenge.displayName = 'DailyCodingChallenge';
|
ShowDailyCodingChallenge.displayName = 'ShowDailyCodingChallenge';
|
||||||
|
|
||||||
export default DailyCodingChallenge;
|
export default ShowDailyCodingChallenge;
|
||||||
@@ -16,7 +16,7 @@ import { updateAllChallengesInfo } from '../../redux/actions';
|
|||||||
import { CertificateNode, ChallengeNode } from '../../redux/prop-types';
|
import { CertificateNode, ChallengeNode } from '../../redux/prop-types';
|
||||||
import { getIsDailyCodingChallenge } from '../../../../shared/config/challenge-types';
|
import { getIsDailyCodingChallenge } from '../../../../shared/config/challenge-types';
|
||||||
import {
|
import {
|
||||||
isValidDateParam,
|
isValidDateString,
|
||||||
formatDisplayDate
|
formatDisplayDate
|
||||||
} from '../daily-coding-challenge/helpers';
|
} from '../daily-coding-challenge/helpers';
|
||||||
import ProgressInner from './progress-inner';
|
import ProgressInner from './progress-inner';
|
||||||
@@ -81,7 +81,7 @@ function Progress({
|
|||||||
const dateParam =
|
const dateParam =
|
||||||
new URLSearchParams(window.location.search).get('date') || '';
|
new URLSearchParams(window.location.search).get('date') || '';
|
||||||
|
|
||||||
if (isValidDateParam(dateParam)) {
|
if (isValidDateString(dateParam)) {
|
||||||
blockTitle += `: ${formatDisplayDate(dateParam)}`;
|
blockTitle += `: ${formatDisplayDate(dateParam)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function DailyCodingChallengeCalendarDay({
|
|||||||
// isAvailable -> render link to challenge
|
// isAvailable -> render link to challenge
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={`/learn/daily-coding-challenge?date=${date}`}
|
to={`/learn/daily-coding-challenge/${date}`}
|
||||||
className='calendar-day available'
|
className='calendar-day available'
|
||||||
data-playwright-test-label='calendar-day'
|
data-playwright-test-label='calendar-day'
|
||||||
aria-label={`${date && formatDisplayDate(date)}`}
|
aria-label={`${date && formatDisplayDate(date)}`}
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ function DailyCodingChallengeCalendar({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
block={true}
|
block={true}
|
||||||
href={`/learn/daily-coding-challenge?date=${todayUsCentral}`}
|
href={`/learn/daily-coding-challenge/${todayUsCentral}`}
|
||||||
>
|
>
|
||||||
{t('buttons.go-to-today')}
|
{t('buttons.go-to-today')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function getTodayUsCentral(dateObj: Date = new Date()) {
|
|||||||
|
|
||||||
// Validate that dateString is in the format yyyy-MM-dd
|
// Validate that dateString is in the format yyyy-MM-dd
|
||||||
// Leading zero's are accepted for single digit month/day
|
// Leading zero's are accepted for single digit month/day
|
||||||
export function isValidDateParam(dateString: string) {
|
export function isValidDateString(dateString: string) {
|
||||||
const parsedDate = parse(dateString, 'yyyy-MM-dd', new Date());
|
const parsedDate = parse(dateString, 'yyyy-MM-dd', new Date());
|
||||||
return isValid(parsedDate);
|
return isValid(parsedDate);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function DailyCodingChallengeNotFound(): JSX.Element {
|
|||||||
<div className='button-wrapper'>
|
<div className='button-wrapper'>
|
||||||
<Button
|
<Button
|
||||||
block={true}
|
block={true}
|
||||||
href={`/learn/daily-coding-challenge?date=${getTodayUsCentral()}`}
|
href={`/learn/daily-coding-challenge/${getTodayUsCentral()}`}
|
||||||
>
|
>
|
||||||
{t(`buttons.go-to-today-long`)}
|
{t(`buttons.go-to-today-long`)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function DailyCodingChallengeWidget({
|
|||||||
block
|
block
|
||||||
size='large'
|
size='large'
|
||||||
className='map-superblock-link'
|
className='map-superblock-link'
|
||||||
href={`/learn/daily-coding-challenge?date=${getTodayUsCentral()}`}
|
href={`/learn/daily-coding-challenge/${getTodayUsCentral()}`}
|
||||||
>
|
>
|
||||||
<div className='daily-coding-challenge-button'>
|
<div className='daily-coding-challenge-button'>
|
||||||
<DailyCodingChallengeIcon className='map-icon' />
|
<DailyCodingChallengeIcon className='map-icon' />
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ interface DefaultLayoutProps extends StateProps, DispatchProps {
|
|||||||
showFooter?: boolean;
|
showFooter?: boolean;
|
||||||
isChallenge?: boolean;
|
isChallenge?: boolean;
|
||||||
isDailyChallenge?: boolean;
|
isDailyChallenge?: boolean;
|
||||||
|
dailyChallengeParam?: string;
|
||||||
usesMultifileEditor?: boolean;
|
usesMultifileEditor?: boolean;
|
||||||
block?: string;
|
block?: string;
|
||||||
examInProgress: boolean;
|
examInProgress: boolean;
|
||||||
@@ -133,6 +134,7 @@ function DefaultLayout({
|
|||||||
showFooter = true,
|
showFooter = true,
|
||||||
isChallenge = false,
|
isChallenge = false,
|
||||||
isDailyChallenge = false,
|
isDailyChallenge = false,
|
||||||
|
dailyChallengeParam,
|
||||||
usesMultifileEditor,
|
usesMultifileEditor,
|
||||||
block,
|
block,
|
||||||
superBlock,
|
superBlock,
|
||||||
@@ -292,7 +294,9 @@ function DefaultLayout({
|
|||||||
<SignoutModal />
|
<SignoutModal />
|
||||||
{isDailyChallenge ? (
|
{isDailyChallenge ? (
|
||||||
<div className='breadcrumbs-demo'>
|
<div className='breadcrumbs-demo'>
|
||||||
<DailyChallengeBreadCrumb />
|
<DailyChallengeBreadCrumb
|
||||||
|
dailyChallengeParam={dailyChallengeParam}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
isChallenge &&
|
isChallenge &&
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { withPrefix } from 'gatsby';
|
||||||
|
import createRedirect from './create-redirect';
|
||||||
|
|
||||||
|
export default createRedirect(
|
||||||
|
withPrefix('/learn/daily-coding-challenge/archive')
|
||||||
|
);
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/* eslint-disable filenames-simple/naming-convention */
|
||||||
|
import { Router } from '@gatsbyjs/reach-router';
|
||||||
|
import { withPrefix } from 'gatsby';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import ShowDailyCodingChallenge from '../../../client-only-routes/show-daily-coding-challenge';
|
||||||
|
import RedirectToArchive from '../../../components/redirect-daily-challenge-archive';
|
||||||
|
|
||||||
|
const inlineStyles = {
|
||||||
|
minHeight: 0,
|
||||||
|
height: '100%'
|
||||||
|
};
|
||||||
|
|
||||||
|
function DailyCodingChallengeAll(): JSX.Element {
|
||||||
|
return (
|
||||||
|
// Router adds an element around the editor, messing with the layout because the editor is a flex item
|
||||||
|
// These few inline styles fix it.
|
||||||
|
<Router style={inlineStyles}>
|
||||||
|
<ShowDailyCodingChallenge
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
path={withPrefix('/learn/daily-coding-challenge/:date')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RedirectToArchive default />
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DailyCodingChallengeAll.displayName = 'DailyCodingChallengeAll';
|
||||||
|
export default DailyCodingChallengeAll;
|
||||||
@@ -5,21 +5,18 @@ import { Link } from '../../../components/helpers/index';
|
|||||||
|
|
||||||
import './challenge-title.css';
|
import './challenge-title.css';
|
||||||
import {
|
import {
|
||||||
isValidDateParam,
|
isValidDateString,
|
||||||
formatDisplayDate
|
formatDisplayDate
|
||||||
} from '../../../components/daily-coding-challenge/helpers';
|
} from '../../../components/daily-coding-challenge/helpers';
|
||||||
|
|
||||||
function DailyChallengeBreadCrumb(): JSX.Element {
|
function DailyChallengeBreadCrumb({
|
||||||
const dateParam =
|
dailyChallengeParam
|
||||||
new URLSearchParams(window.location.search).get('date') || '';
|
}: {
|
||||||
let displayDate = '';
|
dailyChallengeParam?: string;
|
||||||
|
}): JSX.Element | null {
|
||||||
if (isValidDateParam(dateParam)) {
|
|
||||||
displayDate = formatDisplayDate(dateParam);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
|
||||||
|
return dailyChallengeParam && isValidDateString(dailyChallengeParam) ? (
|
||||||
<nav
|
<nav
|
||||||
className='challenge-title-breadcrumbs'
|
className='challenge-title-breadcrumbs'
|
||||||
aria-label={t('aria.breadcrumb-nav')}
|
aria-label={t('aria.breadcrumb-nav')}
|
||||||
@@ -32,12 +29,12 @@ function DailyChallengeBreadCrumb(): JSX.Element {
|
|||||||
</li>
|
</li>
|
||||||
<li className='breadcrumb-right'>
|
<li className='breadcrumb-right'>
|
||||||
<Link to={`/learn/daily-coding-challenge/archive`}>
|
<Link to={`/learn/daily-coding-challenge/archive`}>
|
||||||
{displayDate}
|
{formatDisplayDate(dailyChallengeParam)}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
DailyChallengeBreadCrumb.displayName = 'DailyChallengeBreadCrumb';
|
DailyChallengeBreadCrumb.displayName = 'DailyChallengeBreadCrumb';
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ function getComponentNameAndProps(
|
|||||||
location: {
|
location: {
|
||||||
pathname
|
pathname
|
||||||
},
|
},
|
||||||
pageContext
|
pageContext,
|
||||||
|
params: { '*': '' },
|
||||||
|
path: ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
utils.render(<Provider store={store}>{LayoutReactComponent}</Provider>);
|
utils.render(<Provider store={store}>{LayoutReactComponent}</Provider>);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ interface LayoutSelectorProps {
|
|||||||
data: { challengeNode?: { challenge?: { usesMultifileEditor?: boolean } } };
|
data: { challengeNode?: { challenge?: { usesMultifileEditor?: boolean } } };
|
||||||
location: { pathname: string };
|
location: { pathname: string };
|
||||||
pageContext?: { challengeMeta?: { block?: string; superBlock?: string } };
|
pageContext?: { challengeMeta?: { block?: string; superBlock?: string } };
|
||||||
|
params: { '*'?: string };
|
||||||
|
path: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export default function layoutSelector({
|
export default function layoutSelector({
|
||||||
@@ -20,8 +22,8 @@ export default function layoutSelector({
|
|||||||
location: { pathname }
|
location: { pathname }
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isDailyChallenge =
|
const isDailyChallenge = props.path === '/learn/daily-coding-challenge/*';
|
||||||
props.location.pathname === '/learn/daily-coding-challenge';
|
const dailyChallengeParam = props.params['*'];
|
||||||
|
|
||||||
const isChallenge = !!props.pageContext?.challengeMeta || isDailyChallenge;
|
const isChallenge = !!props.pageContext?.challengeMeta || isDailyChallenge;
|
||||||
|
|
||||||
@@ -42,6 +44,7 @@ export default function layoutSelector({
|
|||||||
showFooter={false}
|
showFooter={false}
|
||||||
isChallenge={true}
|
isChallenge={true}
|
||||||
isDailyChallenge={isDailyChallenge}
|
isDailyChallenge={isDailyChallenge}
|
||||||
|
dailyChallengeParam={dailyChallengeParam}
|
||||||
usesMultifileEditor={
|
usesMultifileEditor={
|
||||||
props.data?.challengeNode?.challenge?.usesMultifileEditor
|
props.data?.challengeNode?.challenge?.usesMultifileEditor
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const mockDaysInMonth = new Date(year, month, 0).getDate();
|
|||||||
|
|
||||||
test.describe('Daily Coding Challenges', () => {
|
test.describe('Daily Coding Challenges', () => {
|
||||||
test('should show not found page for invalid date', async ({ page }) => {
|
test('should show not found page for invalid date', async ({ page }) => {
|
||||||
await page.goto('/learn/daily-coding-challenge?date=invalid-date');
|
await page.goto('/learn/daily-coding-challenge/invalid-date');
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(/daily coding challenge not found\./i)
|
page.getByText(/daily coding challenge not found\./i)
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@@ -83,7 +83,7 @@ test.describe('Daily Coding Challenges', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto('/learn/daily-coding-challenge?date=2025-01-01');
|
await page.goto('/learn/daily-coding-challenge/2025-01-01');
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(/daily coding challenge not found\./i)
|
page.getByText(/daily coding challenge not found\./i)
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@@ -98,7 +98,7 @@ test.describe('Daily Coding Challenges', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto('/learn/daily-coding-challenge?date=2025-01-01');
|
await page.goto('/learn/daily-coding-challenge/2025-01-01');
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(/daily coding challenge not found\./i)
|
page.getByText(/daily coding challenge not found\./i)
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@@ -115,7 +115,7 @@ test.describe('Daily Coding Challenges', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto('/learn/daily-coding-challenge?date=2025-06-27');
|
await page.goto('/learn/daily-coding-challenge/2025-06-27');
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(/daily coding challenge not found\./i)
|
page.getByText(/daily coding challenge not found\./i)
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@@ -132,7 +132,7 @@ test.describe('Daily Coding Challenges', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto(`/learn/daily-coding-challenge?date=${todayUsCentral}`);
|
await page.goto(`/learn/daily-coding-challenge/${todayUsCentral}`);
|
||||||
|
|
||||||
await expect(page.getByText('Test title')).toBeVisible();
|
await expect(page.getByText('Test title')).toBeVisible();
|
||||||
|
|
||||||
@@ -171,14 +171,35 @@ test.describe('Daily Coding Challenges', () => {
|
|||||||
'# Python seed code'
|
'# Python seed code'
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.goto(`/learn/daily-coding-challenge?date=${todayUsCentral}`);
|
await page.goto(`/learn/daily-coding-challenge/${todayUsCentral}`);
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: /main.py/i })).toBeVisible();
|
await expect(page.getByRole('button', { name: /main.py/i })).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Daily Coding Challenge Archive', () => {
|
test.describe('Daily Coding Challenge Archive', () => {
|
||||||
test('should load and display the calendar', async ({ page }) => {
|
test('/learn/daily-coding-challenge should redirect to archive', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
await page.goto('/learn/daily-coding-challenge');
|
||||||
|
await expect(page).toHaveURL('/learn/daily-coding-challenge/archive');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('/learn/daily-coding-challenge/ should redirect to archive', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
await page.goto('/learn/daily-coding-challenge/');
|
||||||
|
await expect(page).toHaveURL('/learn/daily-coding-challenge/archive');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('/learn/daily-coding-challenge/path-1/path2 should redirect to archive', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
await page.goto('/learn/daily-coding-challenge/path-1/path2');
|
||||||
|
await expect(page).toHaveURL('/learn/daily-coding-challenge/archive');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('archive should load and display the calendar', async ({ page }) => {
|
||||||
await page.route(allRouteRe, async route => {
|
await page.route(allRouteRe, async route => {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
Reference in New Issue
Block a user