feat: status details

zio/stable
Anthony Fu 2022-11-14 22:54:30 +08:00
parent 9cc837f5df
commit c7ae7d5a1c
17 changed files with 151 additions and 125 deletions

View File

@ -1,80 +1,5 @@
<p align="center"> # Nuxtodon
<img src="https://user-images.githubusercontent.com/11247099/140462375-7b7ac4db-35b7-453c-8a05-13d8d20282c4.png" width="600"/>
</p>
<h2 align="center"> A Mastodon web client powered by Nuxt.
<a href="https://github.com/antfu/vitesse">Vitesse</a> for Nuxt 3
</h2><br>
<pre align="center"> > Very WIP.
🧪 Working in Progress
</pre>
<p align="center">
<br>
<a href="https://vitesse-nuxt3.netlify.app/">🖥 Online Preview</a>
<br><br>
<a href="https://stackblitz.com/github/antfu/vitesse-nuxt3"><img src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt=""></a>
</p>
## Features
- [💚 Nuxt 3](https://v3.nuxtjs.org) - SSR, ESR, File-based routing, components auto importing, modules, etc.
- ⚡️ Vite - Instant HMR
- 🎨 [UnoCSS](https://github.com/antfu/unocss) - The instant on-demand atomic CSS engine.
- 😃 Use icons from any icon sets in Pure CSS, powered by [UnoCSS](https://github.com/antfu/unocss)
- 🔥 The `<script setup>` syntax
- 🍍 [State Management via Pinia](https://pinia.esm.dev), see [./composables/user.ts](./composables/user.ts)
- 📑 [Layout system](./layouts)
- 📥 APIs auto importing - for Composition API, VueUse and custom composables.
- 🏎 Zero-config cloud functions and deploy
- 🦾 TypeScript, of course
## Plugins
### Nuxt Modules
- [VueUse](https://github.com/vueuse/vueuse) - collection of useful composition APIs.
- [ColorMode](https://github.com/nuxt-community/color-mode-module) - dark and Light mode with auto detection made easy with Nuxt.
- [UnoCSS](https://github.com/antfu/unocss) - the instant on-demand atomic CSS engine.
- [Pinia](https://pinia.esm.dev/) - intuitive, type safe, light and flexible Store for Vue.
## IDE
We recommend using [VS Code](https://code.visualstudio.com/) with [Volar](https://github.com/johnsoncodehk/volar) to get the best experience (You might want to disable Vetur if you have it).
## Variations
- [vitesse](https://github.com/antfu/vitesse) - Opinionated Vite Starter Template
- [vitesse-lite](https://github.com/antfu/vitesse-lite) - Lightweight version of Vitesse
- [vitesse-nuxt-bridge](https://github.com/antfu/vitesse-nuxt-bridge) - Vitesse for Nuxt 2 with Bridge
- [vitesse-webext](https://github.com/antfu/vitesse-webext) - WebExtension Vite starter template
## Try it now!
### Online
<a href="https://stackblitz.com/github/antfu/vitesse-nuxt3"><img src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt=""></a>
### GitHub Template
[Create a repo from this template on GitHub](https://github.com/antfu/vitesse-nuxt3/generate).
### Clone to local
If you prefer to do it manually with the cleaner git history
```bash
npx degit antfu/vitesse-nuxt3 my-nuxt3-app
cd my-nuxt3-app
pnpm i # If you don't have pnpm installed, run: npm install -g pnpm
```

View File

@ -11,6 +11,7 @@ useHead({
<template> <template>
<NuxtLayout> <NuxtLayout>
<NuxtLoadingIndicator />
<NuxtPage /> <NuxtPage />
</NuxtLayout> </NuxtLayout>
</template> </template>

View File

@ -8,12 +8,12 @@ defineProps<{
<template> <template>
<div flex gap-8> <div flex gap-8>
<button flex gap-1 items-center w-full rounded op75 hover="op100 text-blue" group> <RouterLink flex gap-1 items-center w-full rounded op75 hover="op100 text-blue" group :to="`/@${status.account.acct}/${status.id}`">
<div rounded-full p2 group-hover="bg-blue/10"> <div rounded-full p2 group-hover="bg-blue/10">
<div i-ri:chat-3-line /> <div i-ri:chat-3-line />
</div> </div>
<span v-if="status.repliesCount">{{ status.repliesCount }}</span> <span v-if="status.repliesCount">{{ status.repliesCount }}</span>
</button> </RouterLink>
<button flex gap-1 items-center w-full rounded op75 hover="op100 text-green" group> <button flex gap-1 items-center w-full rounded op75 hover="op100 text-green" group>
<div rounded-full p2 group-hover="bg-green/10"> <div rounded-full p2 group-hover="bg-green/10">
<div i-ri:repeat-fill /> <div i-ri:repeat-fill />

View File

@ -12,7 +12,7 @@ const { attachment } = defineProps<{
class="status-attachment-image" class="status-attachment-image"
:src="attachment.previewUrl!" :src="attachment.previewUrl!"
:alt="attachment.description!" :alt="attachment.description!"
border="~ gray/10" border="~ border"
object-cover rounded-lg object-cover rounded-lg
> >
</template> </template>

View File

@ -27,7 +27,7 @@ defineProps<{
--at-apply: font-bold; --at-apply: font-bold;
} }
p { p {
--at-apply: my-1; --at-apply: my-2;
} }
} }
</style> </style>

View File

@ -1,16 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Status } from 'masto' import type { Status } from 'masto'
const { status } = defineProps<{ const props = withDefaults(
status: Status defineProps<{
}>() status: Status
const el = ref<HTMLElement>() actions?: boolean
}>(),
{
actions: true,
},
)
const status = $computed(() => {
if (props.status.reblog && !props.status.content)
return props.status.reblog
return props.status
})
const rebloggedBy = $computed(() => props.status.reblog ? props.status.account : null)
const el = ref<HTMLElement>()
const router = useRouter() const router = useRouter()
function go(e: MouseEvent) { function go(e: MouseEvent) {
const path = e.composedPath() as HTMLElement[] const path = e.composedPath() as HTMLElement[]
const hasButton = path.find(el => el.tagName === 'A' || el.tagName === 'BUTTON') const hasButton = path.find(el => ['A', 'BUTTON', 'P'].includes(el.tagName.toUpperCase()))
if (hasButton) if (hasButton)
return return
@ -52,18 +66,35 @@ const timeago = useTimeAgo(() => status.createdAt, {
</script> </script>
<template> <template>
<div ref="el" flex flex-col gap-2 my-4 @click="go"> <div ref="el" flex flex-col gap-2 my-2 px-4 @click="go">
<div v-if="rebloggedBy" pl8>
<div flex gap-1 items-center text-gray:75 text-sm>
<div i-ri:repeat-fill mr-1 />
<a :href="`/@${rebloggedBy.acct}`" flex gap-2 font-bold items-center>
<img :src="rebloggedBy.avatar" class="w-5 h-5 rounded">
{{ rebloggedBy.displayName }}
</a>
reblogged
</div>
</div>
<AccountInfo :account="status.account"> <AccountInfo :account="status.account">
<div flex-auto /> <div flex-auto />
<div text-sm op50> <div text-sm op50>
{{ timeago }} {{ timeago }}
</div> </div>
</AccountInfo> </AccountInfo>
<StatusBody :status="status" /> <div pl14>
<StatusMedia <StatusBody :status="status" />
v-if="status.mediaAttachments?.length" <StatusMedia
:status="status" v-if="status.mediaAttachments?.length"
/> :status="status"
<StatusActions :status="status" /> />
<StatusCard
v-if="status.reblog"
:status="status.reblog" border="~ rounded"
:actions="false"
/>
</div>
<StatusActions v-if="actions !== false" pl12 :status="status" />
</div> </div>
</template> </template>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import type { Status } from 'masto'
const props = defineProps<{
status: Status
}>()
const status = $computed(() => {
if (props.status.reblog && props.status.reblog)
return props.status.reblog
return props.status
})
const formatter = Intl.DateTimeFormat(undefined, { dateStyle: 'long' })
const date = computed(() => formatter.format(new Date(status.createdAt)))
</script>
<template>
<div flex flex-col gap-2 my-4 px-4>
<AccountInfo :account="status.account" />
<StatusBody :status="status" text-xl />
<StatusMedia
v-if="status.mediaAttachments?.length"
:status="status"
/>
<div>
<span op50 text-sm>
{{ date }} · {{ status.application?.name || 'Unknown client' }}
</span>
</div>
<StatusActions :status="status" border="t border" pt-2 />
</div>
</template>

View File

@ -8,6 +8,6 @@ defineProps<{
<template> <template>
<template v-for="status of timelines" :key="status.id"> <template v-for="status of timelines" :key="status.id">
<StatusCard :status="status" border="t gray/10" pt-4 /> <StatusCard :status="status" border="t border" pt-4 />
</template> </template>
</template> </template>

View File

@ -1,6 +1,13 @@
<template> <template>
<main class="py-10 px-10"> <div h-full of-hidden>
<NavTitle /> <main grid="~ lg:cols-[1fr_40rem_1fr]" h-full>
<slot /> <div>
</main> <NavTitle p4 />
</div>
<div h-full of-auto border="l r border">
<slot />
</div>
<div>Right</div>
</main>
</div>
</template> </template>

View File

@ -1,17 +0,0 @@
<script setup lang="ts">
const router = useRouter()
</script>
<template>
<main p="x4 y10" text="center teal-700 dark:gray-200">
<div text-4xl>
<div i-carbon-warning inline-block />
</div>
<div>Not found</div>
<div>
<button btn text-sm m="3 t8" @click="router.back()">
Back
</button>
</div>
</main>
</template>

View File

@ -4,13 +4,18 @@ const props = defineProps<{
}>() }>()
const params = useRoute().params const params = useRoute().params
const id = computed(() => params.post as string)
const masto = await useMasto() const masto = await useMasto()
const { data: status } = await useAsyncData(() => masto.statuses.fetch(params.post as string)) const { data: status } = await useAsyncData(`${id}-status`, () => masto.statuses.fetch(params.post as string))
const { data: context } = await useAsyncData(`${id}-context`, () => masto.statuses.fetchContext(params.post as string))
</script> </script>
<template> <template>
<div w-130> <StatusDetails :status="status" />
<StatusCard :status="status" /> <template v-for="comment of context?.descendants" :key="comment.id">
</div> <StatusCard :status="comment" border="t border" pt-4 />
</template>
<pre>{{ status }}</pre>
<pre>{{ context }}</pre>
</template> </template>

View File

@ -1,10 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
const token = useCookie('nuxtodon-token')
const router = useRouter()
if (!token.value)
router.replace('/public')
const masto = await useMasto() const masto = await useMasto()
const { data: timelines } = await useAsyncData('public-timelines', () => masto.timelines.fetchPublic().then(r => r.value)) const { data: timelines } = await useAsyncData('timelines-home', () => masto.timelines.fetchHome().then(r => r.value))
</script> </script>
<template> <template>
<div w-120> <TimelineList :timelines="timelines" />
<TimelineList :timelines="timelines" />
</div>
</template> </template>

23
pages/login.vue 100644
View File

@ -0,0 +1,23 @@
<script setup lang="ts">
const server = useCookie('nuxtodon-server')
const token = useCookie('nuxtodon-token')
</script>
<template>
<div p4>
<input
v-model="server"
placeholder="Server URL"
bg-transparent text-current
border="~ border" p="x2 y1" w-full
outline-none
>
<input
v-model="token"
placeholder="Token"
bg-transparent text-current
border="~ border" p="x2 y1" w-full
outline-none
>
</div>
</template>

8
pages/public.vue 100644
View File

@ -0,0 +1,8 @@
<script setup lang="ts">
const masto = await useMasto()
const { data: timelines } = await useAsyncData('timelines-public', () => masto.timelines.fetchPublic().then(r => r.value))
</script>
<template>
<TimelineList :timelines="timelines" />
</template>

View File

@ -1,8 +1,13 @@
import { login } from 'masto' import { login } from 'masto'
export const DEFAULT_SERVER = 'https://mas.to'
export default defineNuxtPlugin((nuxt) => { export default defineNuxtPlugin((nuxt) => {
const server = useCookie('nuxtodon-server')
const token = useCookie('nuxtodon-token')
const masto = login({ const masto = login({
url: 'https://mas.to', url: server.value || DEFAULT_SERVER,
accessToken: token.value,
}) })
nuxt.vueApp.provide('masto', masto) nuxt.vueApp.provide('masto', masto)
}) })

View File

@ -1,3 +1,4 @@
:root { :root {
--color-primary: #53b3cb; --color-primary: #53b3cb;
--color-border: #88888820;
} }

View File

@ -36,6 +36,7 @@ export default defineConfig({
theme: { theme: {
colors: { colors: {
primary: 'var(--color-primary)', primary: 'var(--color-primary)',
border: 'var(--color-border)',
}, },
}, },
}) })