Files
notely/frontend/src/components/NoteList.vue
domrichardson df40cc57e1 first commit
2026-03-24 16:03:04 +00:00

222 lines
4.7 KiB
Vue

<template>
<div class="note-list" :class="{ 'note-list--list': viewMode === 'list' }">
<div v-if="notes.length === 0" class="empty-notes-state" role="status" aria-live="polite">
<i class="mdi mdi-file-document-outline empty-notes-icon" aria-hidden="true"></i>
<h3 class="empty-notes-title">No Notes Yet</h3>
<p class="empty-notes-message">This space is empty for now. Create your first note to get started.</p>
</div>
<div v-for="note in notes" :key="note.id" class="note-card" :class="{ 'is-pinned': note.is_pinned, 'is-featured': note.is_favorite || note.is_featured }" @click="selectNote(note)">
<h5 class="note-title">
<i v-if="note.is_pinned" class="mdi mdi-pin pin-icon" aria-hidden="true"></i>
<i v-else-if="note.is_favorite || note.is_featured" class="mdi mdi-star featured-icon" aria-hidden="true"></i>
{{ note.title }}
</h5>
<p class="note-preview">{{ getDescription(note) }}</p>
<small class="text-muted">Updated: {{ formatDate(note.updated_at) }}</small>
</div>
<div v-if="canLoadMore" class="list-footer">
<button class="btn btn-outline-secondary" :disabled="isLoadingMore" @click="emit('loadMore')">
{{ isLoadingMore ? "Loading..." : "Load more" }}
</button>
</div>
</div>
</template>
<script setup>
defineProps({
notes: {
type: Array,
default: () => [],
},
canLoadMore: {
type: Boolean,
default: false,
},
isLoadingMore: {
type: Boolean,
default: false,
},
viewMode: {
type: String,
default: "grid",
},
});
const emit = defineEmits(["selectNote", "loadMore"]);
const selectNote = (note) => {
emit("selectNote", note);
};
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString();
};
const getDescription = (note) => {
const description = (note?.description || "").trim();
if (!description) {
return "No description";
}
return description;
};
</script>
<style scoped>
.note-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.empty-notes-state {
grid-column: 1 / -1;
min-height: 48vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
border: 1px dashed #cfd6e4;
border-radius: 14px;
background: linear-gradient(180deg, #f8f9fc 0%, #eef3fb 100%);
padding: 2rem 1.5rem;
}
.empty-notes-icon {
font-size: 5.25rem;
line-height: 1;
color: #60789a;
margin-bottom: 0.85rem;
}
.empty-notes-title {
margin: 0;
color: #23364f;
font-size: 1.8rem;
font-weight: 700;
}
.empty-notes-message {
margin: 0.75rem 0 0;
max-width: 460px;
color: #4f637d;
font-size: 1.05rem;
}
.note-card {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
.note-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.note-title {
margin-bottom: 0.5rem;
color: #333;
display: flex;
align-items: center;
gap: 0.3rem;
}
.pin-icon {
color: #408aca;
font-size: 0.9em;
flex-shrink: 0;
}
.featured-icon {
color: #f08c00;
font-size: 0.95em;
flex-shrink: 0;
}
.note-card.is-pinned {
background: #dbf5ff;
border-color: #a8d1ff;
}
.note-card.is-featured {
border-color: #ffd8a8;
background: #fff9db;
}
.note-preview {
color: #666;
margin-bottom: 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.list-footer {
grid-column: 1 / -1;
display: flex;
justify-content: center;
margin-top: 0.5rem;
}
/* List view overrides */
.note-list--list {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.note-list--list .note-card {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.6rem 1rem;
border-radius: 6px;
}
.note-list--list .note-card:hover {
transform: none;
}
.note-list--list .note-title {
flex: 0 0 220px;
margin-bottom: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.note-list--list .note-preview {
flex: 1;
margin-bottom: 0;
}
.note-list--list .note-card > small {
flex: 0 0 auto;
white-space: nowrap;
}
.note-list--list .list-footer {
grid-column: unset;
}
@media (max-width: 768px) {
.empty-notes-state {
min-height: 40vh;
padding: 1.5rem 1rem;
}
.empty-notes-icon {
font-size: 4.3rem;
}
.empty-notes-title {
font-size: 1.45rem;
}
}
</style>