mirror of
https://github.com/versia-pub/frontend.git
synced 2025-12-06 08:28:20 +01:00
refactor: ♻️ Reimplement Notes
This commit is contained in:
parent
9ced2c98e4
commit
d29f181000
69
components/notes/content.vue
Normal file
69
components/notes/content.vue
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<div :class="['prose block relative dark:prose-invert duration-200 !max-w-full break-words', $style.content]" v-html="content">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { content } = defineProps<{
|
||||
content: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
.content pre:has(code) {
|
||||
word-wrap: normal;
|
||||
background: transparent;
|
||||
background-color: #ffffff0d;
|
||||
border-radius: .25rem;
|
||||
hyphens: none;
|
||||
margin-top: 1rem;
|
||||
overflow-x: auto;
|
||||
padding: .75rem 1rem;
|
||||
tab-size: 4;
|
||||
white-space: pre;
|
||||
word-break: normal;
|
||||
word-spacing: normal;
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), 0 0 #0000;
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
--tw-ring-color: hsla(0, 0%, 100%, .1)
|
||||
}
|
||||
|
||||
.content pre code {
|
||||
display: block;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
.content code:not(pre code)::after,
|
||||
.content code:not(pre code)::before {
|
||||
content: ""
|
||||
}
|
||||
|
||||
.content ol li input[type=checkbox],
|
||||
.content ul li input[type=checkbox] {
|
||||
border-radius:.25rem;
|
||||
margin-bottom:0.2rem;
|
||||
margin-right:.5rem;
|
||||
margin-top:0;
|
||||
vertical-align: middle;
|
||||
--tw-text-opacity:1;
|
||||
color: var(--theme-primary-400);
|
||||
}
|
||||
|
||||
.content code:not(pre code) {
|
||||
border-radius: .25rem;
|
||||
padding: .25rem .5rem;
|
||||
word-wrap: break-word;
|
||||
background: transparent;
|
||||
background-color: #ffffff0d;
|
||||
hyphens: none;
|
||||
margin-top: 1rem;
|
||||
tab-size: 4;
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), 0 0 #0000;
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
--tw-ring-color: hsla(0, 0%, 100%, .1)
|
||||
}
|
||||
</style>
|
||||
39
components/notes/copyable-text.vue
Normal file
39
components/notes/copyable-text.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<span :class="cn('text-primary group', $props.class)">
|
||||
<span class="group-hover:hidden">
|
||||
<slot />
|
||||
</span>
|
||||
<span class="hidden group-hover:inline">
|
||||
<span @click="copyText" v-if="!hasCopied"
|
||||
class="select-none cursor-pointer space-x-1">
|
||||
<Clipboard class="size-4 -translate-y-0.5 inline" />
|
||||
Click to copy
|
||||
</span>
|
||||
<span v-else class="select-none space-x-1">
|
||||
<Check class="size-4 -translate-y-0.5 inline" />
|
||||
Copied!
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Check, Clipboard } from "lucide-vue-next";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const { text } = defineProps<{
|
||||
text: string;
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
|
||||
const hasCopied = ref(false);
|
||||
const { copy } = useClipboard();
|
||||
const copyText = () => {
|
||||
copy(text);
|
||||
hasCopied.value = true;
|
||||
setTimeout(() => {
|
||||
hasCopied.value = false;
|
||||
}, 2000);
|
||||
};
|
||||
</script>
|
||||
70
components/notes/header.vue
Normal file
70
components/notes/header.vue
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<div class="rounded flex flex-row gap-4">
|
||||
<Avatar class="size-14 rounded border border-card">
|
||||
<AvatarImage :src="avatar" alt="" />
|
||||
<AvatarFallback class="rounded-lg"> AA </AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="flex flex-col gap-0.5 justify-center flex-1 text-left leading-tight">
|
||||
<span class="truncate font-semibold">{{
|
||||
displayName
|
||||
}}</span>
|
||||
<span class="truncate text-sm">
|
||||
<CopyableText :text="acct">
|
||||
<span
|
||||
class="font-semibold bg-gradient-to-tr from-pink-300 via-purple-300 to-indigo-400 text-transparent bg-clip-text">
|
||||
@{{ username }}
|
||||
</span>
|
||||
<span class="text-muted-foreground">{{ instance && "@" }}{{ instance }}</span>
|
||||
</CopyableText>
|
||||
·
|
||||
<span class="text-muted-foreground ml-auto" :title="fullTime">{{ timeAgo }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 justify-center items-end">
|
||||
<span class="text-xs text-muted-foreground" :title="visibilities[visibility].text">
|
||||
<component :is="visibilities[visibility].icon" class="size-5" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { StatusVisibility } from "@versia/client/types";
|
||||
import { AtSign, Globe, Lock, LockOpen } from "lucide-vue-next";
|
||||
import CopyableText from "./copyable-text.vue";
|
||||
|
||||
const { acct, createdAt } = defineProps<{
|
||||
avatar: string;
|
||||
acct: string;
|
||||
displayName: string;
|
||||
visibility: StatusVisibility;
|
||||
url: string;
|
||||
createdAt: Date;
|
||||
}>();
|
||||
|
||||
const [username, instance] = acct.split("@");
|
||||
const timeAgo = useTimeAgo(createdAt);
|
||||
const fullTime = new Intl.DateTimeFormat("en-US", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
}).format(createdAt);
|
||||
|
||||
const visibilities = {
|
||||
public: {
|
||||
icon: Globe,
|
||||
text: "This note is public: it can be seen by anyone.",
|
||||
},
|
||||
unlisted: {
|
||||
icon: LockOpen,
|
||||
text: "This note is unlisted: it can be seen by anyone with the link.",
|
||||
},
|
||||
private: {
|
||||
icon: Lock,
|
||||
text: "This note is private: it can only be seen by followers.",
|
||||
},
|
||||
direct: {
|
||||
icon: AtSign,
|
||||
text: "This note is direct: it can only be seen by mentioned users.",
|
||||
},
|
||||
};
|
||||
</script>
|
||||
26
components/notes/note.vue
Normal file
26
components/notes/note.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<Card as="article" class="rounded-none border-0 hover:bg-muted/50 duration-200">
|
||||
<CardHeader class="pb-4">
|
||||
<Header :avatar="note.account.avatar" :acct="note.account.acct" :display-name="note.account.display_name"
|
||||
:visibility="note.visibility" :url="accountUrl" :created-at="new Date(note.created_at)" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Content :content="note.content" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Status } from "@versia/client/types";
|
||||
import { Card, CardHeader } from "../ui/card";
|
||||
import { Separator } from "../ui/separator";
|
||||
import Content from "./content.vue";
|
||||
import Header from "./header.vue";
|
||||
|
||||
const { note } = defineProps<{
|
||||
note: Status;
|
||||
}>();
|
||||
|
||||
const url = `/@${note.account.acct}/${note.id}`;
|
||||
const accountUrl = `/@${note.account.acct}`;
|
||||
</script>
|
||||
|
|
@ -121,7 +121,7 @@ const instance = useInstance();
|
|||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<NuxtLink href="/">
|
||||
<SidebarMenuButton size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||
<Avatar shape="square" class="size-8">
|
||||
|
|
@ -135,7 +135,7 @@ const instance = useInstance();
|
|||
</div>
|
||||
<!-- <ChevronsUpDown class="ml-auto" /> -->
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenu>
|
||||
</NuxtLink>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
|
|
@ -180,12 +180,6 @@ const instance = useInstance();
|
|||
</NuxtLink>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton class="text-sidebar-foreground/70">
|
||||
<MoreHorizontal class="text-sidebar-foreground/70" />
|
||||
<span>More</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
|
|
@ -270,7 +264,7 @@ const instance = useInstance();
|
|||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<div class="flex flex-1 flex-col gap-4 pt-0 overflow-auto">
|
||||
<div class="flex flex-1 flex-col gap-4 md:p-1 overflow-auto">
|
||||
<slot />
|
||||
</div>
|
||||
</SidebarInset>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { Moon, Sun } from "lucide-vue-next";
|
||||
import { Moon, Sun, Wrench } from "lucide-vue-next";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -9,25 +9,34 @@ import {
|
|||
} from "../ui/dropdown-menu";
|
||||
|
||||
const colorMode = useColorMode();
|
||||
|
||||
const themeNames = {
|
||||
light: "Light",
|
||||
dark: "Dark",
|
||||
system: "System",
|
||||
} as Record<string, string>;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="ghost">
|
||||
<Sun class="size-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon class="absolute size-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span class="sr-only">Toggle theme</span>
|
||||
<Button variant="outline" class="w-full justify-start">
|
||||
<Sun class="size-[1.2rem] scale-100 transition-all dark:scale-0 dark:hidden inline" />
|
||||
<Moon class="size-[1.2rem] scale-0 transition-all dark:scale-100 hidden dark:inline" />
|
||||
{{ themeNames[colorMode.preference] }}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuContent align="start" >
|
||||
<DropdownMenuItem @click="colorMode.preference = 'light'">
|
||||
<Sun class="size-4 mr-2" />
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="colorMode.preference = 'dark'">
|
||||
<Moon class="size-4 mr-2" />
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="colorMode.preference = 'system'">
|
||||
<Wrench class="size-4 mr-2" />
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<template>
|
||||
<component :is="itemComponent" :element="item" @update="$emit('update', $event)"
|
||||
<component :is="itemComponent" :note="item" @update="$emit('update', $event)"
|
||||
@delete="$emit('delete', item?.id)" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Notification, Status } from "@versia/client/types";
|
||||
import { computed } from "vue";
|
||||
import NewNoteItem from "../notes/note.vue";
|
||||
import NoteItem from "../social-elements/notes/note.vue";
|
||||
import NotificationItem from "../social-elements/notifications/notif.vue";
|
||||
|
||||
|
|
@ -16,7 +17,7 @@ const props = defineProps<{
|
|||
|
||||
const itemComponent = computed(() => {
|
||||
if (props.type === "status") {
|
||||
return NoteItem;
|
||||
return NewNoteItem;
|
||||
}
|
||||
if (props.type === "notification") {
|
||||
return NotificationItem;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
<!-- Timeline.vue -->
|
||||
<template>
|
||||
<div class="timeline rounded overflow-hidden">
|
||||
<TransitionGroup name="timeline-item" tag="div" class="timeline-items">
|
||||
<div class="timeline rounded overflow-hidden ring-1 ring-ring/15">
|
||||
<TransitionGroup name="timeline-item" tag="div" class="timeline-items *:!border-b *:last:border-0">
|
||||
<TimelineItem :type="type" v-for="item in items" :key="item.id" :item="item" @update="updateItem"
|
||||
@delete="removeItem" />
|
||||
</TransitionGroup>
|
||||
|
||||
<TimelineItem v-if="isLoading" :type="type" v-for="_ in 5" />
|
||||
<!-- <TimelineItem v-if="isLoading" :type="type" v-for="_ in 5" /> -->
|
||||
|
||||
<div v-if="error" class="timeline-error">
|
||||
{{ error.message }}
|
||||
|
|
|
|||
27
components/ui/card/Card.vue
Normal file
27
components/ui/card/Card.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Primitive, type PrimitiveProps } from "radix-vue";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(),
|
||||
{
|
||||
as: "div",
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
:as="props.as"
|
||||
:as-child="props.asChild"
|
||||
:class="
|
||||
cn(
|
||||
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
14
components/ui/card/CardContent.vue
Normal file
14
components/ui/card/CardContent.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('p-6 pt-0', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
14
components/ui/card/CardDescription.vue
Normal file
14
components/ui/card/CardDescription.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p :class="cn('text-sm text-muted-foreground', props.class)">
|
||||
<slot />
|
||||
</p>
|
||||
</template>
|
||||
14
components/ui/card/CardFooter.vue
Normal file
14
components/ui/card/CardFooter.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex items-center p-6 pt-0', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
14
components/ui/card/CardHeader.vue
Normal file
14
components/ui/card/CardHeader.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
18
components/ui/card/CardTitle.vue
Normal file
18
components/ui/card/CardTitle.vue
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h3
|
||||
:class="
|
||||
cn('text-2xl font-semibold leading-none tracking-tight', props.class)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</h3>
|
||||
</template>
|
||||
6
components/ui/card/index.ts
Normal file
6
components/ui/card/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export { default as Card } from "./Card.vue";
|
||||
export { default as CardContent } from "./CardContent.vue";
|
||||
export { default as CardDescription } from "./CardDescription.vue";
|
||||
export { default as CardFooter } from "./CardFooter.vue";
|
||||
export { default as CardHeader } from "./CardHeader.vue";
|
||||
export { default as CardTitle } from "./CardTitle.vue";
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
<template>
|
||||
<div class="mx-auto max-w-2xl w-full">
|
||||
<TimelineScroller>
|
||||
<Greeting />
|
||||
<Home />
|
||||
</TimelineScroller>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Greeting from "~/components/headers/greeting.vue";
|
||||
import Home from "~/components/timelines/home.vue";
|
||||
import TimelineScroller from "~/components/timelines/timeline-scroller.vue";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<div class="mx-auto max-w-2xl w-full">
|
||||
<TimelineScroller>
|
||||
<Greeting />
|
||||
<Local />
|
||||
</TimelineScroller>
|
||||
</div>
|
||||
|
|
@ -9,7 +8,6 @@
|
|||
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Greeting from "~/components/headers/greeting.vue";
|
||||
import Local from "~/components/timelines/local.vue";
|
||||
import TimelineScroller from "~/components/timelines/timeline-scroller.vue";
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
</button>
|
||||
</div>
|
||||
<TimelineScroller v-else>
|
||||
<Greeting />
|
||||
<div class="rounded overflow-hidden">
|
||||
<Notifications />
|
||||
</div>
|
||||
|
|
@ -21,7 +20,6 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Greeting from "~/components/headers/greeting.vue";
|
||||
import Notifications from "~/components/timelines/notifications.vue";
|
||||
import TimelineScroller from "~/components/timelines/timeline-scroller.vue";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
<template>
|
||||
<div class="mx-auto max-w-2xl w-full">
|
||||
<TimelineScroller>
|
||||
<Greeting />
|
||||
<Public />
|
||||
</TimelineScroller>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Greeting from "~/components/headers/greeting.vue";
|
||||
import Public from "~/components/timelines/public.vue";
|
||||
import TimelineScroller from "~/components/timelines/timeline-scroller.vue";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
|
|
|
|||
Loading…
Reference in a new issue