generated from nhcarrigan/template
feat: add race/class to character sheet, public /character/:id page
Players can now set their character's race and class in the Character tab. A new public page at /character/:id displays the full character sheet — name, pronouns, race, class, bio, and guild lore.
This commit is contained in:
@@ -15,6 +15,8 @@ model Player {
|
|||||||
avatar String?
|
avatar String?
|
||||||
characterName String @default("")
|
characterName String @default("")
|
||||||
pronouns String @default("")
|
pronouns String @default("")
|
||||||
|
characterRace String @default("")
|
||||||
|
characterClass String @default("")
|
||||||
bio String @default("")
|
bio String @default("")
|
||||||
guildName String @default("")
|
guildName String @default("")
|
||||||
guildDescription String @default("")
|
guildDescription String @default("")
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ profileRouter.get("/:discordId", async (context) => {
|
|||||||
return context.json({
|
return context.json({
|
||||||
characterName: player.characterName,
|
characterName: player.characterName,
|
||||||
pronouns: player.pronouns ?? "",
|
pronouns: player.pronouns ?? "",
|
||||||
|
characterRace: player.characterRace ?? "",
|
||||||
|
characterClass: player.characterClass ?? "",
|
||||||
username: player.username,
|
username: player.username,
|
||||||
avatar: player.avatar ?? null,
|
avatar: player.avatar ?? null,
|
||||||
bio: player.bio ?? "",
|
bio: player.bio ?? "",
|
||||||
@@ -103,6 +105,8 @@ profileRouter.put("/", authMiddleware, async (context) => {
|
|||||||
|
|
||||||
const characterName = (body.characterName ?? "").trim().slice(0, 32);
|
const characterName = (body.characterName ?? "").trim().slice(0, 32);
|
||||||
const pronouns = (body.pronouns ?? "").trim().slice(0, 20);
|
const pronouns = (body.pronouns ?? "").trim().slice(0, 20);
|
||||||
|
const characterRace = (body.characterRace ?? "").trim().slice(0, 32);
|
||||||
|
const characterClass = (body.characterClass ?? "").trim().slice(0, 32);
|
||||||
const bio = (body.bio ?? "").trim().slice(0, 200);
|
const bio = (body.bio ?? "").trim().slice(0, 200);
|
||||||
const guildName = (body.guildName ?? "").trim().slice(0, 64);
|
const guildName = (body.guildName ?? "").trim().slice(0, 64);
|
||||||
const guildDescription = (body.guildDescription ?? "").trim().slice(0, 500);
|
const guildDescription = (body.guildDescription ?? "").trim().slice(0, 500);
|
||||||
@@ -135,12 +139,14 @@ profileRouter.put("/", authMiddleware, async (context) => {
|
|||||||
|
|
||||||
const updated = await prisma.player.update({
|
const updated = await prisma.player.update({
|
||||||
where: { discordId },
|
where: { discordId },
|
||||||
data: { characterName, pronouns, bio, guildName, guildDescription, profileSettings: profileSettings as object },
|
data: { characterName, pronouns, characterRace, characterClass, bio, guildName, guildDescription, profileSettings: profileSettings as object },
|
||||||
});
|
});
|
||||||
|
|
||||||
return context.json({
|
return context.json({
|
||||||
characterName: updated.characterName,
|
characterName: updated.characterName,
|
||||||
pronouns: updated.pronouns,
|
pronouns: updated.pronouns,
|
||||||
|
characterRace: updated.characterRace,
|
||||||
|
characterClass: updated.characterClass,
|
||||||
bio: updated.bio,
|
bio: updated.bio,
|
||||||
guildName: updated.guildName,
|
guildName: updated.guildName,
|
||||||
guildDescription: updated.guildDescription,
|
guildDescription: updated.guildDescription,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { GameProvider } from "./context/GameContext.js";
|
import { GameProvider } from "./context/GameContext.js";
|
||||||
|
import { CharacterPage } from "./components/game/CharacterPage.js";
|
||||||
import { GameLayout } from "./components/game/GameLayout.js";
|
import { GameLayout } from "./components/game/GameLayout.js";
|
||||||
import { LoginPage } from "./components/game/LoginPage.js";
|
import { LoginPage } from "./components/game/LoginPage.js";
|
||||||
import { ProfilePage } from "./components/game/ProfilePage.js";
|
import { ProfilePage } from "./components/game/ProfilePage.js";
|
||||||
@@ -9,6 +10,11 @@ const getProfileDiscordId = (): string | null => {
|
|||||||
return match?.[1] ?? null;
|
return match?.[1] ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCharacterDiscordId = (): string | null => {
|
||||||
|
const match = /^\/character\/(\d+)$/.exec(window.location.pathname);
|
||||||
|
return match?.[1] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
const handleAuthCallback = (): boolean => {
|
const handleAuthCallback = (): boolean => {
|
||||||
if (window.location.pathname !== "/auth/callback") {
|
if (window.location.pathname !== "/auth/callback") {
|
||||||
return false;
|
return false;
|
||||||
@@ -38,6 +44,11 @@ export const App = (): React.JSX.Element => {
|
|||||||
return <ProfilePage discordId={profileDiscordId} />;
|
return <ProfilePage discordId={profileDiscordId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const characterDiscordId = getCharacterDiscordId();
|
||||||
|
if (characterDiscordId) {
|
||||||
|
return <CharacterPage discordId={characterDiscordId} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (!loggedIn) {
|
if (!loggedIn) {
|
||||||
return <LoginPage onLogin={() => { setLoggedIn(true); }} />;
|
return <LoginPage onLogin={() => { setLoggedIn(true); }} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import type { PublicProfileResponse } from "@elysium/types";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface CharacterPageProps {
|
||||||
|
discordId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CharacterPage = ({ discordId }: CharacterPageProps): React.JSX.Element => {
|
||||||
|
const [profile, setProfile] = useState<PublicProfileResponse | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`/api/profile/${discordId}`)
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw new Error("Player not found");
|
||||||
|
return res.json() as Promise<PublicProfileResponse>;
|
||||||
|
})
|
||||||
|
.then(setProfile)
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to load character sheet");
|
||||||
|
});
|
||||||
|
}, [discordId]);
|
||||||
|
|
||||||
|
const handleCopy = (): void => {
|
||||||
|
void navigator.clipboard.writeText(window.location.href).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => { setCopied(false); }, 2000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="character-page">
|
||||||
|
<div className="character-page-error">
|
||||||
|
<p>⚠️ {error}</p>
|
||||||
|
<a className="character-page-link" href="/">← Play Elysium</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profile) {
|
||||||
|
return (
|
||||||
|
<div className="character-page">
|
||||||
|
<div className="character-page-loading">Loading character sheet…</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatarUrl = profile.avatar
|
||||||
|
? `https://cdn.discordapp.com/avatars/${discordId}/${profile.avatar}.png?size=128`
|
||||||
|
: `https://cdn.discordapp.com/embed/avatars/${parseInt(discordId, 10) % 5}.png`;
|
||||||
|
|
||||||
|
const subtitle = [profile.characterRace, profile.characterClass].filter(Boolean).join(" · ");
|
||||||
|
const hasBadge = profile.apotheosisCount > 0 || profile.transcendenceCount > 0 || profile.prestigeCount > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="character-page">
|
||||||
|
<div className="character-page-card">
|
||||||
|
<div className="character-page-header">
|
||||||
|
<img
|
||||||
|
alt={`${profile.characterName || profile.username}'s avatar`}
|
||||||
|
className="character-page-avatar"
|
||||||
|
src={avatarUrl}
|
||||||
|
/>
|
||||||
|
<div className="character-page-identity">
|
||||||
|
<h1 className="character-page-name">
|
||||||
|
{profile.characterName || profile.username}
|
||||||
|
</h1>
|
||||||
|
{profile.pronouns && (
|
||||||
|
<p className="character-page-pronouns">{profile.pronouns}</p>
|
||||||
|
)}
|
||||||
|
{subtitle && (
|
||||||
|
<p className="character-page-subtitle">{subtitle}</p>
|
||||||
|
)}
|
||||||
|
{hasBadge && (
|
||||||
|
<div className="character-page-badges">
|
||||||
|
{profile.apotheosisCount > 0 && (
|
||||||
|
<span className="character-page-badge character-page-badge--apotheosis">
|
||||||
|
✨ Apotheosis {profile.apotheosisCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{profile.transcendenceCount > 0 && (
|
||||||
|
<span className="character-page-badge character-page-badge--transcendence">
|
||||||
|
🌌 Transcendence {profile.transcendenceCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{profile.prestigeCount > 0 && (
|
||||||
|
<span className="character-page-badge character-page-badge--prestige">
|
||||||
|
⭐ Prestige {profile.prestigeCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{profile.bio && (
|
||||||
|
<div className="character-page-section">
|
||||||
|
<h2 className="character-page-section-title">⚔️ About</h2>
|
||||||
|
<p className="character-page-bio">{profile.bio}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{profile.guildName && (
|
||||||
|
<div className="character-page-section">
|
||||||
|
<h2 className="character-page-section-title">🏰 Guild</h2>
|
||||||
|
<p className="character-page-guild-name">{profile.guildName}</p>
|
||||||
|
{profile.guildDescription && (
|
||||||
|
<p className="character-page-guild-desc">{profile.guildDescription}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="character-page-divider" />
|
||||||
|
|
||||||
|
<p className="character-page-player-line">
|
||||||
|
Played by <span className="character-page-username">@{profile.username}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="character-page-actions">
|
||||||
|
<button
|
||||||
|
className="character-page-share-btn"
|
||||||
|
onClick={handleCopy}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{copied ? "✓ Copied!" : "🔗 Share Character"}
|
||||||
|
</button>
|
||||||
|
<a className="character-page-profile-link" href={`/profile/${discordId}`}>
|
||||||
|
📊 View Stats
|
||||||
|
</a>
|
||||||
|
<a className="character-page-play-link" href="/">
|
||||||
|
⚔️ Play Elysium
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,6 +7,8 @@ import { useGame } from "../../context/GameContext.js";
|
|||||||
interface CharacterSheetData {
|
interface CharacterSheetData {
|
||||||
characterName: string;
|
characterName: string;
|
||||||
pronouns: string;
|
pronouns: string;
|
||||||
|
characterRace: string;
|
||||||
|
characterClass: string;
|
||||||
bio: string;
|
bio: string;
|
||||||
guildName: string;
|
guildName: string;
|
||||||
guildDescription: string;
|
guildDescription: string;
|
||||||
@@ -15,6 +17,8 @@ interface CharacterSheetData {
|
|||||||
const EMPTY_SHEET: CharacterSheetData = {
|
const EMPTY_SHEET: CharacterSheetData = {
|
||||||
characterName: "",
|
characterName: "",
|
||||||
pronouns: "",
|
pronouns: "",
|
||||||
|
characterRace: "",
|
||||||
|
characterClass: "",
|
||||||
bio: "",
|
bio: "",
|
||||||
guildName: "",
|
guildName: "",
|
||||||
guildDescription: "",
|
guildDescription: "",
|
||||||
@@ -41,6 +45,8 @@ export const CharacterSheetPanel = (): React.JSX.Element => {
|
|||||||
const data = (await res.json()) as {
|
const data = (await res.json()) as {
|
||||||
characterName: string;
|
characterName: string;
|
||||||
pronouns: string;
|
pronouns: string;
|
||||||
|
characterRace: string;
|
||||||
|
characterClass: string;
|
||||||
bio: string;
|
bio: string;
|
||||||
guildName: string;
|
guildName: string;
|
||||||
guildDescription: string;
|
guildDescription: string;
|
||||||
@@ -49,6 +55,8 @@ export const CharacterSheetPanel = (): React.JSX.Element => {
|
|||||||
const loaded: CharacterSheetData = {
|
const loaded: CharacterSheetData = {
|
||||||
characterName: data.characterName ?? "",
|
characterName: data.characterName ?? "",
|
||||||
pronouns: data.pronouns ?? "",
|
pronouns: data.pronouns ?? "",
|
||||||
|
characterRace: data.characterRace ?? "",
|
||||||
|
characterClass: data.characterClass ?? "",
|
||||||
bio: data.bio ?? "",
|
bio: data.bio ?? "",
|
||||||
guildName: data.guildName ?? "",
|
guildName: data.guildName ?? "",
|
||||||
guildDescription: data.guildDescription ?? "",
|
guildDescription: data.guildDescription ?? "",
|
||||||
@@ -80,6 +88,8 @@ export const CharacterSheetPanel = (): React.JSX.Element => {
|
|||||||
await updateProfile({
|
await updateProfile({
|
||||||
characterName: draft.characterName || (player?.characterName ?? ""),
|
characterName: draft.characterName || (player?.characterName ?? ""),
|
||||||
pronouns: draft.pronouns,
|
pronouns: draft.pronouns,
|
||||||
|
characterRace: draft.characterRace,
|
||||||
|
characterClass: draft.characterClass,
|
||||||
bio: draft.bio,
|
bio: draft.bio,
|
||||||
guildName: draft.guildName,
|
guildName: draft.guildName,
|
||||||
guildDescription: draft.guildDescription,
|
guildDescription: draft.guildDescription,
|
||||||
@@ -137,6 +147,30 @@ export const CharacterSheetPanel = (): React.JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
<span className="character-sheet-hint">{draft.pronouns.length} / 20</span>
|
<span className="character-sheet-hint">{draft.pronouns.length} / 20</span>
|
||||||
|
|
||||||
|
<label className="character-sheet-label" htmlFor="cs-race">Race</label>
|
||||||
|
<input
|
||||||
|
className="character-sheet-input"
|
||||||
|
id="cs-race"
|
||||||
|
maxLength={32}
|
||||||
|
placeholder="e.g. Elf, Dwarf, Human, Tiefling…"
|
||||||
|
type="text"
|
||||||
|
value={draft.characterRace}
|
||||||
|
onChange={(e) => { setDraft((d) => ({ ...d, characterRace: e.target.value })); }}
|
||||||
|
/>
|
||||||
|
<span className="character-sheet-hint">{draft.characterRace.length} / 32</span>
|
||||||
|
|
||||||
|
<label className="character-sheet-label" htmlFor="cs-class">Class</label>
|
||||||
|
<input
|
||||||
|
className="character-sheet-input"
|
||||||
|
id="cs-class"
|
||||||
|
maxLength={32}
|
||||||
|
placeholder="e.g. Paladin, Archmage, Shadow Rogue…"
|
||||||
|
type="text"
|
||||||
|
value={draft.characterClass}
|
||||||
|
onChange={(e) => { setDraft((d) => ({ ...d, characterClass: e.target.value })); }}
|
||||||
|
/>
|
||||||
|
<span className="character-sheet-hint">{draft.characterClass.length} / 32</span>
|
||||||
|
|
||||||
<label className="character-sheet-label" htmlFor="cs-bio">About Your Character</label>
|
<label className="character-sheet-label" htmlFor="cs-bio">About Your Character</label>
|
||||||
<textarea
|
<textarea
|
||||||
className="character-sheet-textarea"
|
className="character-sheet-textarea"
|
||||||
@@ -198,6 +232,8 @@ export const CharacterSheetPanel = (): React.JSX.Element => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subtitle = [sheet.characterRace, sheet.characterClass].filter(Boolean).join(" · ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel character-sheet-panel">
|
<section className="panel character-sheet-panel">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
@@ -222,6 +258,12 @@ export const CharacterSheetPanel = (): React.JSX.Element => {
|
|||||||
<span className="character-sheet-field-value">{sheet.pronouns}</span>
|
<span className="character-sheet-field-value">{sheet.pronouns}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{subtitle && (
|
||||||
|
<div className="character-sheet-field">
|
||||||
|
<span className="character-sheet-field-label">Identity</span>
|
||||||
|
<span className="character-sheet-field-value">{subtitle}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{sheet.bio && (
|
{sheet.bio && (
|
||||||
<div className="character-sheet-bio">
|
<div className="character-sheet-bio">
|
||||||
<span className="character-sheet-field-label">About</span>
|
<span className="character-sheet-field-label">About</span>
|
||||||
|
|||||||
@@ -3242,3 +3242,211 @@ body {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===================== CHARACTER PAGE (public /character/:id) ===================== */
|
||||||
|
.character-page {
|
||||||
|
align-items: center;
|
||||||
|
background: var(--colour-bg);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-loading,
|
||||||
|
.character-page-error {
|
||||||
|
color: var(--colour-text);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-card {
|
||||||
|
background: var(--colour-surface);
|
||||||
|
border: 1px solid var(--colour-border);
|
||||||
|
border-radius: calc(var(--radius) * 2);
|
||||||
|
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.4);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
max-width: 640px;
|
||||||
|
padding: 2.5rem 2rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-avatar {
|
||||||
|
border: 3px solid var(--colour-accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 96px;
|
||||||
|
object-fit: cover;
|
||||||
|
width: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-identity {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-name {
|
||||||
|
color: var(--colour-accent);
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-pronouns {
|
||||||
|
color: var(--colour-text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-subtitle {
|
||||||
|
color: var(--colour-text);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-badge {
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-badge--apotheosis {
|
||||||
|
background: rgba(255, 215, 0, 0.15);
|
||||||
|
border: 1px solid gold;
|
||||||
|
color: gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-badge--transcendence {
|
||||||
|
background: rgba(100, 149, 237, 0.15);
|
||||||
|
border: 1px solid cornflowerblue;
|
||||||
|
color: cornflowerblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-badge--prestige {
|
||||||
|
background: rgba(160, 82, 255, 0.15);
|
||||||
|
border: 1px solid var(--colour-accent);
|
||||||
|
color: var(--colour-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-section {
|
||||||
|
border-top: 1px solid var(--colour-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-section-title {
|
||||||
|
color: var(--colour-accent);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-bio {
|
||||||
|
color: var(--colour-text);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-guild-name {
|
||||||
|
color: var(--colour-text);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-guild-desc {
|
||||||
|
color: var(--colour-text);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--colour-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-player-line {
|
||||||
|
color: var(--colour-text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-username {
|
||||||
|
color: var(--colour-text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-share-btn,
|
||||||
|
.character-page-profile-link,
|
||||||
|
.character-page-play-link,
|
||||||
|
.character-page-link {
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-share-btn {
|
||||||
|
background: var(--colour-surface);
|
||||||
|
border: 1px solid var(--colour-border);
|
||||||
|
color: var(--colour-text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-share-btn:hover {
|
||||||
|
border-color: var(--colour-accent);
|
||||||
|
color: var(--colour-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-profile-link {
|
||||||
|
background: var(--colour-surface);
|
||||||
|
border: 1px solid var(--colour-border);
|
||||||
|
color: var(--colour-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-profile-link:hover {
|
||||||
|
border-color: var(--colour-accent);
|
||||||
|
color: var(--colour-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-play-link,
|
||||||
|
.character-page-link {
|
||||||
|
background: var(--colour-accent);
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-page-play-link:hover,
|
||||||
|
.character-page-link:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ export interface BuyPrestigeUpgradeResponse {
|
|||||||
export interface PublicProfileResponse {
|
export interface PublicProfileResponse {
|
||||||
characterName: string;
|
characterName: string;
|
||||||
pronouns: string;
|
pronouns: string;
|
||||||
|
characterRace: string;
|
||||||
|
characterClass: string;
|
||||||
username: string;
|
username: string;
|
||||||
avatar: string | null;
|
avatar: string | null;
|
||||||
bio: string;
|
bio: string;
|
||||||
@@ -121,6 +123,8 @@ export interface PublicProfileResponse {
|
|||||||
export interface UpdateProfileRequest {
|
export interface UpdateProfileRequest {
|
||||||
characterName: string;
|
characterName: string;
|
||||||
pronouns: string;
|
pronouns: string;
|
||||||
|
characterRace: string;
|
||||||
|
characterClass: string;
|
||||||
bio: string;
|
bio: string;
|
||||||
guildName: string;
|
guildName: string;
|
||||||
guildDescription: string;
|
guildDescription: string;
|
||||||
@@ -130,6 +134,8 @@ export interface UpdateProfileRequest {
|
|||||||
export interface UpdateProfileResponse {
|
export interface UpdateProfileResponse {
|
||||||
characterName: string;
|
characterName: string;
|
||||||
pronouns: string;
|
pronouns: string;
|
||||||
|
characterRace: string;
|
||||||
|
characterClass: string;
|
||||||
bio: string;
|
bio: string;
|
||||||
guildName: string;
|
guildName: string;
|
||||||
guildDescription: string;
|
guildDescription: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user