diff --git a/app/(app)/_layout/index.tsx b/app/(app)/_layout/index.tsx index febc9b4..9d40eb1 100644 --- a/app/(app)/_layout/index.tsx +++ b/app/(app)/_layout/index.tsx @@ -15,8 +15,8 @@ export default function AppLayout({ children }: { children: React.ReactNode }) { {/* Sidebar */} -
-
{children}
+
+
{children}
{/* Mobile Banner */} diff --git a/app/(app)/app/page.tsx b/app/(app)/app/page.tsx index 6102963..224f8ab 100644 --- a/app/(app)/app/page.tsx +++ b/app/(app)/app/page.tsx @@ -1,16 +1,34 @@ +import { Button } from "@/components/ui/button"; import HorizontalCarousel from "./_sections/HorizontalCarousel"; - +import { RiArrowRightLine } from "react-icons/ri"; +import LongFormContentCard from "@/components/LongFormContentCard"; export default function Page() { return ( -
-
-
-

+
+
+
+

Explore Creators

+
-
+ +
+
+

+ Long form content +

+ +
+
+ +
+

); } diff --git a/app/(app)/article/[eventId]/layout.tsx b/app/(app)/article/[eventId]/layout.tsx new file mode 100644 index 0000000..cf6f137 --- /dev/null +++ b/app/(app)/article/[eventId]/layout.tsx @@ -0,0 +1,9 @@ +import { ReactElement } from "react"; + +export default function ModalLayout({ children }: { children: ReactElement }) { + return ( +
+ {children} +
+ ); +} diff --git a/app/(app)/article/[eventId]/page.tsx b/app/(app)/article/[eventId]/page.tsx new file mode 100644 index 0000000..d9196f3 --- /dev/null +++ b/app/(app)/article/[eventId]/page.tsx @@ -0,0 +1,63 @@ +"use client"; +import { useMemo } from "react"; +import dynamic from "next/dynamic"; +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"; +const Viewer = dynamic(() => import("@/components/LongForm/Viewer"), { + ssr: false, +}); + +export default function ArticlePage({ + params: { eventId }, +}: { + params: { + eventId: string; + }; +}) { + const router = useRouter(); + const markdown = `This is a test +### test text + +- First +- Second +1 nest`; + + return ( +
+
+
+ + + SC + + + Derek Seivers + +
+ +
+
+ +
+ ); +} diff --git a/bun.lockb b/bun.lockb index ee16105..0981f15 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/LongForm/Editor.tsx b/components/LongForm/Editor.tsx new file mode 100644 index 0000000..3ae37ba --- /dev/null +++ b/components/LongForm/Editor.tsx @@ -0,0 +1,65 @@ +"use client"; +import { useEffect, useState } from "react"; +import { useTheme } from "next-themes"; +import { BlockNoteEditor, PartialBlock, Block } from "@blocknote/core"; +import { BlockNoteView, useBlockNote } from "@blocknote/react"; +import Spinner from "../spinner"; +import "@blocknote/core/style.css"; + +interface EditorProps { + onChange: (value: string) => void; + initialMarkdown?: string; + editable?: boolean; +} + +const Editor = ({ onChange, initialMarkdown, editable }: EditorProps) => { + const { resolvedTheme } = useTheme(); + const [loading, setLoading] = useState(true); + const [initialContent, setInitialContent] = useState(); + + const editor: BlockNoteEditor = useBlockNote({ + editable, + initialContent: initialContent, + onEditorContentChange: (editor) => { + onChange(JSON.stringify(editor.topLevelBlocks, null, 2)); + }, + }); + + useEffect(() => { + if (editor) { + if (!initialContent && initialMarkdown) { + // Whenever the current Markdown content changes, converts it to an array + // of Block objects and replaces the editor's content with them. + const getBlocks = async () => { + const blocks: Block[] = + await editor.markdownToBlocks(initialMarkdown); + setInitialContent(blocks); + editor.replaceBlocks(editor.topLevelBlocks, blocks); + setLoading(false); + }; + void getBlocks(); + } else if (loading) { + setLoading(false); + } + } + }, [editor]); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ ); +}; + +export default Editor; diff --git a/components/LongForm/Viewer.tsx b/components/LongForm/Viewer.tsx new file mode 100644 index 0000000..e0b4730 --- /dev/null +++ b/components/LongForm/Viewer.tsx @@ -0,0 +1,63 @@ +"use client"; +import { useEffect, useState } from "react"; +import { useTheme } from "next-themes"; +import { BlockNoteEditor, PartialBlock, Block } from "@blocknote/core"; +import { BlockNoteView, useBlockNote } from "@blocknote/react"; +import Spinner from "../spinner"; +import "@blocknote/core/style.css"; + +interface EditorProps { + initialMarkdown?: string; +} + +const Viewer = ({ initialMarkdown }: EditorProps) => { + const { resolvedTheme } = useTheme(); + const [loading, setLoading] = useState(true); + const [initialContent, setInitialContent] = useState(); + + const editor: BlockNoteEditor = useBlockNote({ + editable: false, + initialContent: initialContent, + }); + + useEffect(() => { + console.log("ERFE", editor); + if (editor) { + if (!initialContent && initialMarkdown) { + console.log("initial md", initialMarkdown); + // Whenever the current Markdown content changes, converts it to an array + // of Block objects and replaces the editor's content with them. + const getBlocks = async () => { + const blocks: Block[] = + await editor.markdownToBlocks(initialMarkdown); + setInitialContent(blocks); + console.log("Blocks", blocks); + // editor.replaceBlocks(editor.topLevelBlocks, blocks); + setLoading(false); + }; + void getBlocks(); + } else if (loading) { + setLoading(false); + } + } + }, [editor]); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ ); +}; + +export default Viewer; diff --git a/components/LongFormContentCard/index.tsx b/components/LongFormContentCard/index.tsx new file mode 100644 index 0000000..8d3399e --- /dev/null +++ b/components/LongFormContentCard/index.tsx @@ -0,0 +1,83 @@ +"use client"; +import Image from "next/image"; +import Link from "next/link"; +import { RiMoreFill } from "react-icons/ri"; +import { HiOutlineLightningBolt } from "react-icons/hi"; +import { + HiOutlineHandThumbUp, + HiOutlineChatBubbleLeftEllipsis, + HiOutlineEllipsisHorizontal, +} from "react-icons/hi2"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar"; +import { formatDate } from "@/lib/utils/dates"; +import { Button } from "../ui/button"; + +type CreatorCardProps = { + displayName: string; + about: string; + picture: string; + banner: string; +}; + +export default function LongFormContentCard() { + return ( + + +
+ + + SC + + + Derek Seivers + +
+
+ {formatDate(new Date("10-5-23"), "MMM Do")} + +
+
+ + + The start of the Nostr revolution + + + This is the summary of this artilce. Let's hope that it is a good + article and that it will end up being worth reading. I don't want to + waste my time on some random other stuff. + +
+
+
+ + +
+ +
+
+
+
+ ); +} diff --git a/components/spinner.tsx b/components/spinner.tsx new file mode 100644 index 0000000..8375f73 --- /dev/null +++ b/components/spinner.tsx @@ -0,0 +1,26 @@ +const Spinner = () => { + return ( + + + + + ); +}; + +export default Spinner; diff --git a/lib/utils/dates.ts b/lib/utils/dates.ts index 7939775..8cb9a44 100644 --- a/lib/utils/dates.ts +++ b/lib/utils/dates.ts @@ -1,73 +1,81 @@ import dayjs from "dayjs"; import relative from "dayjs/plugin/relativeTime"; import updateLocale from "dayjs/plugin/updateLocale"; +import advancedFormat from "dayjs/plugin/advancedFormat"; +import timezone from "dayjs/plugin/timezone"; + export function relativeTimeUnix(timestamp: number) { - const config = { - thresholds: [ - { l: "s", r: 1 }, - { l: "m", r: 1 }, - { l: "mm", r: 59, d: "minute" }, - { l: "h", r: 1 }, - { l: "hh", r: 23, d: "hour" }, - { l: "d", r: 1 }, - { l: "dd", r: 364, d: "day" }, - { l: "y", r: 1 }, - { l: "yy", d: "year" }, - ], - rounding: Math.floor, - }; - dayjs.extend(updateLocale); - - dayjs.updateLocale("en", { - relativeTime: { - future: "in %s", - past: "%s ago", - s: "%s seconds", - m: "1 min", - mm: "%d mins", - h: "1 hour", - hh: "%d hours", - d: "1 day", - dd: "%d days", - y: "1 year", - yy: "%d years", - }, - }); - dayjs.extend(relative, config); - return dayjs(timestamp * 1000).fromNow(); - } - export function relativeTime(timestamp: Date) { - const config = { - thresholds: [ - { l: "s", r: 1 }, - { l: "m", r: 1 }, - { l: "mm", r: 59, d: "minute" }, - { l: "h", r: 1 }, - { l: "hh", r: 23, d: "hour" }, - { l: "d", r: 1 }, - { l: "dd", r: 364, d: "day" }, - { l: "y", r: 1 }, - { l: "yy", d: "year" }, - ], - rounding: Math.floor, - }; - dayjs.extend(updateLocale); - - dayjs.updateLocale("en", { - relativeTime: { - future: "in %s", - past: "%s ago", - s: "%s seconds", - m: "1 min", - mm: "%d mins", - h: "1 hour", - hh: "%d hours", - d: "1 day", - dd: "%d days", - y: "1 year", - yy: "%d years", - }, - }); - dayjs.extend(relative, config); - return dayjs(timestamp).fromNow(); - } \ No newline at end of file + const config = { + thresholds: [ + { l: "s", r: 1 }, + { l: "m", r: 1 }, + { l: "mm", r: 59, d: "minute" }, + { l: "h", r: 1 }, + { l: "hh", r: 23, d: "hour" }, + { l: "d", r: 1 }, + { l: "dd", r: 364, d: "day" }, + { l: "y", r: 1 }, + { l: "yy", d: "year" }, + ], + rounding: Math.floor, + }; + dayjs.extend(updateLocale); + + dayjs.updateLocale("en", { + relativeTime: { + future: "in %s", + past: "%s ago", + s: "%s seconds", + m: "1 min", + mm: "%d mins", + h: "1 hour", + hh: "%d hours", + d: "1 day", + dd: "%d days", + y: "1 year", + yy: "%d years", + }, + }); + dayjs.extend(relative, config); + return dayjs(timestamp * 1000).fromNow(); +} +export function relativeTime(timestamp: Date) { + const config = { + thresholds: [ + { l: "s", r: 1 }, + { l: "m", r: 1 }, + { l: "mm", r: 59, d: "minute" }, + { l: "h", r: 1 }, + { l: "hh", r: 23, d: "hour" }, + { l: "d", r: 1 }, + { l: "dd", r: 364, d: "day" }, + { l: "y", r: 1 }, + { l: "yy", d: "year" }, + ], + rounding: Math.floor, + }; + dayjs.extend(updateLocale); + + dayjs.updateLocale("en", { + relativeTime: { + future: "in %s", + past: "%s ago", + s: "%s seconds", + m: "1 min", + mm: "%d mins", + h: "1 hour", + hh: "%d hours", + d: "1 day", + dd: "%d days", + y: "1 year", + yy: "%d years", + }, + }); + dayjs.extend(relative, config); + return dayjs(timestamp).fromNow(); +} +export function formatDate(timestamp: Date, format?: string) { + dayjs.extend(advancedFormat); + dayjs.extend(timezone); + return dayjs(timestamp).format(format ?? "MMMM Do, YYYY"); +} diff --git a/package.json b/package.json index 511fe7e..09c5247 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "lint": "next lint" }, "dependencies": { + "@blocknote/core": "^0.9.5", + "@blocknote/react": "^0.9.5", "@hookform/resolvers": "^3.3.2", "@noble/hashes": "^1.3.2", "@nostr-dev-kit/ndk": "^2.0.0", @@ -29,6 +31,7 @@ "focus-trap-react": "^10.2.3", "framer-motion": "^10.16.4", "next": "13.5.4", + "next-themes": "^0.2.1", "node-html-parser": "^6.1.10", "nostr-tools": "^1.16.0", "ramda": "^0.29.1", @@ -45,6 +48,7 @@ "zustand": "^4.4.3" }, "devDependencies": { + "@tailwindcss/typography": "^0.5.10", "@types/crypto-js": "^4.1.2", "@types/node": "^20", "@types/ramda": "^0.29.6", diff --git a/tailwind.config.ts b/tailwind.config.ts index 3d9b16b..e49271a 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -84,16 +84,20 @@ module.exports = { 80: 80, 90: 90, 99: 99, - mobileTabs: 980, - "header-": 989, - header: 990, - "header+": 991, - headerDialog: 991, - "modal-": 995, - modal: 996, - "modal+": 997, - "top-": 998, - top: 999, + mobileTabs: 900, + "header-": 919, + header: 920, + "header+": 921, + headerDialog: 922, + "overlay-": 929, + overlay: 930, + "overlay+": 931, + "modal-": 939, + modal: 940, + "modal+": 941, + "top-": 949, + top: 950, + "top+": 951, }, flex: { 2: 2, @@ -106,5 +110,6 @@ module.exports = { require("tailwindcss-animate"), require("@tailwindcss/container-queries"), require("tailwind-scrollbar"), + require("@tailwindcss/typography"), ], }; diff --git a/test.md b/test.md new file mode 100644 index 0000000..c03243e --- /dev/null +++ b/test.md @@ -0,0 +1,221 @@ +# h1 Heading 8-) + +## h2 Heading + +### h3 Heading + +#### h4 Heading + +##### h5 Heading + +###### h6 Heading + +## Horizontal Rules + +--- + +--- + +--- + +## Typographic replacements + +Enable typographer option to see result. + +(c) (C) (r) (R) (tm) (TM) (p) (P) +- + +test.. test... test..... test?..... test!.... + +!!!!!! ???? ,, -- --- + +"Smartypants, double quotes" and 'single quotes' + +## Emphasis + +**This is bold text** + +**This is bold text** + +_This is italic text_ + +_This is italic text_ + +~~Strikethrough~~ + +## Blockquotes + +> Blockquotes can also be nested... +> +> > ...by using additional greater-than signs right next to each other... +> > +> > > ...or with spaces between arrows. + +## Lists + +Unordered + +- Create a list by starting a line with `+`, `-`, or `*` +- Sub-lists are made by indenting 2 spaces: + - Marker character change forces new list start: + - Ac tristique libero volutpat at + * Facilisis in pretium nisl aliquet + - Nulla volutpat aliquam velit +- Very easy! + +Ordered + +1. Lorem ipsum dolor sit amet +2. Consectetur adipiscing elit +3. Integer molestie lorem at massa + +4. You can use sequential numbers... +5. ...or keep all the numbers as `1.` + +Start numbering with offset: + +57. foo +1. bar + +## Code + +Inline `code` + +Indented code + + // Some comments + line 1 of code + line 2 of code + line 3 of code + +Block code "fences" + +``` +Sample text here... +``` + +Syntax highlighting + +```js +var foo = function (bar) { + return bar++; +}; + +console.log(foo(5)); +``` + +## Tables + +| Option | Description | +| ------ | ------------------------------------------------------------------------- | +| data | path to data files to supply the data that will be passed into templates. | +| engine | engine to be used for processing templates. Handlebars is the default. | +| ext | extension to be used for dest files. | + +Right aligned columns + +| Option | Description | +| -----: | ------------------------------------------------------------------------: | +| data | path to data files to supply the data that will be passed into templates. | +| engine | engine to be used for processing templates. Handlebars is the default. | +| ext | extension to be used for dest files. | + +## Links + +[link text](http://dev.nodeca.com) + +[link with title](http://nodeca.github.io/pica/demo/ "title text!") + +Autoconverted link https://github.com/nodeca/pica (enable linkify to see) + +## Images + +![Minion](https://octodex.github.com/images/minion.png) +![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat") + +Like links, Images also have a footnote style syntax + +![Alt text][id] + +With a reference later in the document defining the URL location: + +[id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat" + +## Plugins + +The killer feature of `markdown-it` is very effective support of +[syntax plugins](https://www.npmjs.org/browse/keyword/markdown-it-plugin). + +### [Emojies](https://github.com/markdown-it/markdown-it-emoji) + +> Classic markup: :wink: :crush: :cry: :tear: :laughing: :yum: +> +> Shortcuts (emoticons): :-) :-( 8-) ;) + +see [how to change output](https://github.com/markdown-it/markdown-it-emoji#change-output) with twemoji. + +### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup) + +- 19^th^ +- H~2~O + +### [\](https://github.com/markdown-it/markdown-it-ins) + +++Inserted text++ + +### [\](https://github.com/markdown-it/markdown-it-mark) + +==Marked text== + +### [Footnotes](https://github.com/markdown-it/markdown-it-footnote) + +Footnote 1 link[^first]. + +Footnote 2 link[^second]. + +Inline footnote^[Text of inline footnote] definition. + +Duplicated footnote reference[^second]. + +[^first]: Footnote **can have markup** + + and multiple paragraphs. + +[^second]: Footnote text. + +### [Definition lists](https://github.com/markdown-it/markdown-it-deflist) + +Term 1 + +: Definition 1 +with lazy continuation. + +Term 2 with _inline markup_ + +: Definition 2 + + { some code, part of Definition 2 } + + Third paragraph of definition 2. + +_Compact style:_ + +Term 1 +~ Definition 1 + +Term 2 +~ Definition 2a +~ Definition 2b + +### [Abbreviations](https://github.com/markdown-it/markdown-it-abbr) + +This is HTML abbreviation example. + +It converts "HTML", but keep intact partial entries like "xxxHTMLyyy" and so on. + +\*[HTML]: Hyper Text Markup Language + +### [Custom containers](https://github.com/markdown-it/markdown-it-container) + +::: warning +_here be dragons_ +:::