✨ Post detail page
This commit is contained in:
7
DysonNetwork.Drive/Client/src/dy-prefetch.d.ts
vendored
Normal file
7
DysonNetwork.Drive/Client/src/dy-prefetch.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export {}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
DyPrefetch?: any
|
||||||
|
}
|
||||||
|
}
|
@@ -183,7 +183,7 @@ async function fetchUser() {
|
|||||||
|
|
||||||
console.log('[Fetch] Using the API to load user data.')
|
console.log('[Fetch] Using the API to load user data.')
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/accounts/${route.params.name}`)
|
const resp = await fetch(`/api/accounts/${route.params.slug}`)
|
||||||
user.value = await resp.json()
|
user.value = await resp.json()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
@@ -43,13 +43,11 @@ public class SubscriptionController(SubscriptionService subscriptions, AfdianPay
|
|||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var subs = await db.WalletSubscriptions
|
var subscription = await db.WalletSubscriptions
|
||||||
.Where(s => s.AccountId == currentUser.Id && s.IsActive)
|
.Where(s => s.AccountId == currentUser.Id && s.IsActive)
|
||||||
.Where(s => EF.Functions.ILike(s.Identifier, prefix + "%"))
|
.Where(s => EF.Functions.ILike(s.Identifier, prefix + "%"))
|
||||||
.OrderByDescending(s => s.BegunAt)
|
.OrderByDescending(s => s.BegunAt)
|
||||||
.ToListAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (subs.Count == 0) return NotFound();
|
|
||||||
var subscription = subs.FirstOrDefault(s => s.IsAvailable);
|
|
||||||
if (subscription is null) return NotFound();
|
if (subscription is null) return NotFound();
|
||||||
|
|
||||||
return Ok(subscription);
|
return Ok(subscription);
|
||||||
|
@@ -400,9 +400,9 @@ public class SubscriptionService(
|
|||||||
.OrderByDescending(s => s.BegunAt)
|
.OrderByDescending(s => s.BegunAt)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
// Cache the result if found (with 30 minutes expiry)
|
// Cache the result if found (with 5 minutes expiry)
|
||||||
if (subscription != null)
|
if (subscription != null)
|
||||||
await cache.SetAsync(cacheKey, subscription, TimeSpan.FromMinutes(30));
|
await cache.SetAsync(cacheKey, subscription, TimeSpan.FromMinutes(5));
|
||||||
|
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Solar Network</title>
|
<title>Solar Network</title>
|
||||||
<app-data />
|
<app-data />
|
||||||
|
<og-data />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
7
DysonNetwork.Sphere/Client/src/dy-prefetch.d.ts
vendored
Normal file
7
DysonNetwork.Sphere/Client/src/dy-prefetch.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export {}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
DyPrefetch?: any
|
||||||
|
}
|
||||||
|
}
|
@@ -10,6 +10,11 @@ const router = createRouter({
|
|||||||
name: 'index',
|
name: 'index',
|
||||||
component: () => import('../views/index.vue'),
|
component: () => import('../views/index.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/posts/:slug',
|
||||||
|
name: 'postDetail',
|
||||||
|
component: () => import('../views/posts.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -4,7 +4,11 @@
|
|||||||
<n-gi span="3">
|
<n-gi span="3">
|
||||||
<n-infinite-scroll style="height: calc(100vh - 57px)" :distance="10" @load="fetchActivites">
|
<n-infinite-scroll style="height: calc(100vh - 57px)" :distance="10" @load="fetchActivites">
|
||||||
<div v-for="activity in activites" :key="activity.id" class="mt-4">
|
<div v-for="activity in activites" :key="activity.id" class="mt-4">
|
||||||
<post-item v-if="activity.type == 'posts.new'" :item="activity.data" />
|
<post-item
|
||||||
|
v-if="activity.type.startsWith('posts')"
|
||||||
|
:item="activity.data"
|
||||||
|
@click="router.push('/posts/' + activity.id)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</n-infinite-scroll>
|
</n-infinite-scroll>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
@@ -40,11 +44,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NCard, NInfiniteScroll, NGrid, NGi, NAlert, NA, NHr } from 'naive-ui'
|
import { NCard, NInfiniteScroll, NGrid, NGi, NAlert, NA, NHr } from 'naive-ui'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
import PostEditor from '@/components/PostEditor.vue'
|
import PostEditor from '@/components/PostEditor.vue'
|
||||||
import PostItem from '@/components/PostItem.vue'
|
import PostItem from '@/components/PostItem.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const version = ref<any>(null)
|
const version = ref<any>(null)
|
||||||
|
100
DysonNetwork.Sphere/Client/src/views/posts.vue
Normal file
100
DysonNetwork.Sphere/Client/src/views/posts.vue
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="post" class="container max-w-5xl mx-auto mt-4">
|
||||||
|
<n-grid cols="1 l:5" responsive="screen" :x-gap="16">
|
||||||
|
<n-gi span="3">
|
||||||
|
<post-item :item="post" />
|
||||||
|
</n-gi>
|
||||||
|
<n-gi span="2">
|
||||||
|
<n-card title="About the author">
|
||||||
|
<div class="relative mb-7">
|
||||||
|
<img
|
||||||
|
class="object-cover rounded-lg"
|
||||||
|
style="aspect-ratio: 16/7"
|
||||||
|
:src="publisherBackground"
|
||||||
|
/>
|
||||||
|
<div class="absolute left-3 bottom-[-24px]">
|
||||||
|
<n-avatar :src="publisherAvatar" :size="64" round bordered />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<p class="flex gap-1 items-baseline">
|
||||||
|
<span class="font-bold">
|
||||||
|
{{ post.publisher.nick }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm"> @{{ post.publisher.name }} </span>
|
||||||
|
</p>
|
||||||
|
<div class="max-h-96 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
class="prose prose-sm dark:prose-invert prose-slate"
|
||||||
|
v-if="publisherBio"
|
||||||
|
v-html="publisherBio"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="notFound" class="flex justify-center items-center h-full">
|
||||||
|
<n-result
|
||||||
|
status="404"
|
||||||
|
title="Post not found"
|
||||||
|
description="The post you are looking cannot be found, it might be deleted, or you have no permission to view it or it just never been posted."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex justify-center items-center h-full">
|
||||||
|
<n-spin />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { NGrid, NGi, NCard, NAvatar } from 'naive-ui'
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { Marked } from 'marked'
|
||||||
|
|
||||||
|
import PostItem from '@/components/PostItem.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const post = ref<any>()
|
||||||
|
const notFound = ref(false)
|
||||||
|
|
||||||
|
async function fetchPost() {
|
||||||
|
if (window.DyPrefetch?.Post != null) {
|
||||||
|
console.log('[Fetch] Use the pre-rendered post data.')
|
||||||
|
post.value = window.DyPrefetch.Post
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Fetch] Using the API to load user data.')
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/posts/${route.params.slug}`)
|
||||||
|
post.value = await resp.json()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
notFound.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(() => fetchPost())
|
||||||
|
|
||||||
|
const publisherAvatar = computed(() =>
|
||||||
|
post.value.publisher.picture ? `/cgi/drive/files/${post.value.publisher.picture.id}` : undefined,
|
||||||
|
)
|
||||||
|
const publisherBackground = computed(() =>
|
||||||
|
post.value.publisher.background
|
||||||
|
? `/cgi/drive/files/${post.value.publisher.background.id}`
|
||||||
|
: undefined,
|
||||||
|
)
|
||||||
|
|
||||||
|
const marked = new Marked()
|
||||||
|
|
||||||
|
const publisherBio = ref('')
|
||||||
|
watch(
|
||||||
|
post,
|
||||||
|
async (value) => {
|
||||||
|
if (value?.publisher?.bio) publisherBio.value = await marked.parse(value.publisher.bio)
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
)
|
||||||
|
</script>
|
Reference in New Issue
Block a user