mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-30 00:41:00 +00:00
feat: the last commit of year
This commit is contained in:
parent
b1d2496f8e
commit
56fab1dda6
BIN
apps/desktop/public/heading.png
Normal file
BIN
apps/desktop/public/heading.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
apps/desktop/public/heading@2x.png
Normal file
BIN
apps/desktop/public/heading@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
@ -1,25 +1,51 @@
|
|||||||
|
import { SettingsIcon } from "@lume/icons";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export function WelcomeScreen() {
|
export function WelcomeScreen() {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-10">
|
<div className="flex flex-col items-center justify-between w-full h-full">
|
||||||
<div className="mx-auto max-w-md flex w-full text-center flex-col items-center">
|
<div />
|
||||||
<h1 className="mb-2 text-4xl font-semibold">Welcome to Nostr!</h1>
|
<div className="flex flex-col items-center w-full max-w-4xl gap-10 mx-auto">
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<img
|
||||||
|
src="/heading.png"
|
||||||
|
srcSet="/heading@2x.png 2x"
|
||||||
|
alt="lume"
|
||||||
|
className="w-2/3"
|
||||||
|
/>
|
||||||
|
<p className="mt-5 text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
|
||||||
|
Lume is your safe Nostr client to meet, explore and
|
||||||
|
<br />
|
||||||
|
freely sharing your though to everyone in nostrverse
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 px-8 mx-auto max-w-sm">
|
<div className="flex flex-col w-full max-w-xs gap-2 mx-auto">
|
||||||
<Link
|
<Link
|
||||||
to="/auth/create"
|
to="/auth/create"
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
|
className="inline-flex items-center justify-center w-full h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
Create new account
|
Create New Account
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/auth/import"
|
to="/auth/import"
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg font-medium text-neutral-900 hover:bg-neutral-100 dark:text-neutral-100 dark:hover:bg-neutral-900"
|
className="inline-flex items-center justify-center w-full h-12 font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
|
||||||
>
|
>
|
||||||
Log in
|
Login
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center justify-center h-11">
|
||||||
|
<p className="text-neutral-800">
|
||||||
|
Before joining Nostr, you can take time to learn more about Nostr{" "}
|
||||||
|
<Link
|
||||||
|
to="https://nostr.com"
|
||||||
|
target="_blank"
|
||||||
|
className="text-blue-500"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ export function HomeScreen() {
|
|||||||
const renderItem = (column: IColumn) => {
|
const renderItem = (column: IColumn) => {
|
||||||
switch (column.kind) {
|
switch (column.kind) {
|
||||||
case COL_TYPES.newsfeed:
|
case COL_TYPES.newsfeed:
|
||||||
return <Timeline key={column.id} />;
|
return <Timeline key={column.id} column={column} />;
|
||||||
case COL_TYPES.thread:
|
case COL_TYPES.thread:
|
||||||
return <Thread key={column.id} column={column} />;
|
return <Thread key={column.id} column={column} />;
|
||||||
case COL_TYPES.user:
|
case COL_TYPES.user:
|
||||||
@ -24,12 +24,12 @@ export function HomeScreen() {
|
|||||||
case COL_TYPES.hashtag:
|
case COL_TYPES.hashtag:
|
||||||
return <Hashtag key={column.id} column={column} />;
|
return <Hashtag key={column.id} column={column} />;
|
||||||
default:
|
default:
|
||||||
return <Timeline key={column.id} />;
|
return <Timeline key={column.id} column={column} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full">
|
<div className="w-full h-full">
|
||||||
<VList
|
<VList
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="h-full w-full flex-nowrap overflow-x-auto !overflow-y-hidden scrollbar-none focus:outline-none"
|
className="h-full w-full flex-nowrap overflow-x-auto !overflow-y-hidden scrollbar-none focus:outline-none"
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lume/tailwindcss": "workspace:^",
|
"@lume/tailwindcss": "workspace:^",
|
||||||
"@lume/tsconfig": "workspace:^",
|
"@lume/tsconfig": "workspace:^",
|
||||||
|
"@lume/types": "workspace:^",
|
||||||
"@types/react": "^18.2.46",
|
"@types/react": "^18.2.46",
|
||||||
"tailwind": "^4.0.0",
|
"tailwind": "^4.0.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
|
@ -3,11 +3,12 @@ import { TimelineIcon } from "@lume/icons";
|
|||||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
import { IColumn } from "../../../types";
|
||||||
import { EventRoute } from "./event";
|
import { EventRoute } from "./event";
|
||||||
import { HomeRoute } from "./home";
|
import { HomeRoute } from "./home";
|
||||||
import { UserRoute } from "./user";
|
import { UserRoute } from "./user";
|
||||||
|
|
||||||
export function Timeline() {
|
export function Timeline({ column }: { column: IColumn }) {
|
||||||
const colKey = "newsfeed";
|
const colKey = "newsfeed";
|
||||||
const storage = useStorage();
|
const storage = useStorage();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -26,7 +27,7 @@ export function Timeline() {
|
|||||||
return (
|
return (
|
||||||
<Column.Root>
|
<Column.Root>
|
||||||
<Column.Header
|
<Column.Header
|
||||||
id="9999"
|
id={column.id}
|
||||||
queryKey={[colKey]}
|
queryKey={[colKey]}
|
||||||
title="Timeline"
|
title="Timeline"
|
||||||
icon={<TimelineIcon className="size-4" />}
|
icon={<TimelineIcon className="size-4" />}
|
||||||
|
@ -17,35 +17,35 @@ export function ColumnHeader({
|
|||||||
queryKey,
|
queryKey,
|
||||||
icon,
|
icon,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
queryKey?: string[];
|
queryKey?: string[];
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { removeColumn } = useColumnContext();
|
const { moveColumn, removeColumn } = useColumnContext();
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
if (queryKey) await queryClient.refetchQueries({ queryKey });
|
if (queryKey) await queryClient.refetchQueries({ queryKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveLeft = async () => {
|
const moveLeft = async () => {
|
||||||
removeColumn(id);
|
moveColumn(id, "left");
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveRight = async () => {
|
const moveRight = async () => {
|
||||||
removeColumn(id);
|
moveColumn(id, "right");
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteWidget = async () => {
|
const deleteWidget = async () => {
|
||||||
removeColumn(id);
|
await removeColumn(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-11 w-full shrink-0 items-center justify-between border-b border-neutral-100 px-3 dark:border-neutral-900">
|
<div className="flex items-center justify-between w-full px-3 border-b h-11 shrink-0 border-neutral-100 dark:border-neutral-900">
|
||||||
<div className="inline-flex items-center gap-4">
|
<div className="inline-flex items-center gap-4">
|
||||||
<div className="h-5 w-1 shrink-0 rounded-full bg-blue-500" />
|
<div className="w-1 h-5 bg-blue-500 rounded-full shrink-0" />
|
||||||
<div className="text-neutral-800 dark:text-neutral-200 inline-flex items-center gap-2 flex-1">
|
<div className="inline-flex items-center flex-1 gap-2 text-neutral-800 dark:text-neutral-200">
|
||||||
{icon ? icon : <ThreadIcon className="size-4" />}
|
{icon ? icon : <ThreadIcon className="size-4" />}
|
||||||
<div className="text-sm font-medium">{title}</div>
|
<div className="text-sm font-medium">{title}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -55,7 +55,7 @@ export function ColumnHeader({
|
|||||||
<DropdownMenu.Trigger asChild>
|
<DropdownMenu.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex h-6 w-6 items-center justify-center"
|
className="inline-flex items-center justify-center w-6 h-6"
|
||||||
>
|
>
|
||||||
<HorizontalDotsIcon className="size-4" />
|
<HorizontalDotsIcon className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -66,7 +66,7 @@ export function ColumnHeader({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={refresh}
|
onClick={refresh}
|
||||||
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-neutral-700 hover:bg-blue-100 hover:text-blue-500 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-900 dark:hover:text-neutral-50"
|
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-neutral-700 hover:bg-blue-100 hover:text-blue-500 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-900 dark:hover:text-neutral-50"
|
||||||
>
|
>
|
||||||
<RefreshIcon className="size-5" />
|
<RefreshIcon className="size-5" />
|
||||||
Refresh
|
Refresh
|
||||||
@ -76,7 +76,7 @@ export function ColumnHeader({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={moveLeft}
|
onClick={moveLeft}
|
||||||
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-neutral-700 hover:bg-blue-100 hover:text-blue-500 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-900 dark:hover:text-neutral-50"
|
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-neutral-700 hover:bg-blue-100 hover:text-blue-500 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-900 dark:hover:text-neutral-50"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="size-5" />
|
<ArrowLeftIcon className="size-5" />
|
||||||
Move left
|
Move left
|
||||||
@ -86,18 +86,18 @@ export function ColumnHeader({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={moveRight}
|
onClick={moveRight}
|
||||||
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-neutral-700 hover:bg-blue-100 hover:text-blue-500 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-900 dark:hover:text-neutral-50"
|
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-neutral-700 hover:bg-blue-100 hover:text-blue-500 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-900 dark:hover:text-neutral-50"
|
||||||
>
|
>
|
||||||
<ArrowRightIcon className="size-5" />
|
<ArrowRightIcon className="size-5" />
|
||||||
Move right
|
Move right
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Separator className="my-1 h-px bg-neutral-100 dark:bg-neutral-900" />
|
<DropdownMenu.Separator className="h-px my-1 bg-neutral-100 dark:bg-neutral-900" />
|
||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={deleteWidget}
|
onClick={deleteWidget}
|
||||||
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-red-500 hover:bg-red-500 hover:text-red-50 focus:outline-none"
|
className="inline-flex items-center gap-2 px-3 text-sm font-medium text-red-500 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none"
|
||||||
>
|
>
|
||||||
<TrashIcon className="size-5" />
|
<TrashIcon className="size-5" />
|
||||||
Delete
|
Delete
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { IColumn } from "@lume/types";
|
import { IColumn } from "@lume/types";
|
||||||
import { COL_TYPES } from "@lume/utils";
|
import { COL_TYPES } from "@lume/utils";
|
||||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
|
||||||
import {
|
import {
|
||||||
ReactNode,
|
ReactNode,
|
||||||
createContext,
|
createContext,
|
||||||
@ -13,9 +12,10 @@ import { useStorage } from "../../provider";
|
|||||||
|
|
||||||
type ColumnContext = {
|
type ColumnContext = {
|
||||||
columns: IColumn[];
|
columns: IColumn[];
|
||||||
addColumn: (column: IColumn) => void;
|
addColumn: (column: IColumn) => Promise<void>;
|
||||||
removeColumn: (id: string) => void;
|
removeColumn: (id: number) => Promise<void>;
|
||||||
loadAllColumns: () => void;
|
moveColumn: (id: number, position: "left" | "right") => void;
|
||||||
|
loadAllColumns: () => Promise<IColumn[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ColumnContext = createContext<ColumnContext>(null);
|
const ColumnContext = createContext<ColumnContext>(null);
|
||||||
@ -24,7 +24,7 @@ export function ColumnProvider({ children }: { children: ReactNode }) {
|
|||||||
const storage = useStorage();
|
const storage = useStorage();
|
||||||
const [columns, setColumns] = useState<IColumn[]>([
|
const [columns, setColumns] = useState<IColumn[]>([
|
||||||
{
|
{
|
||||||
id: "9999",
|
id: 9999,
|
||||||
title: "Newsfeed",
|
title: "Newsfeed",
|
||||||
content: "",
|
content: "",
|
||||||
kind: COL_TYPES.newsfeed,
|
kind: COL_TYPES.newsfeed,
|
||||||
@ -44,11 +44,28 @@ export function ColumnProvider({ children }: { children: ReactNode }) {
|
|||||||
if (result) setColumns((prev) => [...prev, column]);
|
if (result) setColumns((prev) => [...prev, column]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const removeColumn = useCallback(async (id: string) => {
|
const removeColumn = useCallback(async (id: number) => {
|
||||||
await storage.removeWidget(id);
|
await storage.removeWidget(id);
|
||||||
setColumns((prev) => prev.filter((t) => t.id !== id));
|
setColumns((prev) => prev.filter((t) => t.id !== id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const moveColumn = useCallback(
|
||||||
|
(id: number, position: "left" | "right") => {
|
||||||
|
const newCols = [...columns];
|
||||||
|
|
||||||
|
const col = newCols.find((el) => el.id === id);
|
||||||
|
const colIndex = newCols.findIndex((el) => el.id === id);
|
||||||
|
|
||||||
|
newCols.splice(colIndex, 1);
|
||||||
|
|
||||||
|
if (position === "left") newCols.splice(colIndex - 1, 0, col);
|
||||||
|
if (position === "right") newCols.splice(colIndex + 1, 0, col);
|
||||||
|
|
||||||
|
setColumns(newCols);
|
||||||
|
},
|
||||||
|
[columns],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
|
|
||||||
@ -63,7 +80,7 @@ export function ColumnProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ColumnContext.Provider
|
<ColumnContext.Provider
|
||||||
value={{ columns, addColumn, removeColumn, loadAllColumns }}
|
value={{ columns, addColumn, removeColumn, moveColumn, loadAllColumns }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ColumnContext.Provider>
|
</ColumnContext.Provider>
|
||||||
|
@ -14,23 +14,23 @@ export function NotePin() {
|
|||||||
<Tooltip.Trigger asChild>
|
<Tooltip.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={async () =>
|
||||||
addColumn({
|
await addColumn({
|
||||||
kind: COL_TYPES.thread,
|
kind: COL_TYPES.thread,
|
||||||
title: "Thread",
|
title: "Thread",
|
||||||
content: event.id,
|
content: event.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="inline-flex h-7 w-max items-center justify-center gap-2 rounded-full bg-neutral-100 hover:bg-neutral-200 dark:hover:bg-neutral-800 px-2 text-sm font-medium dark:bg-neutral-900"
|
className="inline-flex items-center justify-center gap-2 pl-2 pr-3 text-sm font-medium rounded-full h-7 w-max bg-neutral-100 hover:bg-neutral-200 dark:hover:bg-neutral-800 dark:bg-neutral-900"
|
||||||
>
|
>
|
||||||
<PinIcon className="size-4" />
|
<PinIcon className="size-4" />
|
||||||
Pin
|
Pin
|
||||||
</button>
|
</button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Portal>
|
<Tooltip.Portal>
|
||||||
<Tooltip.Content className="bg-neutral-950 text-white -left-10 inline-flex h-7 select-none items-center justify-center rounded-md px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
|
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
|
||||||
Pin note
|
Pin note
|
||||||
<Tooltip.Arrow className="fill-neutral-950" />
|
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
</Tooltip.Portal>
|
</Tooltip.Portal>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { ReactionIcon } from "@lume/icons";
|
import { ReactionIcon } from "@lume/icons";
|
||||||
import * as HoverCard from "@radix-ui/react-hover-card";
|
import * as HoverCard from "@radix-ui/react-hover-card";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useNoteContext } from "../provider";
|
import { useNoteContext } from "../provider";
|
||||||
|
|
||||||
const REACTIONS = [
|
const REACTIONS = [
|
||||||
@ -49,7 +48,7 @@ export function NoteReaction() {
|
|||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -58,22 +57,22 @@ export function NoteReaction() {
|
|||||||
<HoverCard.Trigger asChild>
|
<HoverCard.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-600 dark:text-neutral-400"
|
className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400"
|
||||||
>
|
>
|
||||||
{reaction ? (
|
{reaction ? (
|
||||||
<img
|
<img
|
||||||
src={getReactionImage(reaction)}
|
src={getReactionImage(reaction)}
|
||||||
alt={reaction}
|
alt={reaction}
|
||||||
className="size-5"
|
className="size-6"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ReactionIcon className="size-5 group-hover:text-blue-500" />
|
<ReactionIcon className="size-6 group-hover:text-blue-500" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</HoverCard.Trigger>
|
</HoverCard.Trigger>
|
||||||
<HoverCard.Portal>
|
<HoverCard.Portal>
|
||||||
<HoverCard.Content
|
<HoverCard.Content
|
||||||
className="select-none rounded-lg bg-neutral-950 px-1 py-1 text-sm will-change-[transform,opacity] data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=top]:animate-slideDownAndFade"
|
className="select-none rounded-lg bg-neutral-950 dark:bg-neutral-50 px-1 py-1 text-sm will-change-[transform,opacity] data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=top]:animate-slideDownAndFade"
|
||||||
sideOffset={0}
|
sideOffset={0}
|
||||||
side="top"
|
side="top"
|
||||||
>
|
>
|
||||||
@ -81,56 +80,56 @@ export function NoteReaction() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => react("👏")}
|
onClick={() => react("👏")}
|
||||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10"
|
className="inline-flex items-center justify-center w-8 h-8 rounded-md backdrop-blur-xl hover:bg-white/10 dark:hover:bg-black/10"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/clapping_hands.png"
|
src="/clapping_hands.png"
|
||||||
alt="Clapping Hands"
|
alt="Clapping Hands"
|
||||||
className="h-6 w-6"
|
className="size-6"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => react("🤪")}
|
onClick={() => react("🤪")}
|
||||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10"
|
className="inline-flex items-center justify-center w-8 h-8 rounded-md backdrop-blur-xl hover:bg-white/10 dark:hover:bg-black/10"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/face_with_tongue.png"
|
src="/face_with_tongue.png"
|
||||||
alt="Face with Tongue"
|
alt="Face with Tongue"
|
||||||
className="h-6 w-6"
|
className="size-6"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => react("😮")}
|
onClick={() => react("😮")}
|
||||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10"
|
className="inline-flex items-center justify-center w-8 h-8 rounded-md backdrop-blur-xl hover:bg-white/10 dark:hover:bg-black/10"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/face_with_open_mouth.png"
|
src="/face_with_open_mouth.png"
|
||||||
alt="Face with Open Mouth"
|
alt="Face with Open Mouth"
|
||||||
className="h-6 w-6"
|
className="size-6"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => react("😢")}
|
onClick={() => react("😢")}
|
||||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10"
|
className="inline-flex items-center justify-center w-8 h-8 rounded-md backdrop-blur-xl hover:bg-white/10 dark:hover:bg-black/10"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/crying_face.png"
|
src="/crying_face.png"
|
||||||
alt="Crying Face"
|
alt="Crying Face"
|
||||||
className="h-6 w-6"
|
className="size-6"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => react("🤡")}
|
onClick={() => react("🤡")}
|
||||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10"
|
className="inline-flex items-center justify-center w-8 h-8 rounded-md backdrop-blur-xl hover:bg-white/10 dark:hover:bg-black/10"
|
||||||
>
|
>
|
||||||
<img src="/clown_face.png" alt="Clown Face" className="h-6 w-6" />
|
<img src="/clown_face.png" alt="Clown Face" className="size-6" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<HoverCard.Arrow className="fill-neutral-950" />
|
<HoverCard.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||||
</HoverCard.Content>
|
</HoverCard.Content>
|
||||||
</HoverCard.Portal>
|
</HoverCard.Portal>
|
||||||
</HoverCard.Root>
|
</HoverCard.Root>
|
||||||
|
@ -26,15 +26,15 @@ export function NoteReply({
|
|||||||
}).toString(),
|
}).toString(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-600 dark:text-neutral-400"
|
className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400"
|
||||||
>
|
>
|
||||||
<ReplyIcon className="h-5 w-5 group-hover:text-blue-500" />
|
<ReplyIcon className="size-6 group-hover:text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Portal>
|
<Tooltip.Portal>
|
||||||
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-white will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
|
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
|
||||||
Quick reply
|
Quick reply
|
||||||
<Tooltip.Arrow className="fill-neutral-950" />
|
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
</Tooltip.Portal>
|
</Tooltip.Portal>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
|
@ -29,20 +29,20 @@ export function NoteRepost() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-600 dark:text-neutral-400"
|
className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400"
|
||||||
>
|
>
|
||||||
<RepostIcon
|
<RepostIcon
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"h-5 w-5 group-hover:text-blue-600",
|
"size-6 group-hover:text-blue-600",
|
||||||
isRepost ? "text-blue-500" : "",
|
isRepost ? "text-blue-500" : "",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Portal>
|
<Tooltip.Portal>
|
||||||
<Tooltip.Content className="inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-white will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
|
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
|
||||||
Repost
|
Repost
|
||||||
<Tooltip.Arrow className="fill-neutral-950" />
|
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
</Tooltip.Portal>
|
</Tooltip.Portal>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
|
@ -107,32 +107,32 @@ export function NoteZap() {
|
|||||||
<Dialog.Trigger asChild>
|
<Dialog.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-600 dark:text-neutral-400"
|
className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400"
|
||||||
>
|
>
|
||||||
<ZapIcon className="h-5 w-5 group-hover:text-blue-500" />
|
<ZapIcon className="size-6 group-hover:text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm dark:bg-black/20" />
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm dark:bg-black/20" />
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
<Dialog.Content className="fixed inset-0 z-50 flex items-center justify-center min-h-full">
|
||||||
<div className="relative h-min w-full max-w-xl rounded-xl bg-white dark:bg-black">
|
<div className="relative w-full max-w-xl bg-white h-min rounded-xl dark:bg-black">
|
||||||
<div className="inline-flex w-full shrink-0 items-center justify-between px-5 py-3">
|
<div className="inline-flex items-center justify-between w-full px-5 py-3 shrink-0">
|
||||||
<div className="w-6" />
|
<div className="w-6" />
|
||||||
<Dialog.Title className="text-center font-semibold">
|
<Dialog.Title className="font-semibold text-center">
|
||||||
Send tip to{" "}
|
Send tip to{" "}
|
||||||
{user?.name ||
|
{user?.name ||
|
||||||
user?.displayName ||
|
user?.displayName ||
|
||||||
displayNpub(event.pubkey, 16)}
|
displayNpub(event.pubkey, 16)}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md bg-neutral-100 dark:bg-neutral-900">
|
<Dialog.Close className="inline-flex items-center justify-center w-6 h-6 rounded-md bg-neutral-100 dark:bg-neutral-900">
|
||||||
<CancelIcon className="h-4 w-4" />
|
<CancelIcon className="w-4 h-4" />
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto overflow-x-hidden px-5 pb-5">
|
<div className="px-5 pb-5 overflow-x-hidden overflow-y-auto">
|
||||||
{!invoice ? (
|
{!invoice ? (
|
||||||
<>
|
<>
|
||||||
<div className="relative flex h-40 flex-col">
|
<div className="relative flex flex-col h-40">
|
||||||
<div className="inline-flex h-full flex-1 items-center justify-center gap-1">
|
<div className="inline-flex items-center justify-center flex-1 h-full gap-1">
|
||||||
<CurrencyInput
|
<CurrencyInput
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
defaultValue={"21"}
|
defaultValue={"21"}
|
||||||
@ -142,9 +142,9 @@ export function NoteZap() {
|
|||||||
max={10000} // 1M sats
|
max={10000} // 1M sats
|
||||||
maxLength={10000} // 1M sats
|
maxLength={10000} // 1M sats
|
||||||
onValueChange={(value) => setAmount(value)}
|
onValueChange={(value) => setAmount(value)}
|
||||||
className="w-full flex-1 border-none bg-transparent text-right text-4xl font-semibold placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
|
className="flex-1 w-full text-4xl font-semibold text-right bg-transparent border-none placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
|
||||||
/>
|
/>
|
||||||
<span className="w-full flex-1 text-left text-4xl font-semibold text-neutral-600 dark:text-neutral-400">
|
<span className="flex-1 w-full text-4xl font-semibold text-left text-neutral-600 dark:text-neutral-400">
|
||||||
sats
|
sats
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -186,7 +186,7 @@ export function NoteZap() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex w-full flex-col gap-2">
|
<div className="flex flex-col w-full gap-2 mt-4">
|
||||||
<input
|
<input
|
||||||
name="zapMessage"
|
name="zapMessage"
|
||||||
value={zapMessage}
|
value={zapMessage}
|
||||||
@ -203,7 +203,7 @@ export function NoteZap() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => createZapRequest()}
|
onClick={() => createZapRequest()}
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 px-4 font-medium text-white hover:bg-blue-600"
|
className="inline-flex items-center justify-center w-full px-4 font-medium text-white bg-blue-500 rounded-lg h-11 hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
{isCompleted ? (
|
{isCompleted ? (
|
||||||
<p className="leading-tight">Successfully zapped</p>
|
<p className="leading-tight">Successfully zapped</p>
|
||||||
@ -229,7 +229,7 @@ export function NoteZap() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => createZapRequest()}
|
onClick={() => createZapRequest()}
|
||||||
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 px-4 font-medium text-white hover:bg-blue-600"
|
className="inline-flex items-center justify-center w-full px-4 font-medium text-white bg-blue-500 rounded-lg h-11 hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
Create Lightning invoice
|
Create Lightning invoice
|
||||||
</button>
|
</button>
|
||||||
@ -238,13 +238,13 @@ export function NoteZap() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-3 flex flex-col items-center justify-center gap-4">
|
<div className="flex flex-col items-center justify-center gap-4 mt-3">
|
||||||
<div className="rounded-md bg-neutral-100 p-3 dark:bg-neutral-900">
|
<div className="p-3 rounded-md bg-neutral-100 dark:bg-neutral-900">
|
||||||
<QRCodeSVG value={invoice} size={256} />
|
<QRCodeSVG value={invoice} size={256} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center gap-1">
|
<div className="flex flex-col items-center gap-1">
|
||||||
<h3 className="text-lg font-medium">Scan to zap</h3>
|
<h3 className="text-lg font-medium">Scan to zap</h3>
|
||||||
<span className="text-center text-sm text-neutral-600 dark:text-neutral-400">
|
<span className="text-sm text-center text-neutral-600 dark:text-neutral-400">
|
||||||
You must use Bitcoin wallet which support Lightning
|
You must use Bitcoin wallet which support Lightning
|
||||||
<br />
|
<br />
|
||||||
such as: Blue Wallet, Bitkit, Phoenix,...
|
such as: Blue Wallet, Bitkit, Phoenix,...
|
||||||
|
@ -7,14 +7,14 @@ export function Hashtag({ tag }: { tag: string }) {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={async () =>
|
||||||
addColumn({
|
await addColumn({
|
||||||
kind: COL_TYPES.hashtag,
|
kind: COL_TYPES.hashtag,
|
||||||
title: tag,
|
title: tag,
|
||||||
content: tag,
|
content: tag,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="cursor-default break-all text-blue-500 hover:text-blue-600"
|
className="text-blue-500 break-all cursor-default hover:text-blue-600"
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</button>
|
</button>
|
||||||
|
@ -24,7 +24,7 @@ export const MentionNote = memo(function MentionNote({
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full cursor-default rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
|
<div className="w-full p-3 my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900">
|
||||||
Loading
|
Loading
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -32,7 +32,7 @@ export const MentionNote = memo(function MentionNote({
|
|||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full cursor-default rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
|
<div className="w-full p-3 my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900">
|
||||||
Failed to fetch event
|
Failed to fetch event
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -40,11 +40,11 @@ export const MentionNote = memo(function MentionNote({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Note.Provider event={data}>
|
<Note.Provider event={data}>
|
||||||
<Note.Root className="flex w-full cursor-default flex-col gap-1 rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
<Note.Root className="flex flex-col w-full gap-1 my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900">
|
||||||
<div className="mt-3 px-3">
|
<div className="px-3 mt-3">
|
||||||
<Note.User variant="mention" />
|
<Note.User variant="mention" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 px-3 pb-3">
|
<div className="px-3 pb-3 mt-1">
|
||||||
{renderKind(data)}
|
{renderKind(data)}
|
||||||
<Link
|
<Link
|
||||||
to={`/events/${data.id}`}
|
to={`/events/${data.id}`}
|
||||||
|
@ -13,14 +13,14 @@ export const MentionUser = memo(function MentionUser({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger className="break-words text-blue-500 hover:text-blue-600">
|
<DropdownMenu.Trigger className="text-blue-500 break-words hover:text-blue-600">
|
||||||
{`@${user?.name || user?.displayName || user?.username || "anon"}`}
|
{`@${user?.name || user?.displayName || user?.username || "anon"}`}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content className="left-[50px] z-50 relative flex w-[200px] flex-col overflow-hidden rounded-xl border border-neutral-200 bg-neutral-950 focus:outline-none dark:border-neutral-900">
|
<DropdownMenu.Content className="left-[50px] z-50 relative flex w-[200px] flex-col overflow-hidden rounded-xl border border-neutral-200 bg-neutral-950 focus:outline-none dark:border-neutral-900">
|
||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<Link
|
<Link
|
||||||
to={`/users/${pubkey}`}
|
to={`/users/${pubkey}`}
|
||||||
className="inline-flex h-10 items-center px-4 text-sm text-white hover:bg-neutral-900 focus:outline-none"
|
className="inline-flex items-center h-10 px-4 text-sm text-white hover:bg-neutral-900 focus:outline-none"
|
||||||
>
|
>
|
||||||
View profile
|
View profile
|
||||||
</Link>
|
</Link>
|
||||||
@ -28,14 +28,14 @@ export const MentionUser = memo(function MentionUser({
|
|||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={async () =>
|
||||||
addColumn({
|
await addColumn({
|
||||||
kind: COL_TYPES.user,
|
kind: COL_TYPES.user,
|
||||||
title: user?.name || user?.displayName || "",
|
title: user?.name || user?.displayName || "",
|
||||||
content: pubkey,
|
content: pubkey,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="inline-flex h-10 items-center px-4 text-sm text-white hover:bg-neutral-900 focus:outline-none"
|
className="inline-flex items-center h-10 px-4 text-sm text-white hover:bg-neutral-900 focus:outline-none"
|
||||||
>
|
>
|
||||||
Pin
|
Pin
|
||||||
</button>
|
</button>
|
||||||
|
@ -39,7 +39,7 @@ export function ImagePreview({ url }: { url: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
|
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
|
||||||
<div onClick={open} className="group relative">
|
<div onClick={open} className="relative my-1 group">
|
||||||
<img
|
<img
|
||||||
src={url}
|
src={url}
|
||||||
alt={url}
|
alt={url}
|
||||||
@ -47,17 +47,17 @@ export function ImagePreview({ url }: { url: string }) {
|
|||||||
decoding="async"
|
decoding="async"
|
||||||
style={{ contentVisibility: "auto" }}
|
style={{ contentVisibility: "auto" }}
|
||||||
onError={fallback}
|
onError={fallback}
|
||||||
className="h-auto w-full rounded-lg border border-neutral-200/50 object-cover dark:border-neutral-800/50"
|
className="object-cover w-full h-auto border rounded-lg border-neutral-200/50 dark:border-neutral-800/50"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => downloadImage(e)}
|
onClick={(e) => downloadImage(e)}
|
||||||
className="absolute right-2 top-2 z-10 hidden h-10 w-10 items-center justify-center rounded-lg bg-blue-500 group-hover:inline-flex hover:bg-blue-600"
|
className="absolute z-10 items-center justify-center hidden w-10 h-10 bg-blue-500 rounded-lg right-2 top-2 group-hover:inline-flex hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
{downloaded ? (
|
{downloaded ? (
|
||||||
<CheckCircleIcon className="h-5 w-5 text-white" />
|
<CheckCircleIcon className="w-5 h-5 text-white" />
|
||||||
) : (
|
) : (
|
||||||
<DownloadIcon className="h-5 w-5 text-white" />
|
<DownloadIcon className="w-5 h-5 text-white" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,11 +11,11 @@ export function LinkPreview({ url }: { url: string }) {
|
|||||||
|
|
||||||
if (status === "pending") {
|
if (status === "pending") {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
<div className="flex flex-col w-full my-1 rounded-lg bg-neutral-100 dark:bg-neutral-900">
|
||||||
<div className="h-48 w-full animate-pulse bg-neutral-300 dark:bg-neutral-700" />
|
<div className="w-full h-48 animate-pulse bg-neutral-300 dark:bg-neutral-700" />
|
||||||
<div className="flex flex-col gap-2 px-3 py-3">
|
<div className="flex flex-col gap-2 px-3 py-3">
|
||||||
<div className="h-3 w-2/3 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
<div className="w-2/3 h-3 rounded animate-pulse bg-neutral-300 dark:bg-neutral-700" />
|
||||||
<div className="h-3 w-3/4 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
<div className="w-3/4 h-3 rounded animate-pulse bg-neutral-300 dark:bg-neutral-700" />
|
||||||
<span className="mt-2.5 text-sm leading-none text-neutral-600 dark:text-neutral-400">
|
<span className="mt-2.5 text-sm leading-none text-neutral-600 dark:text-neutral-400">
|
||||||
{domain.hostname}
|
{domain.hostname}
|
||||||
</span>
|
</span>
|
||||||
@ -42,29 +42,29 @@ export function LinkPreview({ url }: { url: string }) {
|
|||||||
to={url}
|
to={url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="flex w-full flex-col rounded-lg bg-neutral-100 dark:bg-neutral-900"
|
className="flex flex-col w-full my-1 overflow-hidden rounded-lg bg-neutral-100 dark:bg-neutral-900"
|
||||||
>
|
>
|
||||||
{isImage(data.image) ? (
|
{isImage(data.image) ? (
|
||||||
<img
|
<img
|
||||||
src={data.image}
|
src={data.image}
|
||||||
alt={url}
|
alt={url}
|
||||||
className="h-48 w-full rounded-t-lg bg-white object-cover"
|
className="object-cover w-full h-48 bg-white rounded-t-lg"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex flex-col items-start px-3 py-3">
|
<div className="flex flex-col items-start px-3 py-3">
|
||||||
<div className="flex flex-col items-start gap-1 text-left">
|
<div className="flex flex-col items-start gap-1 text-left">
|
||||||
{data.title ? (
|
{data.title ? (
|
||||||
<div className="break-all text-base font-semibold text-neutral-900 dark:text-neutral-100">
|
<div className="text-base font-semibold break-all text-neutral-900 dark:text-neutral-100">
|
||||||
{data.title}
|
{data.title}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{data.description ? (
|
{data.description ? (
|
||||||
<div className="mb-2 line-clamp-3 break-all text-sm text-neutral-700 dark:text-neutral-400">
|
<div className="mb-2 text-sm break-all line-clamp-3 text-neutral-700 dark:text-neutral-400">
|
||||||
{data.description}
|
{data.description}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="break-all text-sm text-neutral-600 dark:text-neutral-400">
|
<div className="text-sm break-all text-neutral-600 dark:text-neutral-400">
|
||||||
{domain.hostname}
|
{domain.hostname}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,7 @@ export function VideoPreview({ url }: { url: string }) {
|
|||||||
return (
|
return (
|
||||||
<MediaPlayer
|
<MediaPlayer
|
||||||
src={url}
|
src={url}
|
||||||
className="w-full overflow-hidden rounded-lg"
|
className="w-full my-1 overflow-hidden rounded-lg"
|
||||||
aspectRatio="16/9"
|
aspectRatio="16/9"
|
||||||
load="visible"
|
load="visible"
|
||||||
>
|
>
|
||||||
|
@ -94,7 +94,7 @@ const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
|||||||
const sqliteAdapter = await Database.load("sqlite:lume_v2.db");
|
const sqliteAdapter = await Database.load("sqlite:lume_v2.db");
|
||||||
|
|
||||||
const storage = new LumeStorage(sqliteAdapter, platformName);
|
const storage = new LumeStorage(sqliteAdapter, platformName);
|
||||||
storage.init();
|
await storage.init();
|
||||||
|
|
||||||
// check for new update
|
// check for new update
|
||||||
if (storage.settings.autoupdate) {
|
if (storage.settings.autoupdate) {
|
||||||
@ -193,9 +193,9 @@ const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="relative flex h-screen w-screen items-center justify-center bg-neutral-50 dark:bg-neutral-950"
|
className="relative flex items-center justify-center w-screen h-screen bg-neutral-50 dark:bg-neutral-950"
|
||||||
>
|
>
|
||||||
<div className="flex max-w-2xl flex-col items-start gap-1">
|
<div className="flex flex-col items-start max-w-2xl gap-1">
|
||||||
<h5 className="font-semibold uppercase">TIP:</h5>
|
<h5 className="font-semibold uppercase">TIP:</h5>
|
||||||
<Markdown
|
<Markdown
|
||||||
options={{
|
options={{
|
||||||
@ -214,7 +214,7 @@ const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
|||||||
</Markdown>
|
</Markdown>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-5 right-5 inline-flex items-center gap-2.5">
|
<div className="absolute bottom-5 right-5 inline-flex items-center gap-2.5">
|
||||||
<LoaderIcon className="h-6 w-6 animate-spin text-blue-500" />
|
<LoaderIcon className="w-6 h-6 text-blue-500 animate-spin" />
|
||||||
<p className="font-semibold">
|
<p className="font-semibold">
|
||||||
{isNewVersion ? "Found a new version, updating..." : "Starting..."}
|
{isNewVersion ? "Found a new version, updating..." : "Starting..."}
|
||||||
</p>
|
</p>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { SVGProps } from 'react';
|
import { SVGProps } from "react";
|
||||||
|
|
||||||
export function SettingsIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
export function SettingsIcon(
|
||||||
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -12,15 +14,17 @@ export function SettingsIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGEle
|
|||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="1.5"
|
strokeWidth="2"
|
||||||
d="M8.552 5.37l-1.793-.414a1 1 0 00-.932.267l-.604.604a1 1 0 00-.267.932l.414 1.793a1 1 0 01-.42 1.056l-1.755 1.17a1 1 0 00-.445.832v.78a1 1 0 00.445.832l1.755 1.17a1 1 0 01.42 1.056l-.414 1.793a1 1 0 00.267.932l.604.604a1 1 0 00.932.267l1.793-.414a1 1 0 011.056.42l1.17 1.755a1 1 0 00.832.445h.78a1 1 0 00.832-.445l1.17-1.755a1 1 0 011.056-.42l1.793.414a1 1 0 00.932-.267l.604-.604a1 1 0 00.267-.932l-.414-1.793a1 1 0 01.42-1.056l1.755-1.17a1 1 0 00.445-.832v-.78a1 1 0 00-.445-.832l-1.755-1.17a1 1 0 01-.42-1.056l.414-1.793a1 1 0 00-.267-.932l-.604-.604a1 1 0 00-.932-.267l-1.793.414a1 1 0 01-1.056-.42l-1.17-1.755a1 1 0 00-.832-.445h-.78a1 1 0 00-.832.445L9.608 4.95a1 1 0 01-1.056.42z"
|
d="M11 12a1 1 0 112 0 1 1 0 01-2 0z"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="1.5"
|
strokeWidth="2"
|
||||||
d="M14.75 12a2.75 2.75 0 11-5.5 0 2.75 2.75 0 015.5 0z"
|
d="M8.562 4.32c1.252-.723 1.879-1.085 2.544-1.226a4.3 4.3 0 011.788 0c.665.141 1.292.503 2.544 1.225l1.499.865c1.252.723 1.878 1.084 2.334 1.59.403.447.707.974.894 1.546.21.647.21 1.37.21 2.816v1.728c0 1.446 0 2.169-.21 2.816-.186.572-.49 1.1-.894 1.547-.456.505-1.082.866-2.334 1.59l-1.499.864c-1.252.722-1.879 1.084-2.544 1.225a4.299 4.299 0 01-1.788 0c-.665-.141-1.292-.503-2.544-1.225l-1.499-.865c-1.252-.723-1.879-1.084-2.334-1.59a4.296 4.296 0 01-.894-1.546c-.21-.647-.21-1.37-.21-2.816v-1.728c0-1.446 0-2.169.21-2.816.186-.572.49-1.1.894-1.546.455-.506 1.082-.867 2.334-1.59l1.499-.865z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
Account,
|
Account,
|
||||||
|
IColumn,
|
||||||
NDKCacheEvent,
|
NDKCacheEvent,
|
||||||
NDKCacheEventTag,
|
NDKCacheEventTag,
|
||||||
NDKCacheUser,
|
NDKCacheUser,
|
||||||
NDKCacheUserProfile,
|
NDKCacheUserProfile,
|
||||||
WidgetProps,
|
|
||||||
} from "@lume/types";
|
} from "@lume/types";
|
||||||
import { appConfigDir, resolveResource } from "@tauri-apps/api/path";
|
import { appConfigDir, resolveResource } from "@tauri-apps/api/path";
|
||||||
import { invoke } from "@tauri-apps/api/primitives";
|
import { invoke } from "@tauri-apps/api/primitives";
|
||||||
@ -323,7 +323,7 @@ export class LumeStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getWidgets() {
|
public async getWidgets() {
|
||||||
const widgets: Array<WidgetProps> = await this.#db.select(
|
const widgets: Array<IColumn> = await this.#db.select(
|
||||||
"SELECT * FROM widgets WHERE account_id = $1 ORDER BY created_at DESC;",
|
"SELECT * FROM widgets WHERE account_id = $1 ORDER BY created_at DESC;",
|
||||||
[this.account.id],
|
[this.account.id],
|
||||||
);
|
);
|
||||||
@ -341,7 +341,7 @@ export class LumeStorage {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (insert) {
|
if (insert) {
|
||||||
const widgets: Array<WidgetProps> = await this.#db.select(
|
const widgets: Array<IColumn> = await this.#db.select(
|
||||||
"SELECT * FROM widgets ORDER BY id DESC LIMIT 1;",
|
"SELECT * FROM widgets ORDER BY id DESC LIMIT 1;",
|
||||||
);
|
);
|
||||||
if (widgets.length < 1) console.error("get created widget failed");
|
if (widgets.length < 1) console.error("get created widget failed");
|
||||||
@ -351,7 +351,7 @@ export class LumeStorage {
|
|||||||
console.error("create widget failed");
|
console.error("create widget failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async removeWidget(id: string) {
|
public async removeWidget(id: number) {
|
||||||
const res = await this.#db.execute("DELETE FROM widgets WHERE id = $1;", [
|
const res = await this.#db.execute("DELETE FROM widgets WHERE id = $1;", [
|
||||||
id,
|
id,
|
||||||
]);
|
]);
|
||||||
|
2
packages/types/index.d.ts
vendored
2
packages/types/index.d.ts
vendored
@ -34,7 +34,7 @@ export interface WidgetGroupItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IColumn {
|
export interface IColumn {
|
||||||
id?: string;
|
id?: number;
|
||||||
kind: number;
|
kind: number;
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
@ -1,24 +1,23 @@
|
|||||||
import { cn } from "@lume/utils";
|
import { SettingsIcon } from "@lume/icons";
|
||||||
import { type Platform } from "@tauri-apps/plugin-os";
|
import { type Platform } from "@tauri-apps/plugin-os";
|
||||||
import { Outlet, ScrollRestoration } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
import { WindowTitleBar } from "../titlebar";
|
import { WindowTitleBar } from "../titlebar";
|
||||||
|
|
||||||
export function AuthLayout({ platform }: { platform: Platform }) {
|
export function AuthLayout({ platform }: { platform: Platform }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex flex-col w-screen h-screen bg-black">
|
||||||
className={cn(
|
|
||||||
"flex h-screen w-screen flex-col",
|
|
||||||
platform !== "macos" ? "bg-neutral-50 dark:bg-neutral-950" : "",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{platform !== "macos" ? (
|
{platform !== "macos" ? (
|
||||||
<WindowTitleBar platform={platform} />
|
<WindowTitleBar platform={platform} />
|
||||||
) : (
|
) : (
|
||||||
<div data-tauri-drag-region className="h-9 shrink-0" />
|
<div data-tauri-drag-region className="h-9 shrink-0" />
|
||||||
)}
|
)}
|
||||||
<div className="h-full w-full">
|
<div className="relative w-full h-full">
|
||||||
|
<div className="absolute top-0 right-9">
|
||||||
|
<div className="inline-flex items-center justify-center rounded-lg size-10 bg-neutral-950 group hover:bg-neutral-900">
|
||||||
|
<SettingsIcon className="size-6 text-neutral-700 group-hover:text-neutral-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<ScrollRestoration />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -472,6 +472,9 @@ importers:
|
|||||||
'@lume/tsconfig':
|
'@lume/tsconfig':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../tsconfig
|
version: link:../../tsconfig
|
||||||
|
'@lume/types':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../types
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^18.2.46
|
specifier: ^18.2.46
|
||||||
version: 18.2.46
|
version: 18.2.46
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{
|
{
|
||||||
"width": 1080,
|
"width": 1080,
|
||||||
"height": 800,
|
"height": 800,
|
||||||
"minWidth": 560,
|
"minWidth": 1080,
|
||||||
"minHeight": 800,
|
"minHeight": 800,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "Lume",
|
"title": "Lume",
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{
|
{
|
||||||
"width": 1080,
|
"width": 1080,
|
||||||
"height": 800,
|
"height": 800,
|
||||||
"minWidth": 560,
|
"minWidth": 1080,
|
||||||
"minHeight": 800,
|
"minHeight": 800,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "Lume",
|
"title": "Lume",
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{
|
{
|
||||||
"width": 1080,
|
"width": 1080,
|
||||||
"height": 800,
|
"height": 800,
|
||||||
"minWidth": 560,
|
"minWidth": 1080,
|
||||||
"minHeight": 800,
|
"minHeight": 800,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "Lume",
|
"title": "Lume",
|
||||||
|
Loading…
Reference in New Issue
Block a user