Tests, UX improvements.
This commit is contained in:
parent
4e6f5d36b8
commit
0ae18f495e
63 changed files with 1384 additions and 2330 deletions
|
|
@ -167,7 +167,7 @@ const youItems = [
|
|||
{ label: "Dashboard", path: "/member/dashboard" },
|
||||
{ label: "Profile", path: "/member/profile" },
|
||||
{ label: "Account", path: "/member/account" },
|
||||
{ label: "My Updates", path: "/member/my-updates" },
|
||||
{ label: "Activity Log", path: "/member/activity" },
|
||||
];
|
||||
|
||||
const exploreItems = [
|
||||
|
|
|
|||
46
app/components/SidebarLayout.vue
Normal file
46
app/components/SidebarLayout.vue
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<div class="sidebar-layout">
|
||||
<div class="sidebar-layout-main">
|
||||
<slot />
|
||||
</div>
|
||||
<EventsMiniSidebar :events="upcomingEvents" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
limit: { type: Number, default: 3 },
|
||||
})
|
||||
|
||||
const { data: upcomingEvents } = await useFetch('/api/events', {
|
||||
query: { limit: props.limit, upcoming: true },
|
||||
default: () => [],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar-layout {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 200px;
|
||||
align-items: stretch;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sidebar-layout-main {
|
||||
min-width: 0;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.sidebar-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
Now let me apply this to each page. Let me update all four in parallel:
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
<template>
|
||||
<UCard variant="outline" class="update-card">
|
||||
<div class="flex gap-4">
|
||||
<!-- Avatar -->
|
||||
<div class="flex-shrink-0">
|
||||
<img
|
||||
v-if="update.author?.avatar"
|
||||
:src="`/ghosties/Ghost-${capitalize(update.author.avatar)}.png`"
|
||||
:alt="update.author.name"
|
||||
class="w-12 h-12 rounded-full"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="w-12 h-12 rounded-full bg-guild-700 flex items-center justify-center text-guild-300 font-bold"
|
||||
>
|
||||
{{ update.author?.name?.charAt(0)?.toUpperCase() || "?" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- Header -->
|
||||
<div class="flex items-start justify-between gap-4 mb-2">
|
||||
<div>
|
||||
<h3 class="font-semibold text-guild-100">
|
||||
<NuxtLink
|
||||
v-if="update.author?._id"
|
||||
:to="`/updates/user/${update.author._id}`"
|
||||
class="hover:text-guild-300 transition-colors"
|
||||
>
|
||||
{{ update.author.name }}
|
||||
</NuxtLink>
|
||||
<span v-else>Unknown Member</span>
|
||||
</h3>
|
||||
<div class="flex items-center gap-2 text-sm text-guild-400">
|
||||
<time :datetime="update.createdAt">
|
||||
{{ formatDate(update.createdAt) }}
|
||||
</time>
|
||||
<span v-if="isEdited" class="text-guild-500">(edited)</span>
|
||||
<span
|
||||
v-if="update.privacy === 'private'"
|
||||
class="px-2 py-0.5 bg-guild-700 text-guild-300 rounded text-xs"
|
||||
>
|
||||
Private
|
||||
</span>
|
||||
<span
|
||||
v-if="update.privacy === 'public'"
|
||||
class="px-2 py-0.5 bg-guild-700 text-guild-300 rounded text-xs"
|
||||
>
|
||||
Public
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions (for author only) -->
|
||||
<div v-if="isAuthor" class="flex gap-2">
|
||||
<UButton
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
size="xs"
|
||||
icon="i-lucide-edit"
|
||||
aria-label="Edit update"
|
||||
@click="$emit('edit', update)"
|
||||
/>
|
||||
<UButton
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
size="xs"
|
||||
icon="i-lucide-trash-2"
|
||||
aria-label="Delete update"
|
||||
@click="$emit('delete', update)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="text-guild-200 whitespace-pre-wrap break-words mb-3">
|
||||
<template v-if="showPreview && update.content.length > 300">
|
||||
{{ update.content.substring(0, 300) }}...
|
||||
<NuxtLink
|
||||
:to="`/updates/${update._id}`"
|
||||
class="text-guild-400 hover:text-guild-300 ml-1"
|
||||
>
|
||||
Read more
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ update.content }}
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Images (if any) -->
|
||||
<div v-if="update.images?.length" class="mb-3 space-y-2">
|
||||
<img
|
||||
v-for="(image, index) in update.images"
|
||||
:key="index"
|
||||
:src="image.url"
|
||||
:alt="image.alt || 'Update image'"
|
||||
class="rounded-lg max-w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Footer actions -->
|
||||
<div class="flex items-center gap-4 text-sm text-guild-400">
|
||||
<NuxtLink
|
||||
:to="`/updates/${update._id}`"
|
||||
class="hover:text-guild-300 transition-colors"
|
||||
>
|
||||
View full update
|
||||
</NuxtLink>
|
||||
<span v-if="update.commentsEnabled" class="text-guild-500">
|
||||
Comments (coming soon)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
update: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showPreview: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(["edit", "delete"]);
|
||||
|
||||
const { memberData } = useAuth();
|
||||
|
||||
const isAuthor = computed(() => {
|
||||
return memberData.value && props.update.author?._id === memberData.value.id;
|
||||
});
|
||||
|
||||
const isEdited = computed(() => {
|
||||
const created = new Date(props.update.createdAt).getTime();
|
||||
const updated = new Date(props.update.updatedAt).getTime();
|
||||
return updated - created > 1000; // More than 1 second difference
|
||||
});
|
||||
|
||||
const capitalize = (str) => {
|
||||
if (!str) return "";
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
||||
|
||||
const handleImageError = (e) => {
|
||||
e.target.src = "/ghosties/Ghost-Mild.png"; // Fallback ghost
|
||||
};
|
||||
|
||||
const formatDate = (date) => {
|
||||
const now = new Date();
|
||||
const updateDate = new Date(date);
|
||||
const diffInSeconds = Math.floor((now - updateDate) / 1000);
|
||||
|
||||
if (diffInSeconds < 60) return "just now";
|
||||
if (diffInSeconds < 3600)
|
||||
return `${Math.floor(diffInSeconds / 60)} minutes ago`;
|
||||
if (diffInSeconds < 86400)
|
||||
return `${Math.floor(diffInSeconds / 3600)} hours ago`;
|
||||
if (diffInSeconds < 604800)
|
||||
return `${Math.floor(diffInSeconds / 86400)} days ago`;
|
||||
|
||||
return updateDate.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year:
|
||||
updateDate.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.update-card {
|
||||
background-color: var(--color-guild-800);
|
||||
border-color: var(--color-guild-600);
|
||||
}
|
||||
|
||||
.update-card:hover {
|
||||
border-color: var(--color-guild-500);
|
||||
}
|
||||
|
||||
:deep(.card) {
|
||||
background-color: var(--color-guild-800);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
<template>
|
||||
<div class="space-y-6">
|
||||
<UFormField label="What's on your mind?" name="content" required>
|
||||
<UTextarea
|
||||
v-model="formData.content"
|
||||
placeholder="Share your thoughts, updates, questions, or learnings with the community..."
|
||||
:rows="8"
|
||||
autoresize
|
||||
:maxrows="20"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<!-- Privacy Settings -->
|
||||
<div class="border border-guild-700 rounded-lg p-4 bg-guild-800/30">
|
||||
<h3 class="text-sm font-medium text-guild-200 mb-4">Privacy Settings</h3>
|
||||
<div class="space-y-3">
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
v-model="formData.privacy"
|
||||
type="radio"
|
||||
value="public"
|
||||
class="w-4 h-4 text-guild-400"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-guild-200 font-medium">Public</div>
|
||||
<div class="text-sm text-guild-400">
|
||||
Visible to everyone, including non-members
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
v-model="formData.privacy"
|
||||
type="radio"
|
||||
value="members"
|
||||
class="w-4 h-4 text-guild-400"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-guild-200 font-medium">Members Only</div>
|
||||
<div class="text-sm text-guild-400">
|
||||
Only visible to Ghost Guild members
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
v-model="formData.privacy"
|
||||
type="radio"
|
||||
value="private"
|
||||
class="w-4 h-4 text-guild-400"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-guild-200 font-medium">Private</div>
|
||||
<div class="text-sm text-guild-400">Only visible to you</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Upload (Future) -->
|
||||
<!-- TODO: Add image upload integration with Cloudinary -->
|
||||
|
||||
<!-- Comments Toggle -->
|
||||
<div class="flex items-center gap-3">
|
||||
<USwitch v-model="formData.commentsEnabled" />
|
||||
<div>
|
||||
<div class="text-guild-200 font-medium">Enable Comments</div>
|
||||
<div class="text-sm text-guild-400">
|
||||
Allow members to comment on this update
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div
|
||||
class="flex justify-between items-center pt-4 border-t border-guild-700"
|
||||
>
|
||||
<UButton variant="ghost" color="neutral" @click="$emit('cancel')">
|
||||
Cancel
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
:loading="submitting"
|
||||
:disabled="!formData.content.trim()"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ submitLabel }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div
|
||||
v-if="error"
|
||||
class="bg-ember-900/20 border border-ember-400/30 rounded-lg p-4"
|
||||
>
|
||||
<p class="text-ember-400">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
initialData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
content: "",
|
||||
privacy: "members",
|
||||
commentsEnabled: true,
|
||||
images: [],
|
||||
}),
|
||||
},
|
||||
submitLabel: {
|
||||
type: String,
|
||||
default: "Post Update",
|
||||
},
|
||||
submitting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
error: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["submit", "cancel"]);
|
||||
|
||||
const formData = reactive({
|
||||
content: props.initialData.content || "",
|
||||
privacy: props.initialData.privacy || "members",
|
||||
commentsEnabled: props.initialData.commentsEnabled ?? true,
|
||||
images: props.initialData.images || [],
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formData.content.trim()) return;
|
||||
emit("submit", { ...formData });
|
||||
};
|
||||
|
||||
// Watch for initialData changes (for edit mode)
|
||||
watch(
|
||||
() => props.initialData,
|
||||
(newData) => {
|
||||
if (newData) {
|
||||
formData.content = newData.content || "";
|
||||
formData.privacy = newData.privacy || "members";
|
||||
formData.commentsEnabled = newData.commentsEnabled ?? true;
|
||||
formData.images = newData.images || [];
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Field labels */
|
||||
:deep(label) {
|
||||
color: var(--color-guild-200) !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Textarea styling */
|
||||
:deep(textarea) {
|
||||
background-color: var(--color-guild-800) !important;
|
||||
color: var(--color-guild-200) !important;
|
||||
border-color: var(--color-guild-600) !important;
|
||||
}
|
||||
|
||||
:deep(textarea::placeholder) {
|
||||
color: var(--color-guild-500) !important;
|
||||
}
|
||||
|
||||
:deep(textarea:focus) {
|
||||
border-color: var(--color-guild-400) !important;
|
||||
background-color: var(--color-guild-700) !important;
|
||||
}
|
||||
|
||||
/* Radio buttons */
|
||||
input[type="radio"] {
|
||||
accent-color: var(--color-candlelight-600);
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue