diff --git a/app/(app)/saved/_client.tsx b/app/(app)/saved/_client.tsx index f6c41607..f6e0c4ec 100644 --- a/app/(app)/saved/_client.tsx +++ b/app/(app)/saved/_client.tsx @@ -8,10 +8,11 @@ import ArticleLoading from "@/components/ArticlePreview/ArticleLoading"; const SavedPosts = () => { const { - data: bookmarks, + data: bookmarksData, refetch, status: bookmarkStatus, - } = api.post.myBookmarks.useQuery(); + } = api.post.myBookmarks.useQuery({}); + const bookmarks = bookmarksData?.bookmarks || []; const { mutate: bookmark } = api.post.bookmark.useMutation({ onSettled() { diff --git a/components/ArticlePreview/ArticlePreview.tsx b/components/ArticlePreview/ArticlePreview.tsx index 65f59525..71c2009c 100644 --- a/components/ArticlePreview/ArticlePreview.tsx +++ b/components/ArticlePreview/ArticlePreview.tsx @@ -17,6 +17,7 @@ import { } from "@headlessui/react"; import { api } from "@/server/trpc/react"; import { signIn, useSession } from "next-auth/react"; +import { toast } from "sonner"; type ButtonOptions = { label: string; @@ -55,8 +56,12 @@ const ArticlePreview: NextPage = ({ bookmarkedInitialState = false, }) => { const [bookmarked, setIsBookmarked] = useState(bookmarkedInitialState); + const howManySavedToShow = 3; + const { data: bookmarksData, refetch } = api.post.myBookmarks.useQuery({ + limit: howManySavedToShow, + }); const { data: session } = useSession(); - + const bookmarks = bookmarksData?.bookmarks; const dateTime = Temporal.Instant.from(new Date(date).toISOString()); const readableDate = dateTime.toLocaleString(["en-IE"], { year: "numeric", @@ -69,6 +74,13 @@ const ArticlePreview: NextPage = ({ onSettled() { setIsBookmarked((isBookmarked) => !isBookmarked); }, + onSuccess: () => { + refetch(); + }, + onError: (error) => { + toast.error("Failed to update bookmark"); + Sentry.captureException(error); + }, }); const bookmarkPost = async (postId: string, setBookmarked = true) => { diff --git a/components/Hero/Hero.tsx b/components/Hero/Hero.tsx index 06c834a6..b7162aec 100644 --- a/components/Hero/Hero.tsx +++ b/components/Hero/Hero.tsx @@ -1,140 +1,242 @@ "use client"; -import { useState } from "react"; -import "atropos/css"; -import Atropos from "atropos/react"; -import Image from "next/image"; -import space from "public/images/home/space.jpg"; -import rocketman from "public/images/home/rocketman.png"; -import moon from "public/images/home/moon.png"; +import React, { useState, useEffect } from "react"; -export default function Hero() { - const [rocketLoaded, setRocketLoaded] = useState(false); - const [moonLoaded, setMoonLoaded] = useState(false); - const [starsLoaded, setStarsLoaded] = useState(false); - - const isReady = rocketLoaded && moonLoaded && starsLoaded; +const CoduLogo = ({ className }: { className?: string }) => { + return ( + + + + + + + ); +}; - const handleScroll = (id: string) => { - const element = document.getElementById(id); - if (element) { - element.scrollIntoView({ behavior: "smooth" }); +const NightSky = () => { + const styles = ` + @keyframes twinkle { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 0.7; } + } + @keyframes gentleMove1 { + 0%, 100% { transform: translate(0, 0); } + 50% { transform: translate(3px, 2px); } + } + @keyframes gentleMove2 { + 0%, 100% { transform: translate(0, 0); } + 50% { transform: translate(-2px, 4px); } } + @keyframes gentleMove3 { + 0%, 100% { transform: translate(0, 0); } + 50% { transform: translate(4px, -3px); } + } + @keyframes shootingStar { + 0% { transform: translate(0, 0); opacity: 1; } + 100% { transform: translate(300px, 300px); opacity: 0; } + } + .twinkle { animation: twinkle 4s ease-in-out infinite; } + .gentle-move1 { animation: gentleMove1 25s ease-in-out infinite; } + .gentle-move2 { animation: gentleMove2 30s ease-in-out infinite; } + .gentle-move3 { animation: gentleMove3 35s ease-in-out infinite; } + .shooting-star { + position: absolute; + width: 4px; + height: 4px; + background: white; + border-radius: 50%; + top: -4px; + left: -4px; + animation: shootingStar 1.5s linear; + animation-iteration-count: 1; + } + `; + + const [shootingStars, setShootingStars] = useState([]); + useEffect(() => { + const createShootingStar = () => { + const id = Date.now(); + setShootingStars((prev) => [...prev, id]); + setTimeout(() => { + setShootingStars((prev) => prev.filter((starId) => starId !== id)); + }, 1500); // Match the duration of the shooting star animation + }; + + const interval = setInterval(() => { + if (Math.random() < 0.3) { + // 30% chance every 3 seconds + createShootingStar(); + } + }, 3000); + + return () => clearInterval(interval); + }, []); + + const starPositions = [ + { x: 10, y: 15 }, + { x: 25, y: 30 }, + { x: 40, y: 10 }, + { x: 60, y: 40 }, + { x: 75, y: 20 }, + { x: 5, y: 50 }, + { x: 30, y: 70 }, + { x: 50, y: 85 }, + { x: 80, y: 60 }, + { x: 90, y: 35 }, + { x: 15, y: 25 }, + { x: 35, y: 45 }, + { x: 55, y: 15 }, + { x: 70, y: 55 }, + { x: 85, y: 30 }, + { x: 20, y: 65 }, + { x: 45, y: 80 }, + { x: 65, y: 5 }, + { x: 95, y: 45 }, + { x: 8, y: 90 }, + { x: 28, y: 18 }, + { x: 48, y: 38 }, + { x: 68, y: 78 }, + { x: 88, y: 22 }, + { x: 12, y: 72 }, + { x: 32, y: 92 }, + { x: 52, y: 62 }, + { x: 72, y: 42 }, + { x: 92, y: 82 }, + { x: 18, y: 52 }, + { x: 38, y: 32 }, + { x: 58, y: 72 }, + { x: 78, y: 12 }, + { x: 98, y: 58 }, + { x: 3, y: 83 }, + { x: 23, y: 3 }, + { x: 43, y: 93 }, + { x: 63, y: 33 }, + { x: 83, y: 73 }, + { x: 7, y: 37 }, + ]; + + const seededRandom = (function () { + const seed = 12345; // You can change this seed to get a different, but consistent, pattern + let state = seed; + return function () { + state = (state * 1664525 + 1013904223) % 4294967296; + return state / 4294967296; + }; + })(); + + const generateStars = () => { + return starPositions.map((pos, i) => ( + 0.7 ? "twinkle" : ""} + /> + )); + }; + + const generateAnimatedStars = () => { + const animatedStarPositions = [ + { x: 20, y: 20 }, + { x: 45, y: 55 }, + { x: 70, y: 30 }, + { x: 85, y: 75 }, + { x: 15, y: 80 }, + { x: 55, y: 25 }, + { x: 35, y: 65 }, + { x: 65, y: 50 }, + { x: 10, y: 40 }, + { x: 90, y: 10 }, + ]; + return animatedStarPositions.map((pos, i) => ( + + )); }; return ( -
- + + - { - setStarsLoaded(true); - }} - /> - -
-
- Photograph of the moon { - setMoonLoaded(true); - }} - /> -
-
- 3D claymation style model of a astronaut on a rocket { - setRocketLoaded(true); - }} - /> -
-
+ {generateStars()} + {generateAnimatedStars()} + + {shootingStars.map((id) => ( +
+ ))} +
+ ); +}; -
- Codú logo +
+
+ +
+
-
-
- -
+ + ); } diff --git a/components/SideBar/SideBarSavedPosts.tsx b/components/SideBar/SideBarSavedPosts.tsx index 43969bfd..54c4b1f4 100644 --- a/components/SideBar/SideBarSavedPosts.tsx +++ b/components/SideBar/SideBarSavedPosts.tsx @@ -6,14 +6,14 @@ import SideBarSavedArticlePreview from "./SideBarSavedArticlePreview"; import Link from "next/link"; export default React.memo(function SideBarSavedPosts() { - let { data: bookmarks } = api.post.myBookmarks.useQuery(); - const { status: bookmarkStatus } = api.post.myBookmarks.useQuery(); - const howManySavedToShow = 3; - const totalNumberSaved = bookmarks?.length; + const { data: bookmarksData, status: bookmarkStatus } = + api.post.myBookmarks.useQuery({ + limit: howManySavedToShow, + }); - // @TODO query the backend to get the last 3 instead of slice - if (bookmarks) bookmarks = bookmarks.slice(0, howManySavedToShow); + const totalNumberSaved = bookmarksData?.totalCount || 0; + const bookmarks = bookmarksData?.bookmarks || []; return (
diff --git a/components/editor/editor/RenderPost.tsx b/components/editor/editor/RenderPost.tsx index 5fd946f2..3bd1e912 100644 --- a/components/editor/editor/RenderPost.tsx +++ b/components/editor/editor/RenderPost.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { CustomCodeBlockReadOnly, TiptapExtensions } from "./extensions"; +import { TiptapExtensions } from "./extensions"; import { EditorContent, useEditor } from "@tiptap/react"; interface RenderPostProps { @@ -11,7 +11,7 @@ const RenderPost = ({ json }: RenderPostProps) => { const editor = useEditor({ editable: false, - extensions: [...TiptapExtensions, CustomCodeBlockReadOnly], + extensions: [...TiptapExtensions], content, }); diff --git a/components/editor/editor/components/CodeBlock/CodeBlock.module.css b/components/editor/editor/components/CodeBlock/CodeBlock.module.css deleted file mode 100644 index c0ea71df..00000000 --- a/components/editor/editor/components/CodeBlock/CodeBlock.module.css +++ /dev/null @@ -1,5 +0,0 @@ -/* For styling codeblock theme */ -.code-block div { - background-color: black; - color: white; -} diff --git a/components/editor/editor/components/CodeBlock/CodeBlock.tsx b/components/editor/editor/components/CodeBlock/CodeBlock.tsx deleted file mode 100644 index 0abe4459..00000000 --- a/components/editor/editor/components/CodeBlock/CodeBlock.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import type { NodeViewProps } from "@tiptap/react"; -import { NodeViewContent, NodeViewWrapper } from "@tiptap/react"; -import type { ChangeEvent, FunctionComponent } from "react"; - -import styles from "./CodeBlock.module.css"; - -interface CodeBlockProps { - readOnly: boolean; -} - -const CodeBlock: FunctionComponent = ({ - node: { - attrs: { language: defaultLanguage }, - }, - updateAttributes, - extension, - readOnly, -}) => { - return ( - - {/* Read only mode removes selector ability dropdown - more elegant solution potentially possible */} - {!readOnly && ( - - )} -
-        
-      
-
- ); -}; - -export default CodeBlock; diff --git a/components/editor/editor/components/Table/CustomTableNodeView.tsx b/components/editor/editor/components/Table/CustomTableNodeView.tsx deleted file mode 100644 index 44b39bff..00000000 --- a/components/editor/editor/components/Table/CustomTableNodeView.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import type { NodeViewProps } from "@tiptap/react"; -import { NodeViewContent, NodeViewWrapper } from "@tiptap/react"; -import { - BookmarkMinus, - ColumnsIcon, - DeleteIcon, - RowsIcon, - Trash2Icon, -} from "lucide-react"; -import type { FunctionComponent, ReactNode } from "react"; - -import styles from "../Toolbar/Toolbar.module.css"; - -interface TableButtonProps { - disabled: boolean; - onClick: () => boolean | void; - icon: ReactNode; - className: string; - title: string; -} - -const TableButton: FunctionComponent = ({ - onClick, - disabled, - icon, - className, - title, -}) => { - return ( -
- -
- ); -}; - -const CustomTableNodeView = (props: NodeViewProps) => { - const { editor } = props; - - return ( - -
- editor.chain().focus().addColumnAfter().run()} - disabled={!editor.can().addColumnAfter()} - className="bg-neutral-900 hover:bg-neutral-600" - icon={} - title="Add Column" - /> - editor.chain().focus().addRowAfter().run()} - disabled={!editor.can().addColumnAfter()} - className="bg-neutral-900 hover:bg-neutral-600" - title="Add Row" - icon={} - /> - editor.chain().focus().deleteColumn().run()} - disabled={!editor.can().deleteColumn()} - className="bg-red-700 hover:bg-red-400" - title="Delete Column" - icon={} - /> - - editor.chain().focus().deleteRow().run()} - disabled={!editor.can().deleteRow()} - className="bg-red-700 hover:bg-red-400" - title="Delete Row" - icon={} - /> - editor.chain().focus().deleteTable().run()} - disabled={!editor.can().deleteTable()} - className="bg-red-700 hover:bg-red-400" - title="Delete Table" - icon={} - /> -
- -
- ); -}; - -export default CustomTableNodeView; diff --git a/components/editor/editor/components/Toolbar/Toolbar.module.css b/components/editor/editor/components/Toolbar/Toolbar.module.css deleted file mode 100644 index 9e47edf3..00000000 --- a/components/editor/editor/components/Toolbar/Toolbar.module.css +++ /dev/null @@ -1,102 +0,0 @@ -.sticky { - position: sticky; - top: 0; - z-index: 10; - width: 100%; - margin-bottom: 1em; -} - -.flex { - display: flex; - justify-content: space-between; - align-items: center; -} - -.menu { - flex-grow: 1; -} - -.buttons { - display: flex; - gap: 1em; -} - -.switch { - display: inline-block; - position: relative; - width: 4rem; - height: 1.5rem; - margin-left: auto; -} - -.switch input { - opacity: 0; - width: 0; - height: 0; -} - -.slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 1.5rem; - transition: 0.4s; -} - -.slider::before { - position: absolute; - content: ""; - height: 1.25rem; - width: 1.25rem; - left: 0.125rem; - bottom: 0.125rem; - background-color: whitesmoke; - border-radius: 50%; - transition: 0.4s; -} - -input:checked + .slider::before { - transform: translateX(1.25rem); -} - -.gradientText { - background-image: linear-gradient(60deg, #e21143, #ffb03a); - background-clip: text; - color: transparent; -} - -.tooltip { - visibility: 0; - position: absolute; - background-color: #333; - color: white; - text-align: center; - border-radius: 4px; - padding: 4px 8px; - opacity: 0; - margin: auto; - transition: - opacity 0.2s ease-in-out, - transform 0.3s ease-in-out; - transition-delay: 0.75s; - pointer-events: none; - z-index: 1000; -} - -.buttonContainer { - padding: 0; - margin: 0; - justify-content: center; - align-items: center; - display: flex; -} - -.buttonContainer:hover .tooltip { - visibility: 1; - opacity: 1; -} - - diff --git a/components/editor/editor/components/Toolbar/Toolbar.tsx b/components/editor/editor/components/Toolbar/Toolbar.tsx deleted file mode 100644 index 2ccdcf8a..00000000 --- a/components/editor/editor/components/Toolbar/Toolbar.tsx +++ /dev/null @@ -1,441 +0,0 @@ -import styles from "./Toolbar.module.css"; -import type { BubbleMenuProps } from "@tiptap/react"; -import type { ChangeEvent } from "react"; -import { useState } from "react"; -import { - BoldIcon, - ItalicIcon, - UnderlineIcon, - StrikethroughIcon, - CodeIcon, - Heading2Icon, - Heading3Icon, - ListOrderedIcon, - ListIcon, - SquareCodeIcon, - QuoteIcon, - RectangleHorizontalIcon, - UndoIcon, - RedoIcon, - AlignLeftIcon, - AlignCenterIcon, - AlignRightIcon, - SubscriptIcon, - SuperscriptIcon, - ImageIcon, - YoutubeIcon, - TableIcon, -} from "lucide-react"; - -import ToolBarItemButton from "./ToolbarItemButton"; -import ImageDetailsModal from "@/components/ImageDetailsModal/ImageDetailsModal"; - -type ToolbarProps = Omit; - -export interface ToolbarItem { - name: string; - isActive: () => boolean; - command: () => void; - icon: typeof BoldIcon; -} - -function Toolbar({ editor }: ToolbarProps) { - const [isOpen, setIsOpen] = useState(true); - const [, setIsTableEditing] = useState(false); - const [isImageDetailsModalOpen, setIsImageDetailsModalOpen] = useState(false); - - const isRootNode = () => { - try { - return editor?.view.state.selection.$from.before() === 0; - } catch (e) { - // Handle or log the exception if necessary - return false; - } - }; - - const handleExpand = (e: ChangeEvent) => { - setIsOpen(e.target.checked); - }; - - if (!editor) { - return null; - } - - const addYoutubeVideo = () => { - const url = prompt("Enter YouTube URL"); - - if (url) { - editor.commands.setYoutubeVideo({ - src: url, - }); - } - }; - - return ( -
- -
-
-
- editor.chain().focus().toggleBold().run()} - icon={ - - } - /> - editor.chain().focus().toggleItalic().run()} - icon={ - - } - /> - editor.chain().focus().toggleUnderline().run()} - icon={ - - } - /> - editor.chain().focus().toggleStrike().run()} - icon={ - - } - /> - editor.chain().focus().toggleCode().run()} - icon={ - - } - /> - - - editor.chain().focus().toggleHeading({ level: 2 }).run() - } - icon={ - - } - /> - - - editor.chain().focus().toggleHeading({ level: 3 }).run() - } - icon={ - - } - /> - - editor.chain().focus().toggleBulletList().run()} - icon={ - - } - /> - - editor.chain().focus().toggleOrderedList().run()} - icon={ - - } - /> - - editor.chain().focus().toggleCodeBlock().run()} - icon={ - - } - /> - - editor.chain().focus().toggleBlockquote().run()} - icon={ - - } - /> - - editor.chain().focus().setHorizontalRule().run()} - icon={ - - } - /> - editor.chain().focus().undo().run()} - icon={} - /> - editor.chain().focus().redo().run()} - icon={} - /> - - editor.chain().focus().setTextAlign("left").run()} - icon={ - - } - /> - - editor.chain().focus().setTextAlign("center").run() - } - icon={ - - } - /> - editor.chain().focus().setTextAlign("right").run()} - icon={ - - } - /> - - { - if (editor.isActive("superscript")) { - editor.chain().focus().toggleSuperscript().run(); - } - editor.chain().focus().toggleSubscript().run(); - }} - icon={ - - } - /> - - { - if (editor.isActive("subscript")) { - editor.chain().focus().toggleSubscript().run(); - } - - editor.chain().focus().toggleSuperscript().run(); - }} - icon={ - - } - /> - <> - { - editor - .chain() - .focus() - .insertTable({ rows: 3, cols: 3 }) - .run(); - setIsTableEditing(true); - }} - icon={ - - } - /> - - setIsImageDetailsModalOpen(true)} - icon={} - /> - } - /> -
-
- {/* TODO: temporary label needs replacing */} - {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} - -
-
- ); -} - -export default Toolbar; diff --git a/components/editor/editor/components/Toolbar/ToolbarItemButton.tsx b/components/editor/editor/components/Toolbar/ToolbarItemButton.tsx deleted file mode 100644 index 53b0c139..00000000 --- a/components/editor/editor/components/Toolbar/ToolbarItemButton.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type { FunctionComponent, ReactNode } from "react"; - -import styles from "./Toolbar.module.css"; - -interface ToolBarItemButtonProps { - isRootNode: () => boolean; - icon: ReactNode; - title: string; - onClick: () => boolean | void; - className?: string; -} - -const ToolBarItemButton: FunctionComponent = ({ - title, - isRootNode, - icon, - onClick, - className, -}) => { - return ( -
- -
- ); -}; - -export default ToolBarItemButton; diff --git a/components/editor/editor/components/bubble-menu.tsx b/components/editor/editor/components/bubble-menu.tsx index 40f3ee87..a1dcf67f 100644 --- a/components/editor/editor/components/bubble-menu.tsx +++ b/components/editor/editor/components/bubble-menu.tsx @@ -5,7 +5,6 @@ import { useState } from "react"; import { BoldIcon, ItalicIcon, - StrikethroughIcon, CodeIcon, } from "lucide-react"; @@ -36,12 +35,6 @@ export const EditorBubbleMenu: FC = (props) => { command: () => props.editor.chain().focus().toggleItalic().run(), icon: ItalicIcon, }, - { - name: "strike", - isActive: () => props.editor.isActive("strike"), - command: () => props.editor.chain().focus().toggleStrike().run(), - icon: StrikethroughIcon, - }, { name: "code", isActive: () => props.editor.isActive("code"), diff --git a/components/editor/editor/components/color-selector.tsx b/components/editor/editor/components/color-selector.tsx deleted file mode 100644 index 0064e73e..00000000 --- a/components/editor/editor/components/color-selector.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import type { Editor } from "@tiptap/core"; -import { Check, ChevronDown } from "lucide-react"; -import type { Dispatch, FC, SetStateAction } from "react"; - -export interface BubbleColorMenuItem { - name: string; - color: string | null; -} - -interface ColorSelectorProps { - editor: Editor; - isOpen: boolean; - setIsOpen: Dispatch>; -} - -const TEXT_COLORS: BubbleColorMenuItem[] = [ - { - name: "Default", - color: "var(--novel-black)", - }, - { - name: "Purple", - color: "#9333EA", - }, - { - name: "Red", - color: "#E00000", - }, - { - name: "Yellow", - color: "#EAB308", - }, - { - name: "Blue", - color: "#2563EB", - }, - { - name: "Green", - color: "#008A00", - }, - { - name: "Orange", - color: "#FFA500", - }, - { - name: "Pink", - color: "#BA4081", - }, - { - name: "Gray", - color: "#A8A29E", - }, -]; - -const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = [ - { - name: "Default", - color: "var(--novel-highlight-default)", - }, - { - name: "Purple", - color: "var(--novel-highlight-purple)", - }, - { - name: "Red", - color: "var(--novel-highlight-red)", - }, - { - name: "Yellow", - color: "var(--novel-highlight-yellow)", - }, - { - name: "Blue", - color: "var(--novel-highlight-blue)", - }, - { - name: "Green", - color: "var(--novel-highlight-green)", - }, - { - name: "Orange", - color: "var(--novel-highlight-orange)", - }, - { - name: "Pink", - color: "var(--novel-highlight-pink)", - }, - { - name: "Gray", - color: "var(--novel-highlight-gray)", - }, -]; - -export const ColorSelector: FC = ({ - editor, - isOpen, - setIsOpen, -}) => { - const activeColorItem = TEXT_COLORS.find(({ color }) => - editor.isActive("textStyle", { color }), - ); - - const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) => - editor.isActive("highlight", { color }), - ); - - return ( -
- - - {isOpen && ( -
-
Color
- {TEXT_COLORS.map(({ name, color }, index) => ( - - ))} - -
- Background -
- - {HIGHLIGHT_COLORS.map(({ name, color }, index) => ( - - ))} -
- )} -
- ); -}; diff --git a/components/editor/editor/extensions/index.tsx b/components/editor/editor/extensions/index.tsx index 1b84a688..5965c3d2 100644 --- a/components/editor/editor/extensions/index.tsx +++ b/components/editor/editor/extensions/index.tsx @@ -3,29 +3,18 @@ import HorizontalRule from "@tiptap/extension-horizontal-rule"; import TiptapLink from "@tiptap/extension-link"; import Link from "@tiptap/extension-link"; import Placeholder from "@tiptap/extension-placeholder"; -import TiptapUnderline from "@tiptap/extension-underline"; import TextStyle from "@tiptap/extension-text-style"; -import { Color } from "@tiptap/extension-color"; import { Markdown } from "tiptap-markdown"; -import Highlight from "@tiptap/extension-highlight"; import SlashCommand from "./slash-command"; import { InputRule } from "@tiptap/core"; import UpdatedImage from "./updated-image"; import Document from "@tiptap/extension-document"; import Paragraph from "@tiptap/extension-paragraph"; import Text from "@tiptap/extension-text"; -import TextAlign from "@tiptap/extension-text-align"; -import Subscript from "@tiptap/extension-subscript"; -import Superscript from "@tiptap/extension-superscript"; import Youtube from "@tiptap/extension-youtube"; -import Table from "@tiptap/extension-table"; -import TableCell from "@tiptap/extension-table-cell"; -import TableHeader from "@tiptap/extension-table-header"; -import TableRow from "@tiptap/extension-table-row"; import type { NodeViewProps } from "@tiptap/react"; import { ReactNodeViewRenderer } from "@tiptap/react"; -import CustomTableNodeView from "../components/Table/CustomTableNodeView"; import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; import { common, createLowlight } from "lowlight"; @@ -34,7 +23,6 @@ const lowlight = createLowlight(common); // Highlight syntax select your style from here (https://highlightjs.org/examples) import "highlight.js/styles/monokai-sublime.css"; -import CodeBlock from "../components/CodeBlock/CodeBlock"; import DisableHeadingTextStyleShortcuts from "./disable-heading-text-style-shortcuts"; // const CustomImage = TiptapImage.extend({ @@ -47,53 +35,13 @@ const CustomDocument = Document.extend({ content: "heading block*", }); -export const CustomCodeBlockEdit = CodeBlockLowlight.extend({ - addNodeView() { - return ReactNodeViewRenderer(CodeBlock); - }, -}).configure({ lowlight }); - -// Two CodeBlockNodes need to be created to disable selector menu -export const CustomCodeBlockReadOnly = CodeBlockLowlight.extend({ - addNodeView() { - return ReactNodeViewRenderer((props: NodeViewProps) => ( - - )); - }, -}).configure({ lowlight }); - -export const CustomTable = Table.extend({ - addNodeView() { - return ReactNodeViewRenderer((props: NodeViewProps) => ( - - )); - }, -}); export const TiptapExtensions = [ CustomDocument, - CustomTable, // Table.configure({ // HTMLAttributes: { // class: "bg-neutral-100 w-full overflow-scroll", // }, // }), - TableRow.configure({ - HTMLAttributes: { - class: "bg-neutral-900 border border-neutral-500 bg-red-500 flex-1 flex", - }, - }), - TableHeader.configure({ - HTMLAttributes: { - class: - "text-neutral-900 bg-neutral-300 border border-neutral-500 flex-1 flex text-center p-1", - }, - }), - TableCell.configure({ - HTMLAttributes: { - class: - "text-neutral-900 bg-neutral-100 border border-neutral-500 flex-1 flex p-1", - }, - }), Paragraph, Text, StarterKit.configure({ @@ -191,27 +139,17 @@ export const TiptapExtensions = [ }, }), SlashCommand, - TiptapUnderline, TextStyle, - Color, Link.configure({ HTMLAttributes: { class: "text-stone-400 underline underline-offset-[3px] hover:text-stone-600 transition-colors cursor-pointer", }, }), - Highlight.configure({ - multicolor: true, - }), Markdown.configure({ html: false, transformCopiedText: true, }), - TextAlign.configure({ - types: ["heading", "paragraph"], - }), - Subscript, - Superscript, // margin controlled in global.css Youtube.configure({ width: 480, diff --git a/components/editor/editor/index.tsx b/components/editor/editor/index.tsx index 0bab16e9..32dfcb60 100644 --- a/components/editor/editor/index.tsx +++ b/components/editor/editor/index.tsx @@ -5,10 +5,9 @@ import Typography from '@tiptap/extension-typography' import { EditorContent, useEditor } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import { TiptapEditorProps } from "./props"; -import { CustomCodeBlockEdit, TiptapExtensions } from "./extensions"; +import { TiptapExtensions } from "./extensions"; import { EditorBubbleMenu } from "./components/bubble-menu"; import { MediaResizer } from "./components/image-resizer"; -import Toolbar from "./components/Toolbar/Toolbar"; interface EditorProps { initialValue: string; @@ -40,7 +39,6 @@ export default function Editor({ onChange, initialValue }: EditorProps) { editor?.chain().focus().run(); }} > - {editor && } {editor && } {editor && ( diff --git a/e2e/home.spec.ts b/e2e/home.spec.ts index 99ecdf36..f8f50898 100644 --- a/e2e/home.spec.ts +++ b/e2e/home.spec.ts @@ -4,7 +4,6 @@ test.describe("Testing homepage views", () => { test("Authenticated homepage view", async ({ page, isMobile }) => { await page.goto("http://localhost:3000/"); - await expect(page.locator("h1")).toContainText("A space for coders"); await expect(page.locator("h1")).not.toContainText("Unwanted text"); if (!isMobile) @@ -18,11 +17,13 @@ test.describe("Testing homepage views", () => { await page.context().clearCookies(); await page.goto("http://localhost:3000/"); - await expect(page.locator("h1")).toContainText("A space for coders"); await expect(page.locator("h1")).not.toContainText("Unwanted text"); await expect(page.locator("h2")).toContainText( "Sign up today to become a writer and get a free invite to our Discord community", ); + await expect(page.locator("h1")).toContainText( + "The free web developer community", + ); }); test("Authenticated landing page view", async ({ page, isMobile }) => { diff --git a/package-lock.json b/package-lock.json index f0e354a0..70e3720b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,7 +55,6 @@ "@trpc/react-query": "^10.45.1", "@trpc/server": "^10.45.1", "algoliasearch": "^5.7.0", - "atropos": "^2.0.2", "clsx": "^2.1.1", "copy-to-clipboard": "^3.3.3", "drizzle-orm": "^0.34.1", @@ -9491,14 +9490,6 @@ "astring": "bin/astring" } }, - "node_modules/atropos": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/atropos/-/atropos-2.0.2.tgz", - "integrity": "sha512-8f0u0hEOlBTWTSvzY17TcHuQjxUIpkTBq70/I4+UF5B43ORtOoRjm8TPBYEgLM8Ba9AWf6PDtkagbYoybdjaKg==", - "engines": { - "node": ">= 12.0.0" - } - }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", diff --git a/package.json b/package.json index 56cea40a..d878510e 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "@trpc/react-query": "^10.45.1", "@trpc/server": "^10.45.1", "algoliasearch": "^5.7.0", - "atropos": "^2.0.2", "clsx": "^2.1.1", "copy-to-clipboard": "^3.3.3", "drizzle-orm": "^0.34.1", diff --git a/public/images/home/moon.png b/public/images/home/moon.png deleted file mode 100644 index 78a15ddf..00000000 Binary files a/public/images/home/moon.png and /dev/null differ diff --git a/public/images/home/rocketman.png b/public/images/home/rocketman.png deleted file mode 100644 index a6d743e5..00000000 Binary files a/public/images/home/rocketman.png and /dev/null differ diff --git a/public/images/home/space.jpg b/public/images/home/space.jpg deleted file mode 100644 index 5ae21c63..00000000 Binary files a/public/images/home/space.jpg and /dev/null differ diff --git a/schema/post.ts b/schema/post.ts index 99576ea1..224e8940 100644 --- a/schema/post.ts +++ b/schema/post.ts @@ -76,3 +76,7 @@ export const GetSinglePostSchema = z.object({ export const GetByIdSchema = z.object({ id: z.string(), }); + +export const GetLimitSidePosts = z.object({ + limit: z.number().optional(), +}); diff --git a/server/api/router/post.ts b/server/api/router/post.ts index b2792a77..8a41482b 100644 --- a/server/api/router/post.ts +++ b/server/api/router/post.ts @@ -11,6 +11,7 @@ import { LikePostSchema, BookmarkPostSchema, GetByIdSchema, + GetLimitSidePosts, } from "../../../schema/post"; import { removeMarkdown } from "../../../utils/removeMarkdown"; import { bookmark, like, post, post_tag, tag, user } from "@/server/db/schema"; @@ -438,37 +439,51 @@ export const postRouter = createTRPCRouter({ return currentPost; }), - myBookmarks: protectedProcedure.query(async ({ ctx }) => { - const response = await ctx.db.query.bookmark.findMany({ - columns: { - id: true, - }, - where: (bookmarks, { eq }) => eq(bookmarks.userId, ctx.session.user.id), - with: { - post: { - columns: { - id: true, - title: true, - excerpt: true, - updatedAt: true, - published: true, - readTimeMins: true, - slug: true, - }, - with: { - user: { - columns: { - name: true, - username: true, - image: true, + myBookmarks: protectedProcedure + .input(GetLimitSidePosts) + .query(async ({ ctx, input }) => { + const limit = input?.limit ?? undefined; + + const response = await ctx.db.query.bookmark.findMany({ + columns: { + id: true, + }, + where: (bookmarks, { eq }) => eq(bookmarks.userId, ctx.session.user.id), + with: { + post: { + columns: { + id: true, + title: true, + excerpt: true, + updatedAt: true, + published: true, + readTimeMins: true, + slug: true, + }, + with: { + user: { + columns: { + name: true, + username: true, + image: true, + }, }, }, }, }, - }, - orderBy: (bookmarks, { desc }) => [desc(bookmarks.id)], - }); + orderBy: (bookmarks, { desc }) => [desc(bookmarks.id)], + }); - return response.map(({ id, post }) => ({ bookmarkId: id, ...post })); - }), + const totalCount = response.length; + + const bookmarksResponse = response.slice(0, limit || response.length); + + return { + totalCount, + bookmarks: bookmarksResponse.map(({ id, post }) => ({ + bookmarkId: id, + ...post, + })), + }; + }), });