adding new post page and button on home page
This commit is contained in:
parent
d454b337b9
commit
256d76dbca
@ -36,11 +36,12 @@ const LoginModal = dynamic(() => import("@/components/Modals/Login"), {
|
|||||||
export default function AuthActions() {
|
export default function AuthActions() {
|
||||||
const modal = useModal();
|
const modal = useModal();
|
||||||
const { currentUser, logout, attemptLogin } = useCurrentUser();
|
const { currentUser, logout, attemptLogin } = useCurrentUser();
|
||||||
|
const { ndk } = useNDK();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentUser) {
|
if (ndk && !currentUser) {
|
||||||
void attemptLogin();
|
void attemptLogin();
|
||||||
}
|
}
|
||||||
}, []);
|
}, [ndk]);
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -27,7 +27,6 @@ import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
|
|||||||
import { NOTABLE_ACCOUNTS } from "@/constants";
|
import { NOTABLE_ACCOUNTS } from "@/constants";
|
||||||
import { type NDKKind } from "@nostr-dev-kit/ndk";
|
import { type NDKKind } from "@nostr-dev-kit/ndk";
|
||||||
import { uniqBy } from "ramda";
|
import { uniqBy } from "ramda";
|
||||||
import useProfile from "@/lib/hooks/useProfile";
|
|
||||||
import ListCard from "@/components/ListCard";
|
import ListCard from "@/components/ListCard";
|
||||||
|
|
||||||
export default function FeaturedLists() {
|
export default function FeaturedLists() {
|
||||||
@ -66,7 +65,6 @@ export default function FeaturedLists() {
|
|||||||
<SectionHeader>
|
<SectionHeader>
|
||||||
<div className="center gap-x-2">
|
<div className="center gap-x-2">
|
||||||
<SectionTitle>Featured Lists</SectionTitle>
|
<SectionTitle>Featured Lists</SectionTitle>
|
||||||
{processedEvents.length}
|
|
||||||
</div>
|
</div>
|
||||||
<Button variant={"ghost"}>
|
<Button variant={"ghost"}>
|
||||||
View all <RiArrowRightLine className="ml-1 h-4 w-4" />
|
View all <RiArrowRightLine className="ml-1 h-4 w-4" />
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
import Link from "next/link";
|
||||||
import ExploreCreators from "./_sections/ExploreCreators";
|
import ExploreCreators from "./_sections/ExploreCreators";
|
||||||
import LongFormContentSection from "./_sections/LongFormContent";
|
import LongFormContentSection from "./_sections/LongFormContent";
|
||||||
import BecomeACreator from "./_sections/BecomeACreator";
|
import BecomeACreator from "./_sections/BecomeACreator";
|
||||||
|
import { RiAddFill } from "react-icons/ri";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
const LiveStreamingSection = dynamic(
|
const LiveStreamingSection = dynamic(
|
||||||
() => import("./_sections/LiveStreaming"),
|
() => import("./_sections/LiveStreaming"),
|
||||||
{
|
{
|
||||||
@ -23,6 +25,13 @@ export default function Page() {
|
|||||||
<BecomeACreator />
|
<BecomeACreator />
|
||||||
<LiveStreamingSection />
|
<LiveStreamingSection />
|
||||||
<FeaturedListsSection />
|
<FeaturedListsSection />
|
||||||
|
<div className="z-overlay- fixed bottom-[calc(var(--bottom-nav-height)_+_20px)] right-[15px] sm:hidden">
|
||||||
|
<Link href="/article/new">
|
||||||
|
<Button size={"icon"} className="h-[50px] w-[50px]">
|
||||||
|
<RiAddFill className="h-[32px] w-[32px]" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
9
app/(app)/article/new/layout.tsx
Normal file
9
app/(app)/article/new/layout.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ReactElement, ReactNode } from "react";
|
||||||
|
|
||||||
|
export default function ModalLayout(props: { children: ReactElement }) {
|
||||||
|
return (
|
||||||
|
<div className="z-overlay fixed inset-y-[10px] left-[10px] right-[10px] overflow-hidden overflow-y-auto rounded-lg border bg-background px-4 sm:left-[calc(10px_+_var(--sidebar-closed-width))] xl:left-[calc(10px_+_var(--sidebar-open-width))]">
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
23
app/(app)/article/new/page.tsx
Normal file
23
app/(app)/article/new/page.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import Editor_ from "@/containers/Article/Editor";
|
||||||
|
import Editor from "@/components/LongForm/Editor";
|
||||||
|
import { useNDK } from "@nostr-dev-kit/ndk-react";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
import Spinner from "@/components/spinner";
|
||||||
|
import useEvents from "@/lib/hooks/useEvents";
|
||||||
|
|
||||||
|
export default function EditorPage({
|
||||||
|
params: { key },
|
||||||
|
}: {
|
||||||
|
params: {
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
{/* <Editor /> */}
|
||||||
|
<Editor_ />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -7,16 +7,24 @@ import "@blocknote/core/style.css";
|
|||||||
|
|
||||||
interface EditorProps {
|
interface EditorProps {
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
|
onContentChange: (text: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Editor = ({ editable }: EditorProps) => {
|
const Editor = ({ editable, onContentChange }: EditorProps) => {
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
const [content, setContent] = useState("");
|
|
||||||
|
|
||||||
const editor: BlockNoteEditor = useBlockNote({
|
const editor: BlockNoteEditor = useBlockNote({
|
||||||
editable,
|
editable,
|
||||||
onEditorContentChange: (editor) => {
|
onEditorContentChange: (editor) => {
|
||||||
setContent(JSON.stringify(editor.topLevelBlocks, null, 2));
|
// Converts the editor's contents from Block objects to Markdown and
|
||||||
|
// saves them.
|
||||||
|
const saveBlocksAsMarkdown = async () => {
|
||||||
|
const markdown: string = await editor.blocksToMarkdown(
|
||||||
|
editor.topLevelBlocks,
|
||||||
|
);
|
||||||
|
onContentChange(markdown);
|
||||||
|
};
|
||||||
|
saveBlocksAsMarkdown();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
149
components/LongForm/ToolBar.tsx
Normal file
149
components/LongForm/ToolBar.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
import { Textarea } from "../ui/textarea";
|
||||||
|
import useAutosizeTextArea from "@/lib/hooks/useAutoSizeTextArea";
|
||||||
|
import { HiOutlinePhoto } from "react-icons/hi2";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface ToolbarProps {
|
||||||
|
initialData?: {
|
||||||
|
title?: string;
|
||||||
|
summary?: string;
|
||||||
|
image?: string;
|
||||||
|
};
|
||||||
|
preview?: boolean;
|
||||||
|
onSubmit: ({
|
||||||
|
title,
|
||||||
|
summary,
|
||||||
|
image,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
image?: string;
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Toolbar = ({ initialData, preview, onSubmit }: ToolbarProps) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [title, setTitle] = useState(initialData?.title ?? "");
|
||||||
|
const [summary, setSummary] = useState(initialData?.summary ?? "");
|
||||||
|
const [image, setImage] = useState(initialData?.image);
|
||||||
|
const titleRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const summaryRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
useAutosizeTextArea(titleRef.current, title);
|
||||||
|
useAutosizeTextArea(summaryRef.current, summary);
|
||||||
|
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||||
|
const [isEditingSummary, setIsEditingSummary] = useState(false);
|
||||||
|
const [value, setValue] = useState(initialData?.title);
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
console.log("Update");
|
||||||
|
};
|
||||||
|
|
||||||
|
const enableInput = (type: "title" | "summary") => {
|
||||||
|
if (preview) return;
|
||||||
|
if (type === "title") {
|
||||||
|
setIsEditingTitle(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
titleRef.current?.focus();
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
summaryRef.current?.focus();
|
||||||
|
setIsEditingSummary(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
summaryRef.current?.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableInput = () => {
|
||||||
|
setIsEditingTitle(false);
|
||||||
|
setIsEditingSummary(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await onSubmit({ title, summary, image });
|
||||||
|
setLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Error", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
disableInput();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group relative">
|
||||||
|
<div className="flex items-center gap-x-1 py-4">
|
||||||
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
{!initialData?.image && !preview && (
|
||||||
|
<Button
|
||||||
|
className="text-xs text-muted-foreground"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<HiOutlinePhoto className="mr-2 h-4 w-4" />
|
||||||
|
Add cover
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button loading={loading} onClick={handleSubmit} className="ml-auto">
|
||||||
|
Publish
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isEditingTitle && !preview ? (
|
||||||
|
<Textarea
|
||||||
|
ref={titleRef}
|
||||||
|
onBlur={disableInput}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
placeholder="Untitled"
|
||||||
|
className={cn(
|
||||||
|
"resize-none break-words border-0 bg-transparent p-0 text-5xl font-bold text-foreground shadow-none outline-none focus-visible:ring-0",
|
||||||
|
title === "" && "max-h-[60px]",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
onClick={() => enableInput("title")}
|
||||||
|
className="break-words pb-[11.5px] text-5xl font-bold text-foreground outline-none"
|
||||||
|
>
|
||||||
|
{title || "Untitled"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isEditingSummary && !preview ? (
|
||||||
|
<Textarea
|
||||||
|
ref={summaryRef}
|
||||||
|
onBlur={disableInput}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
value={summary}
|
||||||
|
onChange={(e) => setSummary(e.target.value)}
|
||||||
|
placeholder="Write a short summary of your content..."
|
||||||
|
className={cn(
|
||||||
|
"min-h-0 resize-none break-words border-0 bg-transparent p-0 text-base text-foreground shadow-none outline-none focus-visible:ring-0",
|
||||||
|
summary === "" && "!max-h-[35.5px]",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
onClick={() => enableInput("summary")}
|
||||||
|
className="break-words pb-[11.5px] text-base text-muted-foreground/80 outline-none"
|
||||||
|
>
|
||||||
|
{summary || "Write a short summary of your content..."}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -4,18 +4,22 @@ import dynamic from "next/dynamic";
|
|||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { BlockNoteEditor, Block } from "@blocknote/core";
|
import { BlockNoteEditor, Block } from "@blocknote/core";
|
||||||
import { BlockNoteView, useBlockNote } from "@blocknote/react";
|
import { BlockNoteView, useBlockNote } from "@blocknote/react";
|
||||||
import Spinner from "../spinner";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { Toolbar } from "./ToolBar";
|
||||||
|
|
||||||
type MarkdoneProps = {
|
type MarkdoneProps = {
|
||||||
content?: string;
|
content?: string;
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
};
|
};
|
||||||
export default function Markdown({ content }: MarkdoneProps) {
|
export default function Markdown({ content, editable = false }: MarkdoneProps) {
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const editor: BlockNoteEditor = useBlockNote({
|
const editor: BlockNoteEditor = useBlockNote({
|
||||||
editable: false,
|
editable,
|
||||||
|
onEditorContentChange: (e) => {
|
||||||
|
console.log("EDITOR CHANGE", e);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -38,8 +42,11 @@ export default function Markdown({ content }: MarkdoneProps) {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="center py-20 text-primary">
|
<div className="space-y-4 pl-8 pt-5">
|
||||||
<Spinner />
|
<Skeleton className="h-14 w-[50%]" />
|
||||||
|
<Skeleton className="h-4 w-[80%]" />
|
||||||
|
<Skeleton className="h-4 w-[40%]" />
|
||||||
|
<Skeleton className="h-4 w-[60%]" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
69
containers/Article/Editor.tsx
Normal file
69
containers/Article/Editor.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState } from "react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { RiCloseFill } from "react-icons/ri";
|
||||||
|
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { formatDate } from "@/lib/utils/dates";
|
||||||
|
import Actions from "./Actions";
|
||||||
|
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||||
|
import { getTagAllValues, getTagValues } from "@/lib/nostr/utils";
|
||||||
|
import Editor from "@/components/LongForm/Editor";
|
||||||
|
import { Toolbar } from "@/components/LongForm/ToolBar";
|
||||||
|
type ArticleProps = {
|
||||||
|
event?: NDKEvent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EditorPage({ event }: ArticleProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
async function handleSubmit({
|
||||||
|
title,
|
||||||
|
summary,
|
||||||
|
image,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
image?: string;
|
||||||
|
}) {
|
||||||
|
console.log("Writing", title, summary, image, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative @container">
|
||||||
|
<div className="sticky inset-x-0 top-0 z-10 flex items-center justify-between border-b bg-background pb-4 pt-4">
|
||||||
|
<div className="center gap-x-3">
|
||||||
|
<span className="text-xs uppercase text-muted-foreground">
|
||||||
|
New Long Form
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (sessionStorage.getItem("RichHistory")) {
|
||||||
|
void router.back();
|
||||||
|
} else {
|
||||||
|
void router.push("/app");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
size="icon"
|
||||||
|
variant={"outline"}
|
||||||
|
className=""
|
||||||
|
>
|
||||||
|
<RiCloseFill className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="h-[20px] w-full"></div>
|
||||||
|
<article className="relative mx-auto -mt-5 max-w-3xl">
|
||||||
|
<div className="pb-4 pl-[54px]">
|
||||||
|
<Toolbar onSubmit={handleSubmit} />
|
||||||
|
</div>
|
||||||
|
<Editor
|
||||||
|
editable={true}
|
||||||
|
onContentChange={(content) => setContent(content)}
|
||||||
|
/>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -3,7 +3,7 @@ import { useEffect } from "react";
|
|||||||
// Updates the height of a <textarea> when the value changes.
|
// Updates the height of a <textarea> when the value changes.
|
||||||
const useAutosizeTextArea = (
|
const useAutosizeTextArea = (
|
||||||
textAreaRef: HTMLTextAreaElement | null,
|
textAreaRef: HTMLTextAreaElement | null,
|
||||||
value: string
|
value: string,
|
||||||
) => {
|
) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (textAreaRef) {
|
if (textAreaRef) {
|
||||||
@ -13,6 +13,7 @@ const useAutosizeTextArea = (
|
|||||||
|
|
||||||
// We then set the height directly, outside of the render loop
|
// We then set the height directly, outside of the render loop
|
||||||
// Trying to set this with state or a ref will product an incorrect value.
|
// Trying to set this with state or a ref will product an incorrect value.
|
||||||
|
console.log("setting h", scrollHeight);
|
||||||
textAreaRef.style.height = `${scrollHeight}px`;
|
textAreaRef.style.height = `${scrollHeight}px`;
|
||||||
}
|
}
|
||||||
}, [textAreaRef, value]);
|
}, [textAreaRef, value]);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user