845 lines
21 KiB
Vue
845 lines
21 KiB
Vue
<template>
|
|
<PageShell as="form" @submit.prevent="handleSubmit">
|
|
<ClientOnly>
|
|
<div v-if="loading" class="loading-state">
|
|
<p style="color: var(--text-faint)">Loading your profile...</p>
|
|
</div>
|
|
|
|
<div v-else-if="!memberData" class="loading-state">
|
|
<p style="color: var(--text-faint); margin-bottom: 12px">
|
|
Please sign in to access your profile settings.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
@click="
|
|
openLoginModal({
|
|
title: 'Sign in to your profile',
|
|
description: 'Enter your email to manage your profile settings',
|
|
})
|
|
"
|
|
>
|
|
Sign In
|
|
</button>
|
|
</div>
|
|
|
|
<template v-else>
|
|
<!-- PAGE HEADER -->
|
|
<PageHeader title="Edit Profile">
|
|
<NuxtLink
|
|
v-if="
|
|
memberId &&
|
|
memberData?.status === MEMBER_STATUSES.ACTIVE &&
|
|
formData.showInDirectory
|
|
"
|
|
:to="`/members/${memberId}`"
|
|
class="view-profile-link"
|
|
>
|
|
View my public profile →
|
|
</NuxtLink>
|
|
</PageHeader>
|
|
|
|
<ColumnsLayout cols="2">
|
|
<template #left>
|
|
<PageSection>
|
|
<div class="section-label">Basics</div>
|
|
|
|
<div class="field">
|
|
<label>Name</label>
|
|
<input
|
|
v-model="formData.name"
|
|
type="text"
|
|
placeholder="Your name"
|
|
required
|
|
>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Pronouns</label>
|
|
<input
|
|
v-model="formData.pronouns"
|
|
type="text"
|
|
placeholder="e.g., she/her, they/them"
|
|
>
|
|
</div>
|
|
<div class="field">
|
|
<label>Timezone</label>
|
|
<USelectMenu
|
|
v-model="formData.timeZone"
|
|
:items="timezoneItems"
|
|
value-key="value"
|
|
searchable
|
|
searchable-placeholder="Search timezones..."
|
|
placeholder="Select a timezone"
|
|
class="timezone-select"
|
|
:ui="{
|
|
content: 'tz-content',
|
|
item: 'tz-item',
|
|
input: 'tz-input',
|
|
}"
|
|
/>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Avatar</label>
|
|
<div class="avatar-row">
|
|
<button
|
|
v-for="ghost in availableGhosts"
|
|
:key="ghost.value"
|
|
type="button"
|
|
class="avatar-option"
|
|
:class="{ selected: formData.avatar === ghost.value }"
|
|
:title="ghost.label"
|
|
@click="formData.avatar = ghost.value"
|
|
>
|
|
<img :src="ghost.image" :alt="ghost.label" >
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</PageSection>
|
|
|
|
<PageSection divider="top">
|
|
<div class="section-label">About You</div>
|
|
|
|
<div class="row-2">
|
|
<div class="field">
|
|
<label>Studio / Organization</label>
|
|
<input
|
|
v-model="formData.studio"
|
|
type="text"
|
|
placeholder="Studio name"
|
|
>
|
|
</div>
|
|
<div class="field">
|
|
<label>Location</label>
|
|
<input
|
|
v-model="formData.location"
|
|
type="text"
|
|
placeholder="Toronto, ON"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Bio</label>
|
|
<textarea
|
|
v-model="formData.bio"
|
|
rows="4"
|
|
placeholder="Share your background, interests, and experience..."
|
|
maxlength="300"
|
|
/>
|
|
<div class="char-count">
|
|
{{ formData.bio?.length || 0 }} / 300
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>What I Do</label>
|
|
<CraftTagSelector
|
|
v-model="formData.craftTags"
|
|
:tags="craftTags"
|
|
@suggest="openTagSuggest('craft')"
|
|
/>
|
|
</div>
|
|
</PageSection>
|
|
|
|
<PageSection divider="top">
|
|
<div class="section-label">Visibility</div>
|
|
|
|
<div class="toggle-field">
|
|
<USwitch
|
|
v-model="formData.showInDirectory"
|
|
aria-label="Show in Member Directory"
|
|
/>
|
|
<div class="toggle-label">
|
|
Show in Member Directory
|
|
<span class="toggle-sub"
|
|
>Your profile will appear in the private member
|
|
directory.</span
|
|
>
|
|
</div>
|
|
</div>
|
|
</PageSection>
|
|
</template>
|
|
|
|
<template #right>
|
|
<PageSection>
|
|
<div class="section-label">Board</div>
|
|
|
|
<div class="field">
|
|
<label>Gamma Space Slack Handle</label>
|
|
<input
|
|
v-model="formData.boardSlackHandle"
|
|
type="text"
|
|
placeholder="@yourslackname"
|
|
>
|
|
<div class="field-help">
|
|
Shown on your board posts so other members can reach out.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="posts-header">
|
|
<div class="posts-heading">Your Posts</div>
|
|
<NuxtLink to="/board" class="posts-new-link"
|
|
>+ New Post</NuxtLink
|
|
>
|
|
</div>
|
|
|
|
<div v-if="myPosts.length === 0" class="posts-empty">
|
|
No posts yet.
|
|
<NuxtLink to="/board" class="posts-empty-link">
|
|
Visit the Board
|
|
</NuxtLink>
|
|
to share what you're seeking or offering.
|
|
</div>
|
|
|
|
<ul v-else class="posts-list">
|
|
<li v-for="post in myPosts" :key="post._id" class="post-item">
|
|
<div class="post-body">
|
|
<div class="post-title">{{ post.title }}</div>
|
|
<div class="post-excerpt">{{ postExcerpt(post) }}</div>
|
|
</div>
|
|
<div class="post-actions">
|
|
<NuxtLink to="/board" class="post-action">Edit</NuxtLink>
|
|
<button
|
|
type="button"
|
|
class="post-action post-action-danger"
|
|
@click="handleDeletePost(post)"
|
|
>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</PageSection>
|
|
|
|
<PageSection divider="top">
|
|
<div class="section-label">Notifications</div>
|
|
|
|
<div
|
|
v-for="toggle in notificationToggles"
|
|
:key="toggle.key"
|
|
class="toggle-field"
|
|
>
|
|
<USwitch
|
|
v-model="formData.notifications[toggle.key]"
|
|
:aria-label="toggle.label"
|
|
/>
|
|
<div class="toggle-label">
|
|
{{ toggle.label }}
|
|
<span class="toggle-sub">{{ toggle.sub }}</span>
|
|
</div>
|
|
</div>
|
|
</PageSection>
|
|
|
|
<PageSection divider="top">
|
|
<div class="section-label">Recent Activity</div>
|
|
|
|
<div v-if="activityLoading" class="activity-empty">
|
|
Loading activity…
|
|
</div>
|
|
<ul v-else-if="recentActivity.length" class="activity-list">
|
|
<li
|
|
v-for="entry in recentActivity"
|
|
:key="entry._id"
|
|
class="activity-item"
|
|
>
|
|
<div class="activity-time">
|
|
{{ formatActivityTime(entry.timestamp) }}
|
|
</div>
|
|
<div class="activity-text">
|
|
<template v-if="formatActivity(entry).link">
|
|
<span>{{
|
|
formatActivity(entry).text.split(
|
|
formatActivity(entry).linkText,
|
|
)[0]
|
|
}}</span>
|
|
<NuxtLink
|
|
:to="formatActivity(entry).link"
|
|
class="activity-link"
|
|
>
|
|
{{ formatActivity(entry).linkText }}
|
|
</NuxtLink>
|
|
</template>
|
|
<span v-else>{{ formatActivity(entry).text }}</span>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
<div v-else class="activity-empty">
|
|
Your activity will appear here as you use the Guild.
|
|
</div>
|
|
</PageSection>
|
|
</template>
|
|
</ColumnsLayout>
|
|
|
|
<!-- ======== SAVE BAR ======== -->
|
|
<div class="save-bar">
|
|
<button
|
|
type="submit"
|
|
class="btn btn-primary"
|
|
:disabled="saving || !hasChanges"
|
|
>
|
|
{{ saving ? "Saving..." : "Save Profile" }}
|
|
</button>
|
|
<button type="button" class="btn" @click="resetForm">
|
|
Reset Changes
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<template #fallback>
|
|
<div class="loading-state">
|
|
<p style="color: var(--text-faint)">Loading your profile...</p>
|
|
</div>
|
|
</template>
|
|
</ClientOnly>
|
|
|
|
<TagSuggestModal
|
|
v-model:open="showTagSuggestModal"
|
|
:pool="tagSuggestPool"
|
|
/>
|
|
</PageShell>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { MEMBER_STATUSES } from "~/composables/useMemberStatus";
|
|
import { TIMEZONE_OPTIONS } from "~/config/timezones";
|
|
import { formatActivity } from "~/utils/activityText";
|
|
|
|
definePageMeta({
|
|
middleware: "auth",
|
|
});
|
|
|
|
const { memberData, checkMemberStatus } = useAuth();
|
|
const { openLoginModal } = useLoginModal();
|
|
const { posts: myPosts, fetchPosts, deletePost } = useBoardPosts();
|
|
const toast = useToast();
|
|
|
|
const availableGhosts = [
|
|
{
|
|
value: "disbelieving",
|
|
label: "Disbelieving",
|
|
image: "/ghosties/Ghost-Disbelieving.png",
|
|
},
|
|
{
|
|
value: "double-take",
|
|
label: "Double Take",
|
|
image: "/ghosties/Ghost-Double-Take.png",
|
|
},
|
|
{
|
|
value: "exasperated",
|
|
label: "Exasperated",
|
|
image: "/ghosties/Ghost-Exasperated.png",
|
|
},
|
|
{ value: "mild", label: "Mild", image: "/ghosties/Ghost-Mild.png" },
|
|
{ value: "sweet", label: "Sweet", image: "/ghosties/Ghost-Sweet.png" },
|
|
{ value: "wtf", label: "WTF", image: "/ghosties/Ghost-WTF.png" },
|
|
];
|
|
|
|
// Compute current UTC offset for an IANA timezone (DST-aware).
|
|
const utcOffset = (tz) => {
|
|
try {
|
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
timeZone: tz,
|
|
timeZoneName: "longOffset",
|
|
}).formatToParts(new Date());
|
|
const name = parts.find((p) => p.type === "timeZoneName")?.value || "";
|
|
// "GMT-05:00" → "UTC-05:00"; "GMT" → "UTC+00:00"
|
|
if (name === "GMT") return "UTC+00:00";
|
|
return name.replace("GMT", "UTC");
|
|
} catch {
|
|
return "";
|
|
}
|
|
};
|
|
|
|
// Include the saved timezone as a custom option if it's not in the curated list.
|
|
const timezoneItems = computed(() => {
|
|
const saved = formData.timeZone;
|
|
const list = TIMEZONE_OPTIONS.map((t) => {
|
|
const off = utcOffset(t.value);
|
|
return { ...t, label: off ? `${t.label} (${off})` : t.label };
|
|
});
|
|
if (saved && !TIMEZONE_OPTIONS.some((t) => t.value === saved)) {
|
|
const off = utcOffset(saved);
|
|
list.unshift({ label: off ? `${saved} (${off})` : saved, value: saved });
|
|
}
|
|
return list;
|
|
});
|
|
|
|
const notificationToggles = [
|
|
{
|
|
key: "events",
|
|
label: "Registration & cancellation emails",
|
|
sub: "Confirmation when you register for an event, and notice if it's cancelled",
|
|
},
|
|
];
|
|
|
|
const { data: tagsData } = await useFetch("/api/tags");
|
|
|
|
const craftTags = computed(() =>
|
|
(tagsData.value?.tags || []).filter((t) => t.pool === "craft"),
|
|
);
|
|
|
|
const showTagSuggestModal = ref(false);
|
|
const tagSuggestPool = ref("");
|
|
|
|
const openTagSuggest = (pool) => {
|
|
tagSuggestPool.value = pool;
|
|
showTagSuggestModal.value = true;
|
|
};
|
|
|
|
const formData = reactive({
|
|
name: "",
|
|
pronouns: "",
|
|
timeZone: "",
|
|
avatar: "",
|
|
studio: "",
|
|
bio: "",
|
|
location: "",
|
|
showInDirectory: true,
|
|
craftTags: [],
|
|
boardSlackHandle: "",
|
|
notifications: {
|
|
events: true,
|
|
},
|
|
});
|
|
|
|
const loading = ref(false);
|
|
const saving = ref(false);
|
|
const initialData = ref(null);
|
|
|
|
const recentActivity = ref([]);
|
|
const activityLoading = ref(false);
|
|
|
|
const formatActivityTime = (date) => {
|
|
const now = new Date();
|
|
const d = new Date(date);
|
|
const diff = Math.floor((now - d) / 1000);
|
|
if (diff < 60) return "just now";
|
|
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
|
|
return d.toLocaleDateString("en-US", {
|
|
month: "short",
|
|
day: "numeric",
|
|
year: d.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
|
|
});
|
|
};
|
|
|
|
const loadRecentActivity = async () => {
|
|
activityLoading.value = true;
|
|
try {
|
|
const data = await $fetch("/api/members/me/activity", {
|
|
params: { limit: 5 },
|
|
});
|
|
recentActivity.value = data.entries || [];
|
|
} catch (err) {
|
|
console.error("Failed to load activity:", err);
|
|
recentActivity.value = [];
|
|
} finally {
|
|
activityLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const memberId = computed(() => memberData.value?._id || memberData.value?.id);
|
|
|
|
const hasChanges = computed(() => {
|
|
return JSON.stringify(formData) !== JSON.stringify(initialData.value);
|
|
});
|
|
|
|
const loadProfile = () => {
|
|
if (!memberData.value) return;
|
|
|
|
formData.name = memberData.value.name || "";
|
|
formData.pronouns = memberData.value.pronouns || "";
|
|
formData.timeZone = memberData.value.timeZone || "";
|
|
formData.avatar = memberData.value.avatar || "";
|
|
formData.studio = memberData.value.studio || "";
|
|
formData.bio = memberData.value.bio || "";
|
|
formData.location = memberData.value.location || "";
|
|
formData.showInDirectory = memberData.value.showInDirectory ?? true;
|
|
|
|
formData.craftTags = Array.isArray(memberData.value.craftTags)
|
|
? [...memberData.value.craftTags]
|
|
: [];
|
|
|
|
const board = memberData.value.board || {};
|
|
formData.boardSlackHandle = board.slackHandle || "";
|
|
|
|
const notifs = memberData.value.notifications || {};
|
|
formData.notifications.events = notifs.events ?? true;
|
|
|
|
initialData.value = JSON.parse(JSON.stringify(formData));
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
saving.value = true;
|
|
|
|
try {
|
|
await $fetch("/api/members/profile", {
|
|
method: "PATCH",
|
|
body: { ...formData },
|
|
});
|
|
|
|
await checkMemberStatus();
|
|
loadProfile();
|
|
|
|
toast.add({ title: "Profile updated", color: "success" });
|
|
} catch (error) {
|
|
console.error("Profile save error:", error);
|
|
toast.add({
|
|
title: "Update failed",
|
|
description:
|
|
error.data?.statusMessage || error.data?.message || "Please try again.",
|
|
color: "error",
|
|
});
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
};
|
|
|
|
const resetForm = () => {
|
|
loadProfile();
|
|
};
|
|
|
|
onMounted(async () => {
|
|
if (!memberData.value) {
|
|
loading.value = true;
|
|
const isAuthenticated = await checkMemberStatus();
|
|
loading.value = false;
|
|
|
|
if (!isAuthenticated) {
|
|
openLoginModal({
|
|
title: "Sign in to your profile",
|
|
description: "Enter your email to manage your profile settings",
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
loadProfile();
|
|
|
|
if (memberId.value) {
|
|
await Promise.allSettled([
|
|
fetchPosts({ author: memberId.value }),
|
|
loadRecentActivity(),
|
|
]);
|
|
}
|
|
});
|
|
|
|
const postExcerpt = (post) => {
|
|
const text = post.seeking || post.offering || "";
|
|
if (text.length <= 80) return text;
|
|
return text.slice(0, 80).trimEnd() + "...";
|
|
};
|
|
|
|
const handleDeletePost = async (post) => {
|
|
if (!window.confirm(`Delete "${post.title}"?`)) return;
|
|
try {
|
|
await deletePost(post._id, { author: memberId.value });
|
|
} catch (error) {
|
|
console.error("Delete post error:", error);
|
|
toast.add({
|
|
title: "Failed to delete post",
|
|
description: error.data?.message || "Please try again.",
|
|
color: "error",
|
|
});
|
|
}
|
|
};
|
|
|
|
useHead({
|
|
title: "Edit Profile - Ghost Guild",
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* ---- LOADING / EMPTY STATE ---- */
|
|
.loading-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 80px 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
/* ---- MULTI-COLUMN ROWS ---- */
|
|
.row-2 {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
}
|
|
|
|
/* ---- FIELD LABELS (distinct from .section-label) ---- */
|
|
.field label {
|
|
font-size: 11px;
|
|
text-transform: none;
|
|
letter-spacing: normal;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
/* ---- SOLID INPUT BORDERS ---- */
|
|
.field input,
|
|
.field select,
|
|
.field textarea {
|
|
border-style: solid;
|
|
}
|
|
|
|
.field :deep(.tags) {
|
|
border-style: solid;
|
|
}
|
|
|
|
/* ---- VIEW PROFILE LINK ---- */
|
|
.view-profile-link {
|
|
display: inline-block;
|
|
margin-top: 8px;
|
|
font-size: 12px;
|
|
color: var(--candle);
|
|
text-decoration: none;
|
|
}
|
|
.view-profile-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* ---- AVATAR PICKER ---- */
|
|
.avatar-row {
|
|
display: flex;
|
|
gap: 6px;
|
|
align-items: center;
|
|
}
|
|
|
|
.avatar-option {
|
|
width: 40px;
|
|
height: 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--bg);
|
|
cursor: pointer;
|
|
padding: 3px;
|
|
transition: all 0.12s;
|
|
}
|
|
|
|
.avatar-option img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.avatar-option:hover {
|
|
border-color: var(--candle-dim);
|
|
background: var(--surface-hover);
|
|
}
|
|
|
|
.avatar-option.selected {
|
|
border: 2px solid var(--candle);
|
|
background: var(--surface);
|
|
}
|
|
|
|
/* ---- TEXTAREA RESIZE ---- */
|
|
.field textarea {
|
|
resize: vertical;
|
|
}
|
|
|
|
/* ---- CHAR COUNT ---- */
|
|
.char-count {
|
|
font-size: 10px;
|
|
color: var(--text-faint);
|
|
text-align: right;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* ---- TOGGLE FIELD ---- */
|
|
.toggle-field {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.toggle-label {
|
|
font-size: 12px;
|
|
color: var(--text);
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.toggle-sub {
|
|
display: block;
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
margin-top: 1px;
|
|
}
|
|
|
|
/* ---- FIELD HELPER TEXT ---- */
|
|
.field-help {
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* ---- YOUR POSTS LIST ---- */
|
|
.posts-header {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
margin-top: 20px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.posts-heading {
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.posts-new-link {
|
|
font-size: 12px;
|
|
color: var(--candle);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.posts-new-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.posts-empty {
|
|
font-size: 12px;
|
|
color: var(--text-faint);
|
|
padding: 12px 0;
|
|
border-top: 1px dashed var(--border);
|
|
}
|
|
|
|
.posts-empty-link {
|
|
color: var(--candle);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.posts-empty-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.posts-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
border-top: 1px dashed var(--border);
|
|
}
|
|
|
|
.post-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 10px 0;
|
|
border-bottom: 1px dashed var(--border);
|
|
}
|
|
|
|
.post-body {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.post-title {
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.post-excerpt {
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.post-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.post-action {
|
|
font-size: 11px;
|
|
color: var(--candle);
|
|
background: none;
|
|
border: none;
|
|
padding: 0;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.post-action:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.post-action-danger {
|
|
color: var(--ember);
|
|
}
|
|
|
|
/* ---- RECENT ACTIVITY ---- */
|
|
.activity-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
border-top: 1px dashed var(--border);
|
|
}
|
|
.activity-item {
|
|
padding: 8px 0;
|
|
border-bottom: 1px dashed var(--border);
|
|
font-size: 12px;
|
|
color: var(--text);
|
|
line-height: 1.5;
|
|
}
|
|
.activity-time {
|
|
font-size: 10px;
|
|
color: var(--text-faint);
|
|
margin-bottom: 2px;
|
|
}
|
|
.activity-text {
|
|
color: var(--text);
|
|
}
|
|
.activity-link {
|
|
color: var(--candle);
|
|
text-decoration: none;
|
|
}
|
|
.activity-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.activity-empty {
|
|
font-size: 12px;
|
|
color: var(--text-faint);
|
|
padding: 10px 0;
|
|
}
|
|
|
|
/* ---- DISABLED BUTTON ---- */
|
|
.btn:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* ---- SAVE BAR ---- */
|
|
.save-bar {
|
|
flex-shrink: 0;
|
|
padding: 24px 28px;
|
|
margin-top: 0;
|
|
border-top: 1px dashed var(--border);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
/* ---- RESPONSIVE ---- */
|
|
@media (max-width: 768px) {
|
|
.row-2 {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.save-bar {
|
|
padding-left: 16px;
|
|
padding-right: 16px;
|
|
flex-wrap: wrap;
|
|
}
|
|
}
|
|
</style>
|
|
|