⬆️ Upgrade tailwindcss to v4
This commit is contained in:
@@ -5,10 +5,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import "@mdi/font/css/materialdesignicons.css"
|
||||
|
||||
import "~/assets/css/tailwind.css"
|
||||
|
||||
onMounted(() => {
|
||||
const userStore = useUserStore()
|
||||
userStore.fetchUser()
|
||||
|
22
app/assets/css/main.css
Normal file
22
app/assets/css/main.css
Normal file
@@ -0,0 +1,22 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--font-family: "Nunito Variable", "Helvatica", sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@import "@fontsource-variable/nunito";
|
||||
@import "@mdi/font/css/materialdesignicons.css";
|
||||
|
||||
@layer theme, base, components, utilities;
|
||||
@import "tailwindcss/theme.css" layer(theme);
|
||||
@import "tailwindcss/utilities.css" layer(utilities);
|
||||
|
||||
html,
|
||||
body {
|
||||
--font-family: "Nunito Variable", sans-serif;
|
||||
font-family: var(--font-family);
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
@import "@fontsource-variable/nunito";
|
||||
|
||||
html, body {
|
||||
--font-family: 'Nunito Variable', sans-serif;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
@@ -1,9 +1,13 @@
|
||||
<template>
|
||||
<div v-if="itemType == 'image'" class="relative rounded-md overflow-hidden" :style="containerStyle">
|
||||
<div
|
||||
v-if="itemType == 'image'"
|
||||
class="relative rounded-md overflow-hidden"
|
||||
:style="`width: 100%; max-height: 800px; aspect-ratio: ${aspectRatio}`"
|
||||
>
|
||||
<!-- Blurhash placeholder -->
|
||||
<div
|
||||
v-if="blurhash"
|
||||
class="absolute inset-0"
|
||||
class="absolute inset-0 z-[-1]"
|
||||
:style="blurhashContainerStyle"
|
||||
>
|
||||
<canvas
|
||||
@@ -16,7 +20,7 @@
|
||||
<!-- Main image -->
|
||||
<img
|
||||
:src="remoteSource"
|
||||
class="w-full h-auto rounded-md"
|
||||
class="w-full h-auto rounded-md transition-opacity duration-500"
|
||||
:class="{ 'opacity-0': !imageLoaded && blurhash }"
|
||||
@load="imageLoaded = true"
|
||||
@error="imageLoaded = true"
|
||||
@@ -37,54 +41,23 @@ const itemType = computed(() => props.item.mimeType.split("/")[0] ?? "unknown")
|
||||
const blurhash = computed(() => props.item.fileMeta?.blur)
|
||||
const imageWidth = computed(() => props.item.fileMeta?.width)
|
||||
const imageHeight = computed(() => props.item.fileMeta?.height)
|
||||
const aspectRatio = computed(() => props.item.fileMeta?.ratio ?? (imageWidth.value && imageHeight.value ? imageHeight.value / imageWidth.value : 1))
|
||||
const aspectRatio = computed(
|
||||
() =>
|
||||
props.item.fileMeta?.ratio ??
|
||||
(imageWidth.value && imageHeight.value
|
||||
? imageHeight.value / imageWidth.value
|
||||
: 1)
|
||||
)
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
const blurCanvas = ref<HTMLCanvasElement | null>(null)
|
||||
|
||||
const apiBase = useSolarNetworkUrl()
|
||||
const remoteSource = computed(
|
||||
() => `${apiBase}/drive/files/${props.item.id}?original=true`
|
||||
)
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
if (imageWidth.value && imageHeight.value) {
|
||||
const maxWidth = 640 // Cap maximum width
|
||||
const maxHeight = 800 // Cap maximum height
|
||||
|
||||
let width = imageWidth.value
|
||||
let height = imageHeight.value
|
||||
|
||||
// Scale down if width exceeds max
|
||||
if (width > maxWidth) {
|
||||
const ratio = maxWidth / width
|
||||
width = maxWidth
|
||||
height = height * ratio
|
||||
}
|
||||
|
||||
// Scale down if height exceeds max
|
||||
if (height > maxHeight) {
|
||||
const ratio = maxHeight / height
|
||||
height = maxHeight
|
||||
width = width * ratio
|
||||
}
|
||||
|
||||
return {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
'max-width': '100%',
|
||||
'max-height': '100%'
|
||||
}
|
||||
}
|
||||
return {
|
||||
'max-width': '800px',
|
||||
'max-height': '600px'
|
||||
}
|
||||
})
|
||||
const remoteSource = computed(() => `${apiBase}/drive/files/${props.item.id}`)
|
||||
|
||||
const blurhashContainerStyle = computed(() => {
|
||||
return {
|
||||
'padding-bottom': `${aspectRatio.value * 100}%`
|
||||
"padding-bottom": `${aspectRatio.value * 100}%`
|
||||
}
|
||||
})
|
||||
|
||||
|
@@ -324,7 +324,7 @@ function getFactorName(factorType: number) {
|
||||
variant="text"
|
||||
class="text-capitalize"
|
||||
color="primary"
|
||||
>Forgot email?</v-btn
|
||||
>Forgot password?</v-btn
|
||||
>
|
||||
|
||||
<div class="d-flex justify-end">
|
||||
|
96
app/pages/posts/[id].vue
Normal file
96
app/pages/posts/[id].vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<div v-if="loading" class="text-center py-8">
|
||||
<v-progress-circular indeterminate size="64" />
|
||||
<p class="mt-4">Loading post...</p>
|
||||
</div>
|
||||
<div v-else-if="error" class="text-center py-8">
|
||||
<v-alert type="error" class="mb-4">
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
</div>
|
||||
<div v-else-if="post">
|
||||
<v-card class="mb-4">
|
||||
<v-card-text>
|
||||
<div class="flex flex-col gap-3">
|
||||
<post-header :item="post" />
|
||||
|
||||
<div v-if="post.title || post.description">
|
||||
<h1 v-if="post.title" class="text-2xl font-bold">
|
||||
{{ post.title }}
|
||||
</h1>
|
||||
<p v-if="post.description" class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ post.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<article
|
||||
v-if="htmlContent"
|
||||
class="prose prose-lg dark:prose-invert prose-slate prose-p:m-0 max-w-none"
|
||||
>
|
||||
<div v-html="htmlContent" />
|
||||
</article>
|
||||
|
||||
<div v-if="post.attachments && post.attachments.length > 0" class="d-flex gap-2 flex-wrap mt-4">
|
||||
<attachment-item
|
||||
v-for="attachment in post.attachments"
|
||||
:key="attachment.id"
|
||||
:item="attachment"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="post.tags && post.tags.length > 0" class="mt-4">
|
||||
<v-chip
|
||||
v-for="tag in post.tags"
|
||||
:key="tag"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
class="mr-2 mb-2"
|
||||
>
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue"
|
||||
import { Marked } from "marked"
|
||||
import type { SnPost } from "~/types/api"
|
||||
|
||||
import PostHeader from "~/components/PostHeader.vue"
|
||||
import AttachmentItem from "~/components/AttachmentItem.vue"
|
||||
|
||||
const route = useRoute()
|
||||
const id = route.params.id as string
|
||||
|
||||
const post = ref<SnPost | null>(null)
|
||||
const loading = ref(true)
|
||||
const error = ref("")
|
||||
const htmlContent = ref("")
|
||||
|
||||
const marked = new Marked()
|
||||
|
||||
async function fetchPost() {
|
||||
try {
|
||||
const api = useSolarNetwork()
|
||||
const resp = await api(`/sphere/posts/${id}`)
|
||||
post.value = resp as SnPost
|
||||
if (post.value.content) {
|
||||
htmlContent.value = await marked.parse(post.value.content, { breaks: true })
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "Failed to load post"
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchPost()
|
||||
})
|
||||
</script>
|
Reference in New Issue
Block a user