Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions frontend/src/components/AccessControlListEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Button from "@/components/Button";
import Select from "@/components/Select";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowUp, faArrowDown, faXmark } from "@fortawesome/free-solid-svg-icons";
import { roleColour } from "@/lib/colour";

interface AccessControlListEditorProps {
guildId: string;
Expand Down Expand Up @@ -60,12 +61,18 @@ const AccessControlListEditor: FC<AccessControlListEditorProps> = ({
return role ? role.name : "Deleted Role";
};

const roleColours = new Map(roles.map((r) => [r.id, roleColour(r.color)]));

return (
<div className="flex flex-col gap-3">
<Select
label="Add Role"
placeholder="Add another role..."
options={availableRoles.map((r) => ({ key: r.id, label: r.name }))}
options={availableRoles.map((r) => ({
key: r.id,
label: r.name,
color: roleColour(r.color),
}))}
value=""
onChange={addRole}
disabled={acl.length >= MAX_ACL_SIZE}
Expand Down Expand Up @@ -100,7 +107,15 @@ const AccessControlListEditor: FC<AccessControlListEditorProps> = ({
<FontAwesomeIcon icon={faArrowDown} />
</Button>
</div>
<span className="text-white">{getRoleName(subject.role_id)}</span>
<span className="flex items-center gap-2 text-white">
{roleColours.has(subject.role_id) && (
<span
className="w-3 h-3 rounded-full shrink-0 ring-1 ring-white/15"
style={{ backgroundColor: roleColours.get(subject.role_id) }}
/>
)}
{getRoleName(subject.role_id)}
</span>
<div className="flex gap-1">
<Button
variant={subject.action === "allow" ? "success" : "danger"}
Expand Down
9 changes: 2 additions & 7 deletions frontend/src/components/GalleryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { GalleryListing, GalleryTagSnapshot } from "@/types";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faDownload, faStar } from "@fortawesome/free-solid-svg-icons";
import { Link } from "react-router";
import { GALLERY_TYPE_BADGES } from "@/components/gallery/GalleryListingPreview";

interface GalleryCardProps {
listing: GalleryListing;
Expand All @@ -13,12 +14,6 @@ interface GalleryCardProps {
actionLabel?: string;
}

const TYPE_BADGES: Record<string, { label: string; className: string }> = {
panel: { label: "Panel", className: "bg-purple-600/20 text-purple-400" },
tag: { label: "Tag", className: "bg-teal-600/20 text-teal-400" },
form: { label: "Form", className: "bg-orange-600/20 text-orange-400" },
};

const BODY_CLASS =
"group flex flex-col flex-1 p-4 text-left transition-colors hover:bg-gray-700/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-inset";

Expand Down Expand Up @@ -52,7 +47,7 @@ const GalleryCard: FC<GalleryCardProps> = ({
}) => {
const colourHex = getColourHex(listing);
const type = listing.listing_type || "panel";
const badge = TYPE_BADGES[type];
const badge = GALLERY_TYPE_BADGES[type];

const body = (
<>
Expand Down
5 changes: 1 addition & 4 deletions frontend/src/components/LabelBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FC } from "react";
import { intToColour } from "@/lib/colour";

interface LabelBadgeProps {
name: string;
Expand All @@ -7,10 +8,6 @@ interface LabelBadgeProps {
onRemove?: () => void;
}

function intToColour(colour: number): string {
return `#${colour.toString(16).padStart(6, "0")}`;
}

const LabelBadge: FC<LabelBadgeProps> = ({ name, colour, removable, onRemove }) => {
const hex = intToColour(colour);

Expand Down
16 changes: 9 additions & 7 deletions frontend/src/components/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { FC } from "react";
import { useState, useRef, useEffect, useId, useCallback } from "react";
import { createPortal } from "react-dom";
import { useFloatingDropdown } from "@/hooks/useFloatingDropdown";
import { normaliseColour } from "@/lib/colour";

interface MultiSelectOption {
key: string;
Expand Down Expand Up @@ -115,8 +116,9 @@ const MultiSelect: FC<MultiSelectProps> = (props) => {
<div className="w-full p-2 min-h-10 flex flex-wrap gap-1 items-center">
{selectedOptions.length > 0 ? (
selectedOptions.map((selectedOption) => {
const fadedBgStyle = selectedOption.color
? { backgroundColor: `${selectedOption.color}20` }
const swatch = selectedOption.color && normaliseColour(selectedOption.color);
const fadedBgStyle = swatch
? { backgroundColor: `color-mix(in srgb, ${swatch} 20%, transparent)` }
: {};

return (
Expand All @@ -126,10 +128,10 @@ const MultiSelect: FC<MultiSelectProps> = (props) => {
style={fadedBgStyle}
title={selectedOption.label}
>
{selectedOption.color && (
{swatch && (
<div
className="w-3 h-3 rounded-full mr-1 shrink-0"
style={{ backgroundColor: `#${selectedOption.color}` }}
className="w-3 h-3 rounded-full mr-1 shrink-0 ring-1 ring-white/15"
style={{ backgroundColor: swatch }}
/>
)}
<span className="min-w-0 truncate">{selectedOption.label}</span>
Expand Down Expand Up @@ -242,8 +244,8 @@ const MultiSelect: FC<MultiSelectProps> = (props) => {
</div>
{option.color && (
<div
className="w-3 h-3 mt-1.5 shrink-0 rounded-full mr-2"
style={{ backgroundColor: `#${option.color}` }}
className="w-3 h-3 mt-1.5 shrink-0 rounded-full mr-2 ring-1 ring-white/15"
style={{ backgroundColor: normaliseColour(option.color) }}
/>
)}
<span className="min-w-0 flex-1 break-words text-white">{option.label}</span>
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/components/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import ChannelInfoModal from "@/components/modals/ChannelInfoModal";
import type { SelectInfo } from "@/constants/panelChannelInfo";
import { useFloatingDropdown } from "@/hooks/useFloatingDropdown";
import { normaliseColour } from "@/lib/colour";

interface SelectOption {
key: string | null;
Expand Down Expand Up @@ -156,8 +157,8 @@ const Select: FC<SelectProps> = (props) => {
<>
{selectedOption.color && (
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: selectedOption.color }}
className="w-3 h-3 rounded-full shrink-0 ring-1 ring-white/15"
style={{ backgroundColor: normaliseColour(selectedOption.color) }}
/>
)}
<span className="text-white">{selectedOption.label}</span>
Expand Down Expand Up @@ -279,8 +280,8 @@ const Select: FC<SelectProps> = (props) => {
</div>
{option.color && (
<div
className="w-3 h-3 rounded-full mr-2 shrink-0"
style={{ backgroundColor: option.color }}
className="w-3 h-3 rounded-full mr-2 shrink-0 ring-1 ring-white/15"
style={{ backgroundColor: normaliseColour(option.color) }}
/>
)}
<span className="text-white">{option.label}</span>
Expand Down
27 changes: 12 additions & 15 deletions frontend/src/components/discord/container/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isSafeUrl } from "@/lib/url";
import { emojiUrl } from "@/lib/discord-cdn";
import { BUTTON_STYLE_COLOURS } from "@/constants/buttonStyles";

interface ButtonProps {
button_style?: number;
Expand All @@ -12,20 +13,8 @@ interface ButtonProps {
}

function convertButtonStyle(style?: number): string {
switch (style) {
case 1:
return "text-white bg-[#4752c4]";
case 2:
return "bg-[#4f545c] text-white";
case 3:
return "text-white bg-[#2d7d46]";
case 4:
return "text-white bg-[#a12d2f]";
case 5:
return "bg-[#4f545c] text-white no-underline";
default:
return "";
}
if (style === undefined || BUTTON_STYLE_COLOURS[style] === undefined) return "";
return style === 5 ? "text-white no-underline" : "text-white";
}

export default function Button({
Expand All @@ -39,12 +28,16 @@ export default function Button({
}: ButtonProps) {
const baseClasses = `inline-flex items-center justify-center align-middle px-4 h-8 min-w-15 rounded text-sm font-medium select-none transition-colors duration-150 ${disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`;
const styleClasses = convertButtonStyle(button_style);
const backgroundStyle = {
backgroundColor: button_style === undefined ? undefined : BUTTON_STYLE_COLOURS[button_style],
};

if (button_style === 5 && url && isSafeUrl(url)) {
return (
<a
href={url}
className={`${baseClasses} ${styleClasses} ${className}`}
style={backgroundStyle}
data-custom-id={custom_id || ""}
target="_blank"
rel="noopener noreferrer"
Expand All @@ -65,7 +58,11 @@ export default function Button({
}

return (
<div className={`${baseClasses} ${styleClasses} ${className}`} data-custom-id={custom_id || ""}>
<div
className={`${baseClasses} ${styleClasses} ${className}`}
style={backgroundStyle}
data-custom-id={custom_id || ""}
>
{emoji &&
(emoji.id && emoji.id !== "0" ? (
<img
Expand Down
27 changes: 23 additions & 4 deletions frontend/src/components/gallery/GalleryListingPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,31 @@ const FORM_INPUT_TYPE_NAMES: Record<number, string> = {
22: "Checkbox Group",
};

export const GALLERY_TYPE_BADGES: Record<string, { label: string; className: string }> = {
panel: { label: "Panel", className: "bg-purple-600/20 text-purple-400" },
tag: { label: "Tag", className: "bg-teal-600/20 text-teal-400" },
form: { label: "Form", className: "bg-orange-600/20 text-orange-400" },
export const GALLERY_TYPE_BADGES: Record<
string,
{ label: string; plural: string; className: string; dot: string }
> = {
panel: {
label: "Panel",
plural: "Panels",
className: "bg-purple-600/20 text-purple-400",
dot: "#C27AFF",
},
tag: { label: "Tag", plural: "Tags", className: "bg-teal-600/20 text-teal-400", dot: "#00D5BE" },
form: {
label: "Form",
plural: "Forms",
className: "bg-orange-600/20 text-orange-400",
dot: "#FF8904",
},
};

export const GALLERY_TYPE_OPTIONS = Object.entries(GALLERY_TYPE_BADGES).map(([key, badge]) => ({
key,
label: badge.plural,
color: badge.dot,
}));

// Anything the parser leaves as plain text renders identically either way.
function hasMarkdown(content: string | undefined): boolean {
if (!content) return false;
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/constants/buttonStyles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const BUTTON_STYLE_PRIMARY = "#5865F2";
export const BUTTON_STYLE_SECONDARY = "#4E5058";
export const BUTTON_STYLE_SUCCESS = "#248046";
export const BUTTON_STYLE_DANGER = "#DA373C";

export const BUTTON_STYLE_OPTIONS = [
{ key: "1", label: "Blue", color: BUTTON_STYLE_PRIMARY },
{ key: "2", label: "Grey", color: BUTTON_STYLE_SECONDARY },
{ key: "3", label: "Green", color: BUTTON_STYLE_SUCCESS },
{ key: "4", label: "Red", color: BUTTON_STYLE_DANGER },
] as const;

// Style 5 (Link) is not panel-selectable, but the preview renders imported gallery panels.
export const BUTTON_STYLE_COLOURS: Record<number, string> = {
...Object.fromEntries(BUTTON_STYLE_OPTIONS.map((o) => [Number(o.key), o.color] as const)),
5: BUTTON_STYLE_SECONDARY,
};
16 changes: 16 additions & 0 deletions frontend/src/lib/colour.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function intToColour(colour: number): string {
return `#${colour.toString(16).padStart(6, "0")}`;
}

export function colourToInt(hex: string): number {
return parseInt(hex.replace("#", ""), 16);
}

// Discord sends 0 for "no colour"; it renders as the default grey, not black.
export function roleColour(colour: number): string {
return colour === 0 ? "#99AAB5" : intToColour(colour);
}

export function normaliseColour(colour: string): string {
return colour.startsWith("#") ? colour : `#${colour}`;
}
12 changes: 9 additions & 3 deletions frontend/src/pages/admin/affiliate/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ const FLAGGED_SORT_COLUMNS: Record<FlaggedSortKey, SortColumn<AdminFlaggedReferr
created_at: { value: (r) => toTime(r.created_at) },
};

const AFFILIATE_STATUS_DOTS: Record<string, string> = {
pending: "#E17100",
active: "#00A63E",
revoked: "#E7000B",
};

function AffiliateStatusBadge({ status }: { status: string }) {
const colours: Record<string, string> = {
pending: "bg-amber-600",
Expand Down Expand Up @@ -541,9 +547,9 @@ export default function AdminAffiliatePage() {
value={statusFilter}
options={[
{ key: "all", label: "All statuses" },
{ key: "pending", label: "Pending" },
{ key: "active", label: "Active" },
{ key: "revoked", label: "Revoked" },
{ key: "pending", label: "Pending", color: AFFILIATE_STATUS_DOTS.pending },
{ key: "active", label: "Active", color: AFFILIATE_STATUS_DOTS.active },
{ key: "revoked", label: "Revoked", color: AFFILIATE_STATUS_DOTS.revoked },
]}
onChange={(v) => {
setStatusFilter(v ?? "all");
Expand Down
12 changes: 9 additions & 3 deletions frontend/src/pages/admin/bot-staff/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ const TIER_BADGE_STYLES: Record<string, string> = {
helper: "bg-gray-600/20 text-gray-400",
};

const TIER_DOTS: Record<string, string> = {
owner: "#C27AFF",
admin: "#51A2FF",
helper: "#99A1AF",
};

const staffRank = (member: BotStaffMember): number => {
if (member.tier === "owner") return 0;
if (member.tier === "admin") return member.global_view ? 1 : 2;
Expand All @@ -46,12 +52,12 @@ export default function BotStaffPage() {
const tierOptions = useMemo(() => {
if (isOwner) {
return [
{ key: "admin", label: "Admin" },
{ key: "helper", label: "Helper" },
{ key: "admin", label: "Admin", color: TIER_DOTS.admin },
{ key: "helper", label: "Helper", color: TIER_DOTS.helper },
];
}
// Admins can only add helpers
return [{ key: "helper", label: "Helper" }];
return [{ key: "helper", label: "Helper", color: TIER_DOTS.helper }];
}, [isOwner]);

const fetchStaff = useCallback(async () => {
Expand Down
13 changes: 5 additions & 8 deletions frontend/src/pages/admin/gallery/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import GalleryPreviewModal from "@/components/modals/GalleryPreviewModal";
import Select from "@/components/Select";
import Slider from "@/components/Slider";
import Tabs from "@/components/Tabs";
import { GALLERY_TYPE_BADGES } from "@/components/gallery/GalleryListingPreview";
import {
GALLERY_TYPE_BADGES,
GALLERY_TYPE_OPTIONS,
} from "@/components/gallery/GalleryListingPreview";
import Textarea from "@/components/Textarea";
import SearchInput from "@/components/SearchInput";
import { useUrlSearch } from "@/hooks/useUrlSearch";
Expand All @@ -28,12 +31,6 @@ const STATUS_BADGE: Record<TabStatus, string> = {
rejected: "bg-red-600/20 text-red-400",
};

const TYPE_OPTIONS = [
{ key: "panel", label: "Panels" },
{ key: "tag", label: "Tags" },
{ key: "form", label: "Forms" },
];

const AdminGalleryPage: FC = () => {
const { user } = useAuthStore();
const canModerate = isAtLeast(user?.admin_tier ?? "", "admin");
Expand Down Expand Up @@ -173,7 +170,7 @@ const AdminGalleryPage: FC = () => {
label="Type"
value={listingType}
onChange={(v) => setListingType(v ?? "all")}
options={[{ key: "all", label: "All types" }, ...TYPE_OPTIONS]}
options={[{ key: "all", label: "All types" }, ...GALLERY_TYPE_OPTIONS]}
placeholder="All types"
hideSearch
className="w-full sm:w-48"
Expand Down
Loading
Loading