:sparklesS: Post reaction
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<nuxt-loading-indicator />
|
||||||
<nuxt-layout>
|
<nuxt-layout>
|
||||||
<nuxt-page />
|
<nuxt-page />
|
||||||
</nuxt-layout>
|
</nuxt-layout>
|
||||||
|
@@ -18,4 +18,15 @@
|
|||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
|
background-color: rgba(var(--v-theme-background), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-enter-active,
|
||||||
|
.page-leave-active {
|
||||||
|
transition: all 0.4s;
|
||||||
|
}
|
||||||
|
.page-enter-from,
|
||||||
|
.page-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(1rem);
|
||||||
}
|
}
|
@@ -1,9 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="relative rounded-md overflow-hidden" :style="containerStyle">
|
||||||
v-if="itemType == 'image'"
|
<template v-if="itemType == 'image'">
|
||||||
class="relative rounded-md overflow-hidden"
|
|
||||||
:style="`width: 100%; max-height: 800px; aspect-ratio: ${aspectRatio}`"
|
|
||||||
>
|
|
||||||
<!-- Blurhash placeholder -->
|
<!-- Blurhash placeholder -->
|
||||||
<div
|
<div
|
||||||
v-if="blurhash"
|
v-if="blurhash"
|
||||||
@@ -20,14 +17,26 @@
|
|||||||
<!-- Main image -->
|
<!-- Main image -->
|
||||||
<img
|
<img
|
||||||
:src="remoteSource"
|
:src="remoteSource"
|
||||||
class="w-full h-auto rounded-md transition-opacity duration-500"
|
class="w-full h-auto rounded-md transition-opacity duration-500 object-cover cursor-pointer"
|
||||||
:class="{ 'opacity-0': !imageLoaded && blurhash }"
|
:class="{ 'opacity-0': !imageLoaded && blurhash }"
|
||||||
@load="imageLoaded = true"
|
@load="imageLoaded = true"
|
||||||
@error="imageLoaded = true"
|
@error="imageLoaded = true"
|
||||||
|
@click="openExternally"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<audio
|
||||||
|
v-else-if="itemType == 'audio'"
|
||||||
|
class="w-full h-auto"
|
||||||
|
:src="remoteSource"
|
||||||
|
controls
|
||||||
|
/>
|
||||||
|
<video
|
||||||
|
v-else-if="itemType == 'video'"
|
||||||
|
class="w-full h-auto"
|
||||||
|
:src="remoteSource"
|
||||||
|
controls
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<audio v-else-if="itemType == 'audio'" :src="remoteSource" controls />
|
|
||||||
<video v-else-if="itemType == 'video'" :src="remoteSource" controls />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@@ -35,7 +44,7 @@ import { computed, ref, onMounted, watch } from "vue"
|
|||||||
import { decode } from "blurhash"
|
import { decode } from "blurhash"
|
||||||
import type { SnAttachment } from "~/types/api"
|
import type { SnAttachment } from "~/types/api"
|
||||||
|
|
||||||
const props = defineProps<{ item: SnAttachment }>()
|
const props = defineProps<{ item: SnAttachment; maxHeight?: string }>()
|
||||||
|
|
||||||
const itemType = computed(() => props.item.mimeType.split("/")[0] ?? "unknown")
|
const itemType = computed(() => props.item.mimeType.split("/")[0] ?? "unknown")
|
||||||
const blurhash = computed(() => props.item.fileMeta?.blur)
|
const blurhash = computed(() => props.item.fileMeta?.blur)
|
||||||
@@ -50,6 +59,10 @@ const aspectRatio = computed(
|
|||||||
)
|
)
|
||||||
const imageLoaded = ref(false)
|
const imageLoaded = ref(false)
|
||||||
|
|
||||||
|
function openExternally() {
|
||||||
|
window.open(remoteSource.value + "?original=true", "_blank")
|
||||||
|
}
|
||||||
|
|
||||||
const blurCanvas = ref<HTMLCanvasElement | null>(null)
|
const blurCanvas = ref<HTMLCanvasElement | null>(null)
|
||||||
|
|
||||||
const apiBase = useSolarNetworkUrl()
|
const apiBase = useSolarNetworkUrl()
|
||||||
@@ -61,6 +74,13 @@ const blurhashContainerStyle = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const containerStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
"max-height": props.maxHeight ?? "720px",
|
||||||
|
"aspect-ratio": aspectRatio.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const decodeBlurhash = () => {
|
const decodeBlurhash = () => {
|
||||||
if (!blurhash.value || !blurCanvas.value) return
|
if (!blurhash.value || !blurCanvas.value) return
|
||||||
|
|
||||||
|
@@ -15,18 +15,29 @@
|
|||||||
|
|
||||||
<article
|
<article
|
||||||
v-if="htmlContent"
|
v-if="htmlContent"
|
||||||
class="prose prose-sm dark:prose-invert prose-slate prose-p:m-0"
|
class="prose prose-sm dark:prose-invert prose-slate prose-p:m-0 max-w-none"
|
||||||
>
|
>
|
||||||
<div v-html="htmlContent" />
|
<div v-html="htmlContent" />
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<div v-if="props.item.attachments" class="d-flex gap-2 flex-wrap">
|
<div v-if="props.item.attachments.length > 0" class="d-flex gap-2 flex-wrap" @click.stop>
|
||||||
<attachment-item
|
<attachment-item
|
||||||
v-for="attachment in props.item.attachments"
|
v-for="attachment in props.item.attachments"
|
||||||
:key="attachment.id"
|
:key="attachment.id"
|
||||||
:item="attachment"
|
:item="attachment"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Post Reactions -->
|
||||||
|
<div @click.stop>
|
||||||
|
<post-reaction-list
|
||||||
|
:parent-id="props.item.id"
|
||||||
|
:reactions="(props.item as any).reactions || {}"
|
||||||
|
:reactions-made="(props.item as any).reactionsMade || {}"
|
||||||
|
:can-react="true"
|
||||||
|
@react="handleReaction"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -39,6 +50,7 @@ import type { SnPost } from "~/types/api"
|
|||||||
|
|
||||||
import PostHeader from "./PostHeader.vue"
|
import PostHeader from "./PostHeader.vue"
|
||||||
import AttachmentItem from "./AttachmentItem.vue"
|
import AttachmentItem from "./AttachmentItem.vue"
|
||||||
|
import PostReactionList from "./PostReactionList.vue"
|
||||||
|
|
||||||
const props = defineProps<{ item: SnPost }>()
|
const props = defineProps<{ item: SnPost }>()
|
||||||
|
|
||||||
@@ -46,6 +58,33 @@ const marked = new Marked()
|
|||||||
|
|
||||||
const htmlContent = ref<string>("")
|
const htmlContent = ref<string>("")
|
||||||
|
|
||||||
|
function handleReaction(symbol: string, attitude: number, delta: number) {
|
||||||
|
// Update the local item data
|
||||||
|
if (!props.item) return
|
||||||
|
|
||||||
|
const reactions = (props.item as any).reactions || {}
|
||||||
|
const currentCount = reactions[symbol] || 0
|
||||||
|
const newCount = Math.max(0, currentCount + delta)
|
||||||
|
|
||||||
|
if (newCount === 0) {
|
||||||
|
delete reactions[symbol]
|
||||||
|
} else {
|
||||||
|
reactions[symbol] = newCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the reactionsMade status
|
||||||
|
const reactionsMade = (props.item as any).reactionsMade || {}
|
||||||
|
if (delta > 0) {
|
||||||
|
reactionsMade[symbol] = true
|
||||||
|
} else {
|
||||||
|
delete reactionsMade[symbol]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the item object (this will trigger reactivity)
|
||||||
|
;(props.item as any).reactions = { ...reactions }
|
||||||
|
;(props.item as any).reactionsMade = { ...reactionsMade }
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
props.item,
|
props.item,
|
||||||
async (value) => {
|
async (value) => {
|
||||||
|
341
app/components/PostReactionList.vue
Normal file
341
app/components/PostReactionList.vue
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
<template>
|
||||||
|
<v-chip-group class="d-flex flex-wrap gap-2">
|
||||||
|
<!-- Add Reaction Button -->
|
||||||
|
<v-chip
|
||||||
|
v-if="canReact"
|
||||||
|
color="primary"
|
||||||
|
rounded
|
||||||
|
:disabled="submitting"
|
||||||
|
@click="showReactionDialog"
|
||||||
|
>
|
||||||
|
<v-icon start size="16">mdi-plus</v-icon>
|
||||||
|
<span class="text-caption">React</span>
|
||||||
|
</v-chip>
|
||||||
|
|
||||||
|
<!-- Existing Reactions -->
|
||||||
|
<v-chip
|
||||||
|
v-for="(count, symbol) in reactions"
|
||||||
|
rounded
|
||||||
|
:key="symbol"
|
||||||
|
:color="getReactionColor(symbol)"
|
||||||
|
:disabled="submitting"
|
||||||
|
@click="reactToPost(symbol)"
|
||||||
|
>
|
||||||
|
<span class="reaction-emoji">{{ getReactionEmoji(symbol) }}</span>
|
||||||
|
<span class="reaction-symbol">{{ symbol }}</span>
|
||||||
|
<v-chip size="x-small" variant="flat" class="reaction-count ms-1">
|
||||||
|
{{ count }}
|
||||||
|
</v-chip>
|
||||||
|
</v-chip>
|
||||||
|
</v-chip-group>
|
||||||
|
|
||||||
|
<!-- Reaction Selection Dialog -->
|
||||||
|
<v-dialog v-model="reactionDialog" max-width="500" height="600">
|
||||||
|
<v-card prepend-icon="mdi-emoticon-outline" title="React Post">
|
||||||
|
<!-- Dialog Content -->
|
||||||
|
<div class="dialog-content">
|
||||||
|
<!-- Positive Reactions -->
|
||||||
|
<div class="reaction-section">
|
||||||
|
<div class="section-header d-flex align-center px-6 py-3">
|
||||||
|
<v-icon class="me-2">mdi-emoticon-happy</v-icon>
|
||||||
|
<span class="text-subtitle-1 font-weight-bold">Positive</span>
|
||||||
|
</div>
|
||||||
|
<div class="reaction-grid">
|
||||||
|
<v-card
|
||||||
|
v-for="reaction in getReactionsByAttitude(0)"
|
||||||
|
:key="reaction.symbol"
|
||||||
|
class="reaction-card mx-2"
|
||||||
|
:class="{ selected: isReactionMade(reaction.symbol) }"
|
||||||
|
:disabled="submitting"
|
||||||
|
@click="selectReaction(reaction.symbol)"
|
||||||
|
>
|
||||||
|
<div class="d-flex flex-column align-center justify-center pa-3">
|
||||||
|
<span class="text-h4 mb-1">{{ reaction.emoji }}</span>
|
||||||
|
<span class="text-caption text-center mb-1">{{
|
||||||
|
reaction.symbol
|
||||||
|
}}</span>
|
||||||
|
<span
|
||||||
|
v-if="getReactionCount(reaction.symbol) > 0"
|
||||||
|
class="text-caption font-weight-bold"
|
||||||
|
>
|
||||||
|
x{{ getReactionCount(reaction.symbol) }}
|
||||||
|
</span>
|
||||||
|
<div v-else class="spacer"></div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Neutral Reactions -->
|
||||||
|
<div class="reaction-section">
|
||||||
|
<div class="section-header d-flex align-center px-6 py-3">
|
||||||
|
<v-icon class="me-2">mdi-emoticon-neutral</v-icon>
|
||||||
|
<span class="text-subtitle-1 font-weight-bold">Neutral</span>
|
||||||
|
</div>
|
||||||
|
<div class="reaction-grid">
|
||||||
|
<v-card
|
||||||
|
v-for="reaction in getReactionsByAttitude(1)"
|
||||||
|
:key="reaction.symbol"
|
||||||
|
class="reaction-card mx-2"
|
||||||
|
:class="{ selected: isReactionMade(reaction.symbol) }"
|
||||||
|
:disabled="submitting"
|
||||||
|
@click="selectReaction(reaction.symbol)"
|
||||||
|
>
|
||||||
|
<div class="d-flex flex-column align-center justify-center pa-3">
|
||||||
|
<span class="text-h4 mb-1">{{ reaction.emoji }}</span>
|
||||||
|
<span class="text-caption text-center mb-1">{{
|
||||||
|
reaction.symbol
|
||||||
|
}}</span>
|
||||||
|
<span
|
||||||
|
v-if="getReactionCount(reaction.symbol) > 0"
|
||||||
|
class="text-caption font-weight-bold"
|
||||||
|
>
|
||||||
|
x{{ getReactionCount(reaction.symbol) }}
|
||||||
|
</span>
|
||||||
|
<div v-else class="spacer"></div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Negative Reactions -->
|
||||||
|
<div class="reaction-section">
|
||||||
|
<div class="section-header d-flex align-center px-6 py-3">
|
||||||
|
<v-icon class="me-2">mdi-emoticon-sad</v-icon>
|
||||||
|
<span class="text-subtitle-1 font-weight-bold">Negative</span>
|
||||||
|
</div>
|
||||||
|
<div class="reaction-grid">
|
||||||
|
<v-card
|
||||||
|
v-for="reaction in getReactionsByAttitude(2)"
|
||||||
|
:key="reaction.symbol"
|
||||||
|
class="reaction-card mx-2"
|
||||||
|
:class="{ selected: isReactionMade(reaction.symbol) }"
|
||||||
|
:disabled="submitting"
|
||||||
|
@click="selectReaction(reaction.symbol)"
|
||||||
|
>
|
||||||
|
<div class="d-flex flex-column align-center justify-center pa-3">
|
||||||
|
<span class="text-h4 mb-1">{{ reaction.emoji }}</span>
|
||||||
|
<span class="text-caption text-center mb-1">{{
|
||||||
|
reaction.symbol
|
||||||
|
}}</span>
|
||||||
|
<span
|
||||||
|
v-if="getReactionCount(reaction.symbol) > 0"
|
||||||
|
class="text-caption font-weight-bold"
|
||||||
|
>
|
||||||
|
x{{ getReactionCount(reaction.symbol) }}
|
||||||
|
</span>
|
||||||
|
<div v-else class="spacer"></div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from "vue"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
parentId: string
|
||||||
|
reactions?: Record<string, number>
|
||||||
|
reactionsMade?: Record<string, boolean>
|
||||||
|
canReact?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReactionTemplate {
|
||||||
|
symbol: string
|
||||||
|
emoji: string
|
||||||
|
attitude: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
reactions: () => ({}),
|
||||||
|
reactionsMade: () => ({}),
|
||||||
|
canReact: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
react: [symbol: string, attitude: number, delta: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const submitting = ref(false)
|
||||||
|
const reactionDialog = ref(false)
|
||||||
|
|
||||||
|
// Available reaction templates
|
||||||
|
const availableReactions: ReactionTemplate[] = [
|
||||||
|
{ symbol: "like", emoji: "👍", attitude: 1 },
|
||||||
|
{ symbol: "love", emoji: "❤️", attitude: 2 },
|
||||||
|
{ symbol: "laugh", emoji: "😂", attitude: 1 },
|
||||||
|
{ symbol: "wow", emoji: "😮", attitude: 1 },
|
||||||
|
{ symbol: "sad", emoji: "😢", attitude: -1 },
|
||||||
|
{ symbol: "angry", emoji: "😠", attitude: -2 },
|
||||||
|
{ symbol: "fire", emoji: "🔥", attitude: 2 },
|
||||||
|
{ symbol: "clap", emoji: "👏", attitude: 1 },
|
||||||
|
{ symbol: "think", emoji: "🤔", attitude: 0 },
|
||||||
|
{ symbol: "pray", emoji: "🙏", attitude: 1 },
|
||||||
|
{ symbol: "celebrate", emoji: "🎉", attitude: 2 },
|
||||||
|
{ symbol: "heart", emoji: "💖", attitude: 2 }
|
||||||
|
]
|
||||||
|
|
||||||
|
function getReactionEmoji(symbol: string): string {
|
||||||
|
const reaction = availableReactions.find((r) => r.symbol === symbol)
|
||||||
|
return reaction?.emoji || "❓"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReactionColor(symbol: string): string {
|
||||||
|
const attitude =
|
||||||
|
availableReactions.find((r) => r.symbol === symbol)?.attitude || 0
|
||||||
|
if (attitude > 0) return "success"
|
||||||
|
if (attitude < 0) return "error"
|
||||||
|
return "primary"
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reactToPost(symbol: string) {
|
||||||
|
if (submitting.value) return
|
||||||
|
|
||||||
|
const reaction = availableReactions.find((r) => r.symbol === symbol)
|
||||||
|
if (!reaction) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
submitting.value = true
|
||||||
|
const api = useSolarNetwork()
|
||||||
|
const response = await api(`/sphere/posts/${props.parentId}/reactions`, {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
symbol: symbol,
|
||||||
|
attitude: reaction.attitude
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check if we're removing the reaction (204 status) or adding (200)
|
||||||
|
// In Nuxt, we can check the response status through the fetch response
|
||||||
|
const isRemoving =
|
||||||
|
response && typeof response === "object" && "status" in response
|
||||||
|
? (response as any).status === 204
|
||||||
|
: false
|
||||||
|
const delta = isRemoving ? -1 : 1
|
||||||
|
|
||||||
|
emit("react", symbol, reaction.attitude, delta)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to react to post:", error)
|
||||||
|
// You might want to show a toast notification here
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showReactionDialog() {
|
||||||
|
reactionDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectReaction(symbol: string) {
|
||||||
|
reactionDialog.value = false
|
||||||
|
reactToPost(symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computed properties and helper functions
|
||||||
|
const totalReactionsCount = computed(() => {
|
||||||
|
return Object.values(props.reactions || {}).reduce(
|
||||||
|
(sum, count) => sum + count,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function getReactionsByAttitude(attitude: number): ReactionTemplate[] {
|
||||||
|
return availableReactions.filter((reaction) => reaction.attitude === attitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isReactionMade(symbol: string): boolean {
|
||||||
|
return (props.reactionsMade || {})[symbol] || false
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReactionCount(symbol: string): number {
|
||||||
|
return (props.reactions || {})[symbol] || 0
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.post-reaction-list {
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-chip {
|
||||||
|
height: 28px !important;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-emoji {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-symbol {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-count {
|
||||||
|
height: 16px !important;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dialog Styles */
|
||||||
|
.reaction-dialog {
|
||||||
|
height: 600px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: calc(600px - 80px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
background-color: rgba(var(--v-theme-surface-variant), 0.5);
|
||||||
|
border-bottom: 1px solid rgb(var(--v-theme-outline-variant));
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-grid {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-grid::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-card {
|
||||||
|
min-width: 80px;
|
||||||
|
height: 100px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-card:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-card.selected {
|
||||||
|
border-color: rgb(var(--v-theme-primary));
|
||||||
|
background-color: rgb(var(--v-theme-primary-container));
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -156,7 +156,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<v-card v-if="htmlBio" title="Bio" prepend-icon="mdi-pencil">
|
<v-card v-if="htmlBio" title="Bio" prepend-icon="mdi-pencil">
|
||||||
<v-card-text class="px-8">
|
<v-card-text>
|
||||||
<article
|
<article
|
||||||
class="bio-prose prose prose-sm dark:prose-invert prose-slate"
|
class="bio-prose prose prose-sm dark:prose-invert prose-slate"
|
||||||
v-html="htmlBio"
|
v-html="htmlBio"
|
||||||
|
@@ -1,58 +1,206 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container class="py-6">
|
||||||
<div v-if="loading" class="text-center py-8">
|
<div v-if="loading" class="text-center py-12">
|
||||||
<v-progress-circular indeterminate size="64" />
|
<v-progress-circular indeterminate size="64" color="primary" />
|
||||||
<p class="mt-4">Loading post...</p>
|
<p class="mt-4">Loading post...</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="error" class="text-center py-8">
|
<div v-else-if="error" class="text-center py-12">
|
||||||
<v-alert type="error" class="mb-4">
|
<v-alert type="error" class="mb-4" prominent>
|
||||||
|
<v-alert-title>Error Loading Post</v-alert-title>
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
<v-btn color="primary" @click="fetchPost">Try Again</v-btn>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="post">
|
<div v-else-if="post" class="max-w-4xl mx-auto">
|
||||||
<v-card class="mb-4">
|
<!-- Article Type: Split Header and Content -->
|
||||||
<v-card-text>
|
<template v-if="post.type === 1">
|
||||||
<div class="flex flex-col gap-3">
|
<!-- Post Header Section (Article) -->
|
||||||
<post-header :item="post" />
|
<v-card class="mb-4 elevation-2" rounded="lg">
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<post-header :item="post" class="mb-4" />
|
||||||
|
|
||||||
<div v-if="post.title || post.description">
|
<!-- Post Title and Description -->
|
||||||
<h1 v-if="post.title" class="text-2xl font-bold">
|
<div v-if="post.title || post.description" class="mb-4">
|
||||||
|
<h1
|
||||||
|
v-if="post.title"
|
||||||
|
class="text-3xl font-bold mb-3 leading-tight"
|
||||||
|
>
|
||||||
{{ post.title }}
|
{{ post.title }}
|
||||||
</h1>
|
</h1>
|
||||||
<p v-if="post.description" class="text-sm text-gray-600 dark:text-gray-400">
|
<p
|
||||||
|
v-if="post.description"
|
||||||
|
class="text-lg text-medium-emphasis leading-relaxed"
|
||||||
|
>
|
||||||
{{ post.description }}
|
{{ post.description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Post Metadata -->
|
||||||
|
<div class="flex items-center gap-4 text-sm text-medium-emphasis">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<v-icon size="16">mdi-calendar</v-icon>
|
||||||
|
<span>{{ formatDate(post.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="post.updatedAt && post.updatedAt !== post.createdAt"
|
||||||
|
class="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<v-icon size="16">mdi-pencil</v-icon>
|
||||||
|
<span>Updated {{ formatDate(post.updatedAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="(post as any).viewCount || (post as any).view_count"
|
||||||
|
class="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<v-icon size="16">mdi-eye</v-icon>
|
||||||
|
<span
|
||||||
|
>{{
|
||||||
|
(post as any).viewCount || (post as any).view_count || 0
|
||||||
|
}}
|
||||||
|
views</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Merged Content and Attachments Section (Article) -->
|
||||||
|
<v-card class="mb-4 elevation-1" rounded="lg">
|
||||||
|
<v-card-text class="pa-8">
|
||||||
<article
|
<article
|
||||||
v-if="htmlContent"
|
v-if="htmlContent"
|
||||||
class="prose prose-lg dark:prose-invert prose-slate prose-p:m-0 max-w-none"
|
class="prose prose-xl dark:prose-invert prose-slate max-w-none mb-8"
|
||||||
>
|
>
|
||||||
<div v-html="htmlContent" />
|
<div v-html="htmlContent" />
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<div v-if="post.attachments && post.attachments.length > 0" class="d-flex gap-2 flex-wrap mt-4">
|
<!-- Attachments within Content Section -->
|
||||||
|
<div v-if="post.attachments && post.attachments.length > 0">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<attachment-item
|
<attachment-item
|
||||||
v-for="attachment in post.attachments"
|
v-for="attachment in post.attachments"
|
||||||
:key="attachment.id"
|
:key="attachment.id"
|
||||||
:item="attachment"
|
:item="attachment"
|
||||||
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div v-if="post.tags && post.tags.length > 0" class="mt-4">
|
<!-- Other Types: Merged Header, Content, and Attachments -->
|
||||||
|
<template v-else>
|
||||||
|
<!-- Merged Header, Content, and Attachments Section -->
|
||||||
|
<v-card class="mb-4 elevation-1" rounded="lg">
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<post-header :item="post" class="mb-4" />
|
||||||
|
|
||||||
|
<!-- Post Title and Description -->
|
||||||
|
<div v-if="post.title || post.description" class="mb-4">
|
||||||
|
<h1
|
||||||
|
v-if="post.title"
|
||||||
|
class="text-3xl font-bold mb-3 leading-tight"
|
||||||
|
>
|
||||||
|
{{ post.title }}
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
v-if="post.description"
|
||||||
|
class="text-lg text-medium-emphasis leading-relaxed"
|
||||||
|
>
|
||||||
|
{{ post.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Post Metadata -->
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-4 text-sm text-medium-emphasis mb-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<v-icon size="16">mdi-calendar</v-icon>
|
||||||
|
<span>{{ formatDate(post.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="post.updatedAt && post.updatedAt !== post.createdAt"
|
||||||
|
class="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<v-icon size="16">mdi-pencil</v-icon>
|
||||||
|
<span>Updated {{ formatDate(post.updatedAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="(post as any).viewCount || (post as any).view_count"
|
||||||
|
class="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<v-icon size="16">mdi-eye</v-icon>
|
||||||
|
<span
|
||||||
|
>{{
|
||||||
|
(post as any).viewCount || (post as any).view_count || 0
|
||||||
|
}}
|
||||||
|
views</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<article
|
||||||
|
v-if="htmlContent"
|
||||||
|
class="prose prose-xl dark:prose-invert prose-slate max-w-none mb-8"
|
||||||
|
>
|
||||||
|
<div v-html="htmlContent" />
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- Attachments within Merged Section -->
|
||||||
|
<div v-if="post.attachments && post.attachments.length > 0">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<attachment-item
|
||||||
|
v-for="attachment in post.attachments"
|
||||||
|
:key="attachment.id"
|
||||||
|
:item="attachment"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Tags Section -->
|
||||||
|
<v-card
|
||||||
|
v-if="post.tags && post.tags.length > 0"
|
||||||
|
class="mb-4 elevation-1"
|
||||||
|
rounded="lg"
|
||||||
|
>
|
||||||
|
<v-card-title class="text-h6">
|
||||||
|
<v-icon class="mr-2">mdi-tag-multiple</v-icon>
|
||||||
|
Tags
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
<v-chip
|
<v-chip
|
||||||
v-for="tag in post.tags"
|
v-for="tag in post.tags"
|
||||||
:key="tag"
|
:key="tag"
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
class="mr-2 mb-2"
|
color="primary"
|
||||||
|
class="cursor-pointer hover:bg-primary hover:text-primary-foreground transition-colors"
|
||||||
>
|
>
|
||||||
|
<v-icon start size="16">mdi-tag</v-icon>
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Post Reactions -->
|
||||||
|
<div>
|
||||||
|
<post-reaction-list
|
||||||
|
can-react
|
||||||
|
:parent-id="id"
|
||||||
|
:reactions="(post as any).reactions || {}"
|
||||||
|
:reactions-made="(post as any).reactionsMade || {}"
|
||||||
|
@react="handleReaction"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
@@ -64,47 +212,57 @@ import type { SnPost } from "~/types/api"
|
|||||||
|
|
||||||
import PostHeader from "~/components/PostHeader.vue"
|
import PostHeader from "~/components/PostHeader.vue"
|
||||||
import AttachmentItem from "~/components/AttachmentItem.vue"
|
import AttachmentItem from "~/components/AttachmentItem.vue"
|
||||||
|
import PostReactionList from "~/components/PostReactionList.vue"
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const id = route.params.id as string
|
const id = route.params.id as string
|
||||||
|
|
||||||
useHead({
|
|
||||||
title: computed(() => {
|
|
||||||
if (loading.value) return 'Loading post...'
|
|
||||||
if (error.value) return 'Error'
|
|
||||||
if (!post.value) return 'Post not found'
|
|
||||||
return post.value.title || 'Post'
|
|
||||||
}),
|
|
||||||
meta: computed(() => {
|
|
||||||
if (post.value) {
|
|
||||||
const description = post.value.description || post.value.content?.substring(0, 150) || ''
|
|
||||||
return [
|
|
||||||
{ name: 'description', content: description },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
defineOgImage({
|
|
||||||
title: computed(() => post.value?.title || 'Post'),
|
|
||||||
description: computed(() => post.value?.description || post.value?.content?.substring(0, 150) || ''),
|
|
||||||
})
|
|
||||||
|
|
||||||
const post = ref<SnPost | null>(null)
|
const post = ref<SnPost | null>(null)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const error = ref("")
|
const error = ref("")
|
||||||
const htmlContent = ref("")
|
const htmlContent = ref("")
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: computed(() => {
|
||||||
|
if (loading.value) return "Loading post..."
|
||||||
|
if (error.value) return "Error"
|
||||||
|
if (!post.value) return "Post not found"
|
||||||
|
return post.value.title || "Post"
|
||||||
|
}),
|
||||||
|
meta: computed(() => {
|
||||||
|
if (post.value) {
|
||||||
|
const description =
|
||||||
|
post.value.description || post.value.content?.substring(0, 150) || ""
|
||||||
|
return [{ name: "description", content: description }]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// defineOgImage({
|
||||||
|
// title: computed(() => post.value?.title || 'Post'),
|
||||||
|
// description: computed(() => post.value?.description || post.value?.content?.substring(0, 150) || ''),
|
||||||
|
// })
|
||||||
|
|
||||||
const marked = new Marked()
|
const marked = new Marked()
|
||||||
|
|
||||||
|
function formatDate(dateString: string): string {
|
||||||
|
return new Date(dateString).toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchPost() {
|
async function fetchPost() {
|
||||||
try {
|
try {
|
||||||
const api = useSolarNetwork()
|
const api = useSolarNetwork()
|
||||||
const resp = await api(`/sphere/posts/${id}`)
|
const resp = await api(`/sphere/posts/${id}`)
|
||||||
post.value = resp as SnPost
|
post.value = resp as SnPost
|
||||||
if (post.value.content) {
|
if (post.value.content) {
|
||||||
htmlContent.value = await marked.parse(post.value.content, { breaks: true })
|
htmlContent.value = await marked.parse(post.value.content, {
|
||||||
|
breaks: true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : "Failed to load post"
|
error.value = e instanceof Error ? e.message : "Failed to load post"
|
||||||
@@ -113,6 +271,33 @@ async function fetchPost() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleReaction(symbol: string, attitude: number, delta: number) {
|
||||||
|
if (!post.value) return
|
||||||
|
|
||||||
|
// Update the reactions count
|
||||||
|
const reactions = (post.value as any).reactions || {}
|
||||||
|
const currentCount = reactions[symbol] || 0
|
||||||
|
const newCount = Math.max(0, currentCount + delta)
|
||||||
|
|
||||||
|
if (newCount === 0) {
|
||||||
|
delete reactions[symbol]
|
||||||
|
} else {
|
||||||
|
reactions[symbol] = newCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the reactionsMade status
|
||||||
|
const reactionsMade = (post.value as any).reactionsMade || {}
|
||||||
|
if (delta > 0) {
|
||||||
|
reactionsMade[symbol] = true
|
||||||
|
} else {
|
||||||
|
delete reactionsMade[symbol]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the post object
|
||||||
|
;(post.value as any).reactions = reactions
|
||||||
|
;(post.value as any).reactionsMade = reactionsMade
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchPost()
|
fetchPost()
|
||||||
})
|
})
|
||||||
|
58
bun.lock
58
bun.lock
@@ -19,12 +19,12 @@
|
|||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
"cfturnstile-vue3": "^2.0.0",
|
"cfturnstile-vue3": "^2.0.0",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"fslightbox-vue": "^2.2.1",
|
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"marked": "^16.3.0",
|
"marked": "^16.3.0",
|
||||||
"nuxt": "^4.1.2",
|
"nuxt": "^4.1.2",
|
||||||
"nuxt-og-image": "^5.1.11",
|
"nuxt-og-image": "^5.1.11",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
|
"sharp": "^0.34.4",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
"tus-js-client": "^4.3.1",
|
"tus-js-client": "^4.3.1",
|
||||||
"vue": "^3.5.21",
|
"vue": "^3.5.21",
|
||||||
@@ -206,6 +206,52 @@
|
|||||||
|
|
||||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||||
|
|
||||||
|
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
|
||||||
|
|
||||||
|
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA=="],
|
||||||
|
|
||||||
|
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.3" }, "os": "darwin", "cpu": "x64" }, "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.3", "", { "os": "linux", "cpu": "arm" }, "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.3" }, "os": "linux", "cpu": "arm" }, "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.3" }, "os": "linux", "cpu": "s390x" }, "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A=="],
|
||||||
|
|
||||||
|
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA=="],
|
||||||
|
|
||||||
|
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg=="],
|
||||||
|
|
||||||
|
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.4", "", { "dependencies": { "@emnapi/runtime": "^1.5.0" }, "cpu": "none" }, "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA=="],
|
||||||
|
|
||||||
|
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA=="],
|
||||||
|
|
||||||
|
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw=="],
|
||||||
|
|
||||||
|
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.4", "", { "os": "win32", "cpu": "x64" }, "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig=="],
|
||||||
|
|
||||||
"@intlify/bundle-utils": ["@intlify/bundle-utils@11.0.1", "", { "dependencies": { "@intlify/message-compiler": "^11.1.10", "@intlify/shared": "^11.1.10", "acorn": "^8.8.2", "esbuild": "^0.25.4", "escodegen": "^2.1.0", "estree-walker": "^2.0.2", "jsonc-eslint-parser": "^2.3.0", "source-map-js": "^1.0.2", "yaml-eslint-parser": "^1.2.2" } }, "sha512-5l10G5wE2cQRsZMS9y0oSFMOLW5IG/SgbkIUltqnwF1EMRrRbUAHFiPabXdGTHeexCsMTcxj/1w9i0rzjJU9IQ=="],
|
"@intlify/bundle-utils": ["@intlify/bundle-utils@11.0.1", "", { "dependencies": { "@intlify/message-compiler": "^11.1.10", "@intlify/shared": "^11.1.10", "acorn": "^8.8.2", "esbuild": "^0.25.4", "escodegen": "^2.1.0", "estree-walker": "^2.0.2", "jsonc-eslint-parser": "^2.3.0", "source-map-js": "^1.0.2", "yaml-eslint-parser": "^1.2.2" } }, "sha512-5l10G5wE2cQRsZMS9y0oSFMOLW5IG/SgbkIUltqnwF1EMRrRbUAHFiPabXdGTHeexCsMTcxj/1w9i0rzjJU9IQ=="],
|
||||||
|
|
||||||
"@intlify/core": ["@intlify/core@11.1.12", "", { "dependencies": { "@intlify/core-base": "11.1.12", "@intlify/shared": "11.1.12" } }, "sha512-Uccp4VtalUSk/b4F9nBBs7VGgIh9VnXTSHHQ+Kc0AetsHJLxdi04LfhfSi4dujtsTAWnHMHWZw07UbMm6Umq1g=="],
|
"@intlify/core": ["@intlify/core@11.1.12", "", { "dependencies": { "@intlify/core-base": "11.1.12", "@intlify/shared": "11.1.12" } }, "sha512-Uccp4VtalUSk/b4F9nBBs7VGgIh9VnXTSHHQ+Kc0AetsHJLxdi04LfhfSi4dujtsTAWnHMHWZw07UbMm6Umq1g=="],
|
||||||
@@ -1098,8 +1144,6 @@
|
|||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
"fslightbox-vue": ["fslightbox-vue@2.2.1", "", { "peerDependencies": { "vue": ">=2.5.0" } }, "sha512-GMlp8JoyRxN8dJuIGQCoB2O9CWnxG7uTK4bBzaw+VyXyVUHFA30UPRXSUFFnHuprX1qF+L0f7oimVF/FGSDwgA=="],
|
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
"fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
|
"fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
|
||||||
@@ -1688,7 +1732,7 @@
|
|||||||
|
|
||||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||||
|
|
||||||
"sharp": ["sharp@0.32.6", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.2", "node-addon-api": "^6.1.0", "prebuild-install": "^7.1.1", "semver": "^7.5.4", "simple-get": "^4.0.1", "tar-fs": "^3.0.4", "tunnel-agent": "^0.6.0" } }, "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w=="],
|
"sharp": ["sharp@0.34.4", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.4", "@img/sharp-darwin-x64": "0.34.4", "@img/sharp-libvips-darwin-arm64": "1.2.3", "@img/sharp-libvips-darwin-x64": "1.2.3", "@img/sharp-libvips-linux-arm": "1.2.3", "@img/sharp-libvips-linux-arm64": "1.2.3", "@img/sharp-libvips-linux-ppc64": "1.2.3", "@img/sharp-libvips-linux-s390x": "1.2.3", "@img/sharp-libvips-linux-x64": "1.2.3", "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", "@img/sharp-libvips-linuxmusl-x64": "1.2.3", "@img/sharp-linux-arm": "0.34.4", "@img/sharp-linux-arm64": "0.34.4", "@img/sharp-linux-ppc64": "0.34.4", "@img/sharp-linux-s390x": "0.34.4", "@img/sharp-linux-x64": "0.34.4", "@img/sharp-linuxmusl-arm64": "0.34.4", "@img/sharp-linuxmusl-x64": "0.34.4", "@img/sharp-wasm32": "0.34.4", "@img/sharp-win32-arm64": "0.34.4", "@img/sharp-win32-ia32": "0.34.4", "@img/sharp-win32-x64": "0.34.4" } }, "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA=="],
|
||||||
|
|
||||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
@@ -2142,6 +2186,8 @@
|
|||||||
|
|
||||||
"ipx/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
"ipx/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||||
|
|
||||||
|
"ipx/sharp": ["sharp@0.32.6", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.2", "node-addon-api": "^6.1.0", "prebuild-install": "^7.1.1", "semver": "^7.5.4", "simple-get": "^4.0.1", "tar-fs": "^3.0.4", "tunnel-agent": "^0.6.0" } }, "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w=="],
|
||||||
|
|
||||||
"is-wsl/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
"is-wsl/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||||
|
|
||||||
"jsonc-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"jsonc-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
@@ -2216,8 +2262,6 @@
|
|||||||
|
|
||||||
"rollup-plugin-visualizer/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
"rollup-plugin-visualizer/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
||||||
|
|
||||||
"sharp/node-addon-api": ["node-addon-api@6.1.0", "", {}, "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="],
|
|
||||||
|
|
||||||
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
@@ -2422,6 +2466,8 @@
|
|||||||
|
|
||||||
"importx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.23.1", "", { "os": "win32", "cpu": "x64" }, "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg=="],
|
"importx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.23.1", "", { "os": "win32", "cpu": "x64" }, "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg=="],
|
||||||
|
|
||||||
|
"ipx/sharp/node-addon-api": ["node-addon-api@6.1.0", "", {}, "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="],
|
||||||
|
|
||||||
"lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
"lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||||
|
|
||||||
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||||
|
@@ -15,6 +15,7 @@ export default defineNuxtConfig({
|
|||||||
],
|
],
|
||||||
css: ["~/assets/css/main.css"],
|
css: ["~/assets/css/main.css"],
|
||||||
app: {
|
app: {
|
||||||
|
pageTransition: { name: 'page', mode: 'out-in' },
|
||||||
head: {
|
head: {
|
||||||
titleTemplate: "%s - Solar Network"
|
titleTemplate: "%s - Solar Network"
|
||||||
}
|
}
|
||||||
|
@@ -25,7 +25,6 @@
|
|||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
"cfturnstile-vue3": "^2.0.0",
|
"cfturnstile-vue3": "^2.0.0",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"fslightbox-vue": "^2.2.1",
|
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"marked": "^16.3.0",
|
"marked": "^16.3.0",
|
||||||
"nuxt": "^4.1.2",
|
"nuxt": "^4.1.2",
|
||||||
|
Reference in New Issue
Block a user