feat(discovery): more work on discovery plus a few fixes
This commit is contained in:
@@ -121,6 +121,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: var(--spacing-4);
|
||||
padding-top: var(--spacing-5);
|
||||
padding-bottom: var(--spacing-5);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
|
||||
.userName {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -94,28 +94,34 @@
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
border-top: 1px solid var(--background-modifier-accent);
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.joinButton {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.statDot {
|
||||
margin-right: 0.3rem;
|
||||
height: 0.5rem;
|
||||
width: 0.5rem;
|
||||
border-radius: var(--radius-full);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.statDotOnline {
|
||||
@@ -125,10 +131,12 @@
|
||||
|
||||
.statDotMembers {
|
||||
composes: statDot;
|
||||
background-color: var(--text-quaternary);
|
||||
background-color: var(--text-tertiary-secondary);
|
||||
}
|
||||
|
||||
.statText {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -19,11 +19,13 @@
|
||||
|
||||
import type {DiscoveryGuild} from '@app/actions/DiscoveryActionCreators';
|
||||
import * as DiscoveryActionCreators from '@app/actions/DiscoveryActionCreators';
|
||||
import * as ToastActionCreators from '@app/actions/ToastActionCreators';
|
||||
import {GuildBadge} from '@app/components/guild/GuildBadge';
|
||||
import styles from '@app/components/modals/discovery/DiscoveryGuildCard.module.css';
|
||||
import {GuildIcon} from '@app/components/popouts/GuildIcon';
|
||||
import {Button} from '@app/components/uikit/button/Button';
|
||||
import GuildStore from '@app/stores/GuildStore';
|
||||
import {getApiErrorMessage} from '@app/utils/ApiErrorUtils';
|
||||
import {getCurrentLocale} from '@app/utils/LocaleUtils';
|
||||
import type {DiscoveryCategory} from '@fluxer/constants/src/DiscoveryConstants';
|
||||
import {DiscoveryCategoryLabels} from '@fluxer/constants/src/DiscoveryConstants';
|
||||
@@ -40,7 +42,7 @@ export const DiscoveryGuildCard = observer(function DiscoveryGuildCard({guild}:
|
||||
const {t} = useLingui();
|
||||
const [joining, setJoining] = useState(false);
|
||||
const isAlreadyMember = GuildStore.getGuild(guild.id) != null;
|
||||
const categoryLabel = DiscoveryCategoryLabels[guild.category_id as DiscoveryCategory] ?? '';
|
||||
const categoryLabel = DiscoveryCategoryLabels[guild.category_type as DiscoveryCategory] ?? '';
|
||||
const memberCount = formatNumber(guild.member_count, getCurrentLocale());
|
||||
const onlineCount = formatNumber(guild.online_count, getCurrentLocale());
|
||||
|
||||
@@ -49,10 +51,12 @@ export const DiscoveryGuildCard = observer(function DiscoveryGuildCard({guild}:
|
||||
setJoining(true);
|
||||
try {
|
||||
await DiscoveryActionCreators.joinGuild(guild.id);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
setJoining(false);
|
||||
const message = getApiErrorMessage(error) ?? t`Failed to join this community. Please try again.`;
|
||||
ToastActionCreators.error(message);
|
||||
}
|
||||
}, [guild.id, joining, isAlreadyMember]);
|
||||
}, [guild.id, joining, isAlreadyMember, t]);
|
||||
|
||||
return (
|
||||
<div className={styles.card}>
|
||||
@@ -69,12 +73,10 @@ export const DiscoveryGuildCard = observer(function DiscoveryGuildCard({guild}:
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<div className={styles.stats}>
|
||||
{guild.online_count > 0 && (
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.statDotOnline} />
|
||||
<span className={styles.statText}>{t`${onlineCount} Online`}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.statDotOnline} />
|
||||
<span className={styles.statText}>{t`${onlineCount} Online`}</span>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.statDotMembers} />
|
||||
<span className={styles.statText}>
|
||||
@@ -82,8 +84,13 @@ export const DiscoveryGuildCard = observer(function DiscoveryGuildCard({guild}:
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="primary" small onClick={handleJoin} disabled={joining || isAlreadyMember}>
|
||||
{isAlreadyMember ? t`Joined` : t`Join`}
|
||||
<Button
|
||||
variant="primary"
|
||||
className={styles.joinButton}
|
||||
onClick={handleJoin}
|
||||
disabled={joining || isAlreadyMember}
|
||||
>
|
||||
{isAlreadyMember ? t`Joined` : t`Join Community`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.spinnerContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.statusCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid var(--background-header-secondary);
|
||||
background-color: var(--background-secondary);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.statusRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.statusLabel {
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.statusBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
border-radius: 9999px;
|
||||
padding: 0.125rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.statusPending {
|
||||
background-color: rgba(234, 179, 8, 0.15);
|
||||
color: rgb(234, 179, 8);
|
||||
}
|
||||
|
||||
.statusApproved {
|
||||
background-color: rgba(34, 197, 94, 0.15);
|
||||
color: rgb(34, 197, 94);
|
||||
}
|
||||
|
||||
.statusRejected {
|
||||
background-color: rgba(239, 68, 68, 0.15);
|
||||
color: rgb(239, 68, 68);
|
||||
}
|
||||
|
||||
.statusRemoved {
|
||||
background-color: rgba(239, 68, 68, 0.15);
|
||||
color: rgb(239, 68, 68);
|
||||
}
|
||||
|
||||
.reviewReason {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.formCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid var(--background-header-secondary);
|
||||
background-color: var(--background-secondary);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.fieldLabel {
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.helpText {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.charCount {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
color: var(--text-primary-muted);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.actions > * {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.warning {
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid rgba(234, 179, 8, 0.5);
|
||||
background-color: rgba(234, 179, 8, 0.1);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.warningContent {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.warningIcon {
|
||||
margin-top: 0.125rem;
|
||||
color: rgb(234, 179, 8);
|
||||
}
|
||||
|
||||
.warningBody {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.warningTitle {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
color: rgb(234, 179, 8);
|
||||
}
|
||||
|
||||
.warningText {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.info {
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid rgba(59, 130, 246, 0.5);
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.infoContent {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
margin-top: 0.125rem;
|
||||
color: rgb(59, 130, 246);
|
||||
}
|
||||
|
||||
.infoText {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as GuildActionCreators from '@app/actions/GuildActionCreators';
|
||||
import * as ToastActionCreators from '@app/actions/ToastActionCreators';
|
||||
import {Form} from '@app/components/form/Form';
|
||||
import {Textarea} from '@app/components/form/Input';
|
||||
import {Select, type SelectOption} from '@app/components/form/Select';
|
||||
import styles from '@app/components/modals/guild_tabs/GuildDiscoveryTab.module.css';
|
||||
import {Button} from '@app/components/uikit/button/Button';
|
||||
import {Spinner} from '@app/components/uikit/Spinner';
|
||||
import {useFormSubmit} from '@app/hooks/useFormSubmit';
|
||||
import {Logger} from '@app/lib/Logger';
|
||||
import {
|
||||
DISCOVERY_DESCRIPTION_MAX_LENGTH,
|
||||
DISCOVERY_DESCRIPTION_MIN_LENGTH,
|
||||
DiscoveryApplicationStatus,
|
||||
} from '@fluxer/constants/src/DiscoveryConstants';
|
||||
import type {
|
||||
DiscoveryApplicationResponse,
|
||||
DiscoveryStatusResponse,
|
||||
} from '@fluxer/schema/src/domains/guild/GuildDiscoverySchemas';
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {InfoIcon, WarningIcon} from '@phosphor-icons/react';
|
||||
import {clsx} from 'clsx';
|
||||
import type React from 'react';
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {Controller, useForm} from 'react-hook-form';
|
||||
|
||||
const logger = new Logger('GuildDiscoveryTab');
|
||||
|
||||
interface FormInputs {
|
||||
description: string;
|
||||
category_type: number;
|
||||
}
|
||||
|
||||
function StatusBadge({status}: {status: string}) {
|
||||
const {t} = useLingui();
|
||||
|
||||
const statusConfig: Record<string, {label: string; className: string}> = useMemo(
|
||||
() => ({
|
||||
[DiscoveryApplicationStatus.PENDING]: {label: t`Pending`, className: styles.statusPending},
|
||||
[DiscoveryApplicationStatus.APPROVED]: {label: t`Approved`, className: styles.statusApproved},
|
||||
[DiscoveryApplicationStatus.REJECTED]: {label: t`Rejected`, className: styles.statusRejected},
|
||||
[DiscoveryApplicationStatus.REMOVED]: {label: t`Removed`, className: styles.statusRemoved},
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
const config = statusConfig[status];
|
||||
if (!config) return null;
|
||||
|
||||
return <span className={clsx(styles.statusBadge, config.className)}>{config.label}</span>;
|
||||
}
|
||||
|
||||
const GuildDiscoveryTab: React.FC<{guildId: string}> = ({guildId}) => {
|
||||
const {t} = useLingui();
|
||||
const [status, setStatus] = useState<DiscoveryStatusResponse | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isWithdrawing, setIsWithdrawing] = useState(false);
|
||||
|
||||
const categoryOptions: ReadonlyArray<SelectOption<number>> = useMemo(
|
||||
() => [
|
||||
{value: 0, label: t`Gaming`},
|
||||
{value: 1, label: t`Music`},
|
||||
{value: 2, label: t`Entertainment`},
|
||||
{value: 3, label: t`Education`},
|
||||
{value: 4, label: t`Science & Technology`},
|
||||
{value: 5, label: t`Content Creator`},
|
||||
{value: 6, label: t`Anime & Manga`},
|
||||
{value: 7, label: t`Movies & TV`},
|
||||
{value: 8, label: t`Other`},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const fetchStatus = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await GuildActionCreators.getDiscoveryStatus(guildId);
|
||||
setStatus(data);
|
||||
} catch (err) {
|
||||
logger.error('Failed to fetch discovery status', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [guildId]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchStatus();
|
||||
}, [fetchStatus]);
|
||||
|
||||
const application = status?.application ?? null;
|
||||
const eligible = status?.eligible ?? false;
|
||||
const minMemberCount = status?.min_member_count ?? 0;
|
||||
|
||||
const hasActiveApplication =
|
||||
application != null &&
|
||||
(application.status === DiscoveryApplicationStatus.PENDING ||
|
||||
application.status === DiscoveryApplicationStatus.APPROVED);
|
||||
|
||||
const canApply =
|
||||
!hasActiveApplication &&
|
||||
(application == null ||
|
||||
application.status === DiscoveryApplicationStatus.REJECTED ||
|
||||
application.status === DiscoveryApplicationStatus.REMOVED);
|
||||
|
||||
const form = useForm<FormInputs>({
|
||||
defaultValues: {
|
||||
description: hasActiveApplication ? application.description : '',
|
||||
category_type: hasActiveApplication ? application.category_type : 0,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (hasActiveApplication && application) {
|
||||
form.reset({
|
||||
description: application.description,
|
||||
category_type: application.category_type,
|
||||
});
|
||||
}
|
||||
}, [application, hasActiveApplication, form]);
|
||||
|
||||
const setApplicationFromResponse = useCallback((response: DiscoveryApplicationResponse) => {
|
||||
setStatus((prev) => (prev ? {...prev, application: response} : prev));
|
||||
}, []);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormInputs) => {
|
||||
if (hasActiveApplication) {
|
||||
const result = await GuildActionCreators.updateDiscoveryApplication(guildId, {
|
||||
description: data.description,
|
||||
category_type: data.category_type,
|
||||
});
|
||||
setApplicationFromResponse(result);
|
||||
form.reset(data);
|
||||
ToastActionCreators.createToast({
|
||||
type: 'success',
|
||||
children: <Trans>Discovery listing updated</Trans>,
|
||||
});
|
||||
} else {
|
||||
const result = await GuildActionCreators.applyForDiscovery(guildId, {
|
||||
description: data.description,
|
||||
category_type: data.category_type,
|
||||
});
|
||||
setApplicationFromResponse(result);
|
||||
form.reset(data);
|
||||
ToastActionCreators.createToast({
|
||||
type: 'success',
|
||||
children: <Trans>Discovery application submitted</Trans>,
|
||||
});
|
||||
}
|
||||
},
|
||||
[guildId, hasActiveApplication, form, setApplicationFromResponse],
|
||||
);
|
||||
|
||||
const {handleSubmit, isSubmitting} = useFormSubmit({
|
||||
form,
|
||||
onSubmit,
|
||||
defaultErrorField: 'description',
|
||||
});
|
||||
|
||||
const handleWithdraw = useCallback(async () => {
|
||||
try {
|
||||
setIsWithdrawing(true);
|
||||
await GuildActionCreators.withdrawDiscoveryApplication(guildId);
|
||||
setStatus((prev) => (prev ? {...prev, application: null} : prev));
|
||||
form.reset({description: '', category_type: 0});
|
||||
ToastActionCreators.createToast({
|
||||
type: 'success',
|
||||
children: <Trans>Discovery application withdrawn</Trans>,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('Failed to withdraw discovery application', err);
|
||||
ToastActionCreators.createToast({
|
||||
type: 'error',
|
||||
children: <Trans>Failed to withdraw application. Please try again.</Trans>,
|
||||
});
|
||||
} finally {
|
||||
setIsWithdrawing(false);
|
||||
}
|
||||
}, [guildId, form]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={styles.spinnerContainer}>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<h2 className={styles.title}>
|
||||
<Trans>Discovery</Trans>
|
||||
</h2>
|
||||
<p className={styles.subtitle}>
|
||||
<Trans>List your community in Discovery so others can find and join it.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!eligible && canApply && (
|
||||
<div className={styles.warning}>
|
||||
<div className={styles.warningContent}>
|
||||
<div className={styles.warningIcon}>
|
||||
<WarningIcon size={20} weight="fill" />
|
||||
</div>
|
||||
<div className={styles.warningBody}>
|
||||
<p className={styles.warningTitle}>
|
||||
<Trans>Not enough members</Trans>
|
||||
</p>
|
||||
<p className={styles.warningText}>
|
||||
<Trans>
|
||||
Your community needs at least {minMemberCount} members before it can be listed in Discovery.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{application != null && (
|
||||
<div className={styles.statusCard}>
|
||||
<div className={styles.statusRow}>
|
||||
<span className={styles.statusLabel}>
|
||||
<Trans>Status:</Trans>
|
||||
</span>
|
||||
<StatusBadge status={application.status} />
|
||||
</div>
|
||||
{application.review_reason && (
|
||||
<p className={styles.reviewReason}>
|
||||
<Trans>Reason: {application.review_reason}</Trans>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{application?.status === DiscoveryApplicationStatus.APPROVED && (
|
||||
<div className={styles.info}>
|
||||
<div className={styles.infoContent}>
|
||||
<div className={styles.infoIcon}>
|
||||
<InfoIcon size={20} weight="fill" />
|
||||
</div>
|
||||
<p className={styles.infoText}>
|
||||
<Trans>
|
||||
Your community is listed in Discovery. You can update your listing details below or withdraw to remove
|
||||
it.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{application?.status === DiscoveryApplicationStatus.PENDING && (
|
||||
<div className={styles.info}>
|
||||
<div className={styles.infoContent}>
|
||||
<div className={styles.infoIcon}>
|
||||
<InfoIcon size={20} weight="fill" />
|
||||
</div>
|
||||
<p className={styles.infoText}>
|
||||
<Trans>
|
||||
Your application is pending review. You can still update your listing details or withdraw the
|
||||
application.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(canApply || hasActiveApplication) && (
|
||||
<Form form={form} onSubmit={handleSubmit}>
|
||||
<div className={styles.formCard}>
|
||||
<div>
|
||||
<div className={styles.fieldLabel}>
|
||||
<Trans>Description</Trans>
|
||||
</div>
|
||||
<Textarea
|
||||
{...form.register('description', {
|
||||
required: t`A description is required.`,
|
||||
minLength: {
|
||||
value: DISCOVERY_DESCRIPTION_MIN_LENGTH,
|
||||
message: t`Description must be at least ${DISCOVERY_DESCRIPTION_MIN_LENGTH} characters.`,
|
||||
},
|
||||
maxLength: {
|
||||
value: DISCOVERY_DESCRIPTION_MAX_LENGTH,
|
||||
message: t`Description must be no more than ${DISCOVERY_DESCRIPTION_MAX_LENGTH} characters.`,
|
||||
},
|
||||
})}
|
||||
error={form.formState.errors.description?.message}
|
||||
label=""
|
||||
placeholder={t`Describe what your community is about...`}
|
||||
minRows={3}
|
||||
maxRows={6}
|
||||
maxLength={DISCOVERY_DESCRIPTION_MAX_LENGTH}
|
||||
showCharacterCount
|
||||
disabled={!eligible && canApply}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={styles.fieldLabel}>
|
||||
<Trans>Category</Trans>
|
||||
</div>
|
||||
<Controller
|
||||
name="category_type"
|
||||
control={form.control}
|
||||
render={({field}) => (
|
||||
<Select<number>
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
options={categoryOptions}
|
||||
isSearchable={false}
|
||||
disabled={!eligible && canApply}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className={styles.helpText}>
|
||||
<Trans>Choose the category that best describes your community.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{hasActiveApplication && (
|
||||
<Button type="button" variant="danger-secondary" onClick={handleWithdraw} submitting={isWithdrawing}>
|
||||
<Trans>Withdraw</Trans>
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit" submitting={isSubmitting} disabled={!eligible && canApply}>
|
||||
{hasActiveApplication ? <Trans>Save</Trans> : <Trans>Apply</Trans>}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GuildDiscoveryTab;
|
||||
@@ -237,7 +237,3 @@
|
||||
color: var(--text-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.permissionWarning {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
import {Switch as UISwitch} from '@app/components/form/Switch';
|
||||
import styles from '@app/components/modals/shared/PermissionComponents.module.css';
|
||||
import {Tooltip} from '@app/components/uikit/tooltip/Tooltip';
|
||||
import {WarningAlert} from '@app/components/uikit/warning_alert/WarningAlert';
|
||||
import PermissionLayoutStore from '@app/stores/PermissionLayoutStore';
|
||||
import type * as PermissionUtils from '@app/utils/PermissionUtils';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
@@ -128,6 +127,7 @@ const PermissionOverwriteToggle: React.FC<{
|
||||
const buttons = <PermissionStateButtons currentState={state} onStateChange={onChange} disabled={disabled} />;
|
||||
const buttonsTooltipTrigger = <div className={styles.tooltipTriggerInline}>{buttons}</div>;
|
||||
const showDescription = PermissionLayoutStore.isComfy;
|
||||
const tooltipText = (disabled && disabledReason) || warning;
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.overwriteToggle, PermissionLayoutStore.isDense && styles.overwriteToggleDense)}>
|
||||
@@ -142,10 +142,9 @@ const PermissionOverwriteToggle: React.FC<{
|
||||
</div>
|
||||
{showDescription && description && <p className={styles.overwriteToggleDescription}>{description}</p>}
|
||||
{extra}
|
||||
{warning && <WarningAlert className={styles.permissionWarning}>{warning}</WarningAlert>}
|
||||
</div>
|
||||
<div className={styles.overwriteToggleActions}>
|
||||
{disabled && disabledReason ? <Tooltip text={disabledReason}>{buttonsTooltipTrigger}</Tooltip> : buttons}
|
||||
{tooltipText ? <Tooltip text={tooltipText}>{buttonsTooltipTrigger}</Tooltip> : buttons}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -238,12 +237,12 @@ const PermissionRoleToggle: React.FC<{
|
||||
/>
|
||||
);
|
||||
const switchTooltipTrigger = <div className={styles.tooltipTriggerBlock}>{switchEl}</div>;
|
||||
const tooltipText = (disabled && disabledReason) || warning;
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.roleToggle, PermissionLayoutStore.isDense && styles.roleToggleDense)}>
|
||||
{disabled && disabledReason ? <Tooltip text={disabledReason}>{switchTooltipTrigger}</Tooltip> : switchEl}
|
||||
{tooltipText ? <Tooltip text={tooltipText}>{switchTooltipTrigger}</Tooltip> : switchEl}
|
||||
{extra}
|
||||
{warning && <WarningAlert className={styles.permissionWarning}>{warning}</WarningAlert>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import GuildAuditLogTab from '@app/components/modals/guild_tabs/GuildAuditLogTab';
|
||||
import GuildBansTab from '@app/components/modals/guild_tabs/GuildBansTab';
|
||||
import GuildDiscoveryTab from '@app/components/modals/guild_tabs/GuildDiscoveryTab';
|
||||
import GuildEmojiTab from '@app/components/modals/guild_tabs/GuildEmojiTab';
|
||||
import GuildInvitesTab from '@app/components/modals/guild_tabs/GuildInvitesTab';
|
||||
import GuildModerationTab from '@app/components/modals/guild_tabs/GuildModerationTab';
|
||||
@@ -33,6 +34,7 @@ import type {I18n, MessageDescriptor} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {
|
||||
BookOpenIcon,
|
||||
CompassIcon,
|
||||
GearIcon,
|
||||
HammerIcon,
|
||||
type Icon,
|
||||
@@ -57,6 +59,7 @@ export type GuildSettingsTabType =
|
||||
| 'audit_log'
|
||||
| 'webhooks'
|
||||
| 'vanity_url'
|
||||
| 'discovery'
|
||||
| 'members'
|
||||
| 'invites'
|
||||
| 'bans';
|
||||
@@ -151,6 +154,15 @@ const GUILD_SETTINGS_TABS_DESCRIPTORS: Array<GuildSettingsTabDescriptor> = [
|
||||
permission: Permissions.MANAGE_GUILD,
|
||||
requireFeature: GuildFeatures.VANITY_URL,
|
||||
},
|
||||
{
|
||||
type: 'discovery',
|
||||
category: 'guild_settings',
|
||||
label: msg`Discovery`,
|
||||
icon: CompassIcon,
|
||||
iconWeight: 'fill',
|
||||
component: GuildDiscoveryTab,
|
||||
permission: Permissions.MANAGE_GUILD,
|
||||
},
|
||||
{
|
||||
type: 'members',
|
||||
category: 'user_management',
|
||||
|
||||
Reference in New Issue
Block a user