refactor: ♻️ Reimplement Notes

This commit is contained in:
Jesse Wierzbinski 2024-11-30 02:19:32 +01:00
parent 9ced2c98e4
commit d29f181000
No known key found for this signature in database
21 changed files with 335 additions and 30 deletions

BIN
bun.lockb

Binary file not shown.

View 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>

View 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>

View 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>
&middot;
<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
View 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>

View file

@ -121,7 +121,7 @@ const instance = useInstance();
<SidebarHeader> <SidebarHeader>
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
<DropdownMenu> <NuxtLink href="/">
<SidebarMenuButton size="lg" <SidebarMenuButton size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"> class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
<Avatar shape="square" class="size-8"> <Avatar shape="square" class="size-8">
@ -135,7 +135,7 @@ const instance = useInstance();
</div> </div>
<!-- <ChevronsUpDown class="ml-auto" /> --> <!-- <ChevronsUpDown class="ml-auto" /> -->
</SidebarMenuButton> </SidebarMenuButton>
</DropdownMenu> </NuxtLink>
</SidebarMenuItem> </SidebarMenuItem>
</SidebarMenu> </SidebarMenu>
</SidebarHeader> </SidebarHeader>
@ -180,12 +180,6 @@ const instance = useInstance();
</NuxtLink> </NuxtLink>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton class="text-sidebar-foreground/70">
<MoreHorizontal class="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu> </SidebarMenu>
</SidebarGroup> </SidebarGroup>
</SidebarContent> </SidebarContent>
@ -270,7 +264,7 @@ const instance = useInstance();
</Breadcrumb> </Breadcrumb>
</div> </div>
</header> </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 /> <slot />
</div> </div>
</SidebarInset> </SidebarInset>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <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 { Button } from "../ui/button";
import { import {
DropdownMenu, DropdownMenu,
@ -9,25 +9,34 @@ import {
} from "../ui/dropdown-menu"; } from "../ui/dropdown-menu";
const colorMode = useColorMode(); const colorMode = useColorMode();
const themeNames = {
light: "Light",
dark: "Dark",
system: "System",
} as Record<string, string>;
</script> </script>
<template> <template>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger as-child> <DropdownMenuTrigger as-child>
<Button variant="ghost"> <Button variant="outline" class="w-full justify-start">
<Sun class="size-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Sun class="size-[1.2rem] scale-100 transition-all dark:scale-0 dark:hidden inline" />
<Moon class="absolute size-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <Moon class="size-[1.2rem] scale-0 transition-all dark:scale-100 hidden dark:inline" />
<span class="sr-only">Toggle theme</span> {{ themeNames[colorMode.preference] }}
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="start" >
<DropdownMenuItem @click="colorMode.preference = 'light'"> <DropdownMenuItem @click="colorMode.preference = 'light'">
<Sun class="size-4 mr-2" />
Light Light
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem @click="colorMode.preference = 'dark'"> <DropdownMenuItem @click="colorMode.preference = 'dark'">
<Moon class="size-4 mr-2" />
Dark Dark
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem @click="colorMode.preference = 'system'"> <DropdownMenuItem @click="colorMode.preference = 'system'">
<Wrench class="size-4 mr-2" />
System System
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>

View file

@ -1,11 +1,12 @@
<template> <template>
<component :is="itemComponent" :element="item" @update="$emit('update', $event)" <component :is="itemComponent" :note="item" @update="$emit('update', $event)"
@delete="$emit('delete', item?.id)" /> @delete="$emit('delete', item?.id)" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { Notification, Status } from "@versia/client/types"; import type { Notification, Status } from "@versia/client/types";
import { computed } from "vue"; import { computed } from "vue";
import NewNoteItem from "../notes/note.vue";
import NoteItem from "../social-elements/notes/note.vue"; import NoteItem from "../social-elements/notes/note.vue";
import NotificationItem from "../social-elements/notifications/notif.vue"; import NotificationItem from "../social-elements/notifications/notif.vue";
@ -16,7 +17,7 @@ const props = defineProps<{
const itemComponent = computed(() => { const itemComponent = computed(() => {
if (props.type === "status") { if (props.type === "status") {
return NoteItem; return NewNoteItem;
} }
if (props.type === "notification") { if (props.type === "notification") {
return NotificationItem; return NotificationItem;

View file

@ -1,12 +1,12 @@
<!-- Timeline.vue --> <!-- Timeline.vue -->
<template> <template>
<div class="timeline rounded overflow-hidden"> <div class="timeline rounded overflow-hidden ring-1 ring-ring/15">
<TransitionGroup name="timeline-item" tag="div" class="timeline-items"> <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" <TimelineItem :type="type" v-for="item in items" :key="item.id" :item="item" @update="updateItem"
@delete="removeItem" /> @delete="removeItem" />
</TransitionGroup> </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"> <div v-if="error" class="timeline-error">
{{ error.message }} {{ error.message }}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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";

View file

@ -1,14 +1,12 @@
<template> <template>
<div class="mx-auto max-w-2xl w-full"> <div class="mx-auto max-w-2xl w-full">
<TimelineScroller> <TimelineScroller>
<Greeting />
<Home /> <Home />
</TimelineScroller> </TimelineScroller>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Greeting from "~/components/headers/greeting.vue";
import Home from "~/components/timelines/home.vue"; import Home from "~/components/timelines/home.vue";
import TimelineScroller from "~/components/timelines/timeline-scroller.vue"; import TimelineScroller from "~/components/timelines/timeline-scroller.vue";

View file

@ -1,7 +1,6 @@
<template> <template>
<div class="mx-auto max-w-2xl w-full"> <div class="mx-auto max-w-2xl w-full">
<TimelineScroller> <TimelineScroller>
<Greeting />
<Local /> <Local />
</TimelineScroller> </TimelineScroller>
</div> </div>
@ -9,7 +8,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import Greeting from "~/components/headers/greeting.vue";
import Local from "~/components/timelines/local.vue"; import Local from "~/components/timelines/local.vue";
import TimelineScroller from "~/components/timelines/timeline-scroller.vue"; import TimelineScroller from "~/components/timelines/timeline-scroller.vue";

View file

@ -12,7 +12,6 @@
</button> </button>
</div> </div>
<TimelineScroller v-else> <TimelineScroller v-else>
<Greeting />
<div class="rounded overflow-hidden"> <div class="rounded overflow-hidden">
<Notifications /> <Notifications />
</div> </div>
@ -21,7 +20,6 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import Greeting from "~/components/headers/greeting.vue";
import Notifications from "~/components/timelines/notifications.vue"; import Notifications from "~/components/timelines/notifications.vue";
import TimelineScroller from "~/components/timelines/timeline-scroller.vue"; import TimelineScroller from "~/components/timelines/timeline-scroller.vue";

View file

@ -1,14 +1,12 @@
<template> <template>
<div class="mx-auto max-w-2xl w-full"> <div class="mx-auto max-w-2xl w-full">
<TimelineScroller> <TimelineScroller>
<Greeting />
<Public /> <Public />
</TimelineScroller> </TimelineScroller>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Greeting from "~/components/headers/greeting.vue";
import Public from "~/components/timelines/public.vue"; import Public from "~/components/timelines/public.vue";
import TimelineScroller from "~/components/timelines/timeline-scroller.vue"; import TimelineScroller from "~/components/timelines/timeline-scroller.vue";

View file

@ -1,6 +1,4 @@
@tailwind base; @tailwind base;
@tailwind components;
@tailwind utilities;
@layer base { @layer base {
:root { :root {