diff --git a/frontend/nextjs/package.json b/frontend/nextjs/package.json index 465d21c..382d583 100644 --- a/frontend/nextjs/package.json +++ b/frontend/nextjs/package.json @@ -10,13 +10,21 @@ }, "dependencies": { "@auth/firebase-adapter": "^1.0.4", + "@hookform/resolvers": "^3.3.2", "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-toast": "^1.1.5", + "@radix-ui/react-toggle": "^1.0.3", "@rainbow-me/rainbowkit": "^1.1.4", "@rainbow-me/rainbowkit-siwe-next-auth": "^0.3.2", + "@tiptap/extension-placeholder": "^2.1.12", + "@tiptap/pm": "^2.1.12", + "@tiptap/react": "^2.1.12", + "@tiptap/starter-kit": "^2.1.12", "axios": "^1.6.2", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", @@ -30,12 +38,14 @@ "prettier": "^3.0.3", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.48.2", "react-icons": "^4.11.0", "siwe": "^2.1.4", "tailwind-merge": "^2.0.0", "tailwindcss-animate": "^1.0.7", "viem": "^1.18.1", - "wagmi": "^1.4.5" + "wagmi": "^1.4.5", + "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20", diff --git a/frontend/nextjs/src/app/dapp/components/providers.tsx b/frontend/nextjs/src/app/dapp/_components/providers.tsx similarity index 100% rename from frontend/nextjs/src/app/dapp/components/providers.tsx rename to frontend/nextjs/src/app/dapp/_components/providers.tsx diff --git a/frontend/nextjs/src/app/dapp/components/right-sidebar.tsx b/frontend/nextjs/src/app/dapp/_components/right-sidebar.tsx similarity index 100% rename from frontend/nextjs/src/app/dapp/components/right-sidebar.tsx rename to frontend/nextjs/src/app/dapp/_components/right-sidebar.tsx diff --git a/frontend/nextjs/src/app/dapp/components/sidebar.tsx b/frontend/nextjs/src/app/dapp/_components/sidebar.tsx similarity index 100% rename from frontend/nextjs/src/app/dapp/components/sidebar.tsx rename to frontend/nextjs/src/app/dapp/_components/sidebar.tsx diff --git a/frontend/nextjs/src/app/dapp/layout.tsx b/frontend/nextjs/src/app/dapp/layout.tsx index 45e0338..762cf9b 100644 --- a/frontend/nextjs/src/app/dapp/layout.tsx +++ b/frontend/nextjs/src/app/dapp/layout.tsx @@ -5,23 +5,25 @@ import '@rainbow-me/rainbowkit/styles.css'; import { ConnectButton } from '@rainbow-me/rainbowkit'; -import { Sidebar } from './components/sidebar'; -import { RightSidebar } from './components/right-sidebar'; +import { Sidebar } from './_components/sidebar'; +import { RightSidebar } from './_components/right-sidebar'; import { Search } from '@/components/ui/forms/search'; import { Button } from '@/components/ui/button'; import { HiOutlinePencilAlt } from "react-icons/hi" -import DappProviders from './components/providers'; +import DappProviders from './_components/providers'; import SessionProvider from "@/lib/hooks/sessionProvider"; import { getServerSession } from "next-auth"; export const metadata: Metadata = { title: 'MEMM! Homepage', } +type Props = { + children: React.ReactNode +} + export default async function DappLayout({ children -}: { - children: React.ReactNode -}) { +}: Props) { const session = await getServerSession(); return ( <> diff --git a/frontend/nextjs/src/app/dapp/p/[slug]/page.tsx b/frontend/nextjs/src/app/dapp/p/[slug]/page.tsx new file mode 100644 index 0000000..952cf0c --- /dev/null +++ b/frontend/nextjs/src/app/dapp/p/[slug]/page.tsx @@ -0,0 +1,221 @@ +import React from 'react' +import { HiCheckBadge, HiOutlineHandThumbUp, HiOutlineHandThumbDown, HiOutlineShare, HiOutlineFire } from 'react-icons/hi2' +import Image from 'next/image' +import Link from 'next/link' +import { Button } from '@/components/ui/button' +import { Card, CardHeader, CardContent, CardTitle, CardDescription, CardFooter } from '@/components/ui/card' +import { Separator } from '@/components/ui/separator' +import { Badge } from '@/components/ui/badge' +import { ScrollArea } from '@/components/ui/scroll-area' +import { RightSidebar } from '../../_components/right-sidebar' + +const Post = ({ params }: { params: { slug: string } }) => { + const post = { + title: "I’m an experienced CEO. I applied for 1001 positions. This is what happened.", + body: `Facilisi at lorem semper eget. Eget posuere dictumst velit lacus est. Fringilla quam sollicitudin diam sollicitudin magna. Arcu ullamcorper nisl at aliquet luctus. Vitae commodo dictum sed et. In ultrices eu curabitur neque pulvinar ac eget ullamcorper lorem. Velit vitae id sit gravida mi viverra. Non ipsum nunc sed risus fermentum sed in. Lectus donec dignissim diam sed non tortor. Nibh euismod id tincidunt scelerisque cras est. Tincidunt mollis commodo urna scelerisque nibh at sed. Amet odio erat congue diam in. + + Elit tellus velit diam suspendisse eget. Sed in et accumsan amet id sed ultrices lorem mollis. Donec tempus sapien pellentesque est pretium et. Ut euismod vitae feugiat donec amet euismod arcu egestas dis. Elementum neque suspendisse facilisis mi ullamcorper purus aliquam adipiscing. Sagittis non tristique sed sed purus magna sem. Integer non habitasse ornare in amet mauris id. Nulla condimentum ipsum aliquam urna vitae consequat. Nec lobortis aenean auctor imperdiet facilisis vel. Cras amet euismod neque dictumst vestibulum. Faucibus orci accumsan ipsum eget nunc magnis elit. Quam ultricies turpis scelerisque aliquet amet enim venenatis non. Iaculis hac in aliquet sed blandit vestibulum etiam. Adipiscing adipiscing augue senectus tempor. Mauris pellentesque consequat aliquet sagittis. + + In diam vestibulum eu tellus suspendisse non vestibulum. Ut ipsum risus suscipit amet quam a mi. Pellentesque est in amet in. Vitae urna laoreet non eu. Euismod ut quis elit risus massa. Posuere amet massa pulvinar cursus morbi nibh varius quam proin. Et tortor risus elementum morbi ante tortor adipiscing pretium vestibulum.`, + image: "https://images.unsplash.com/photo-1559136555-9303baea8ebd?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + author: { + name: "Naval", + avatar: "https://images.unsplash.com/photo-1640960543409-dbe56ccc30e2?q=80&w=1480&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + mentor: "true" + }, + metadata: { + upvotes: 314, + downvotes: 42 + } + } + + const topCreatorList = [ + { + name: "Naval", + avatar: "https://images.unsplash.com/photo-1607746882042-944635dfe10e?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fGF2YXRhcnxlbnwwfHwwfHx8MA%3D%3D", + mentor: "false", + skill: "UI Design", + href: "/profile/naval", + ment: 134 + }, + { + name: "Naval", + avatar: "https://images.unsplash.com/photo-1607746882042-944635dfe10e?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fGF2YXRhcnxlbnwwfHwwfHx8MA%3D%3D", + mentor: "true", + skill: "Java", + href: "/profile/naval", + ment: 693 + }, + { + name: "Naval", + avatar: "https://images.unsplash.com/photo-1607746882042-944635dfe10e?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fGF2YXRhcnxlbnwwfHwwfHx8MA%3D%3D", + mentor: "true", + skill: "Ruby", + href: "/profile/naval", + ment: 953 + }, + { + name: "Naval", + avatar: "https://images.unsplash.com/photo-1607746882042-944635dfe10e?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fGF2YXRhcnxlbnwwfHwwfHx8MA%3D%3D", + mentor: "true", + skill: "AI", + href: "/profile/naval", + ment: 422 + }, + ] + + return ( +
+
+ + + +
+
+ {`${post.author.name}-avatar`} +
+
+
+

{post.author.name}

+ {post.author.mentor === "true" && } +
20 secs. ago
+
+ +
+
+ +
+ +
+ {`${post.title} +
+ {post.title} + {post.body} +
+ + + +
+
+ +
+ {post.metadata.upvotes} +
+ +
+
+ +
+
+ + +
+
+
+
+ + <> + +
+

+ Top Creators +

+
+ {topCreatorList.map((profile, key) => { + return +
+
+ {`${profile.name}-avatar`} +
+
+
+

{profile.name}

+ {profile.mentor === "true" && } +
+ {profile.skill} +
+
+
+ +
+ 245 MENT +
+
+ + })} + + +
+
+ +
+

+ Who to Follow +

+
+ {topCreatorList.map((profile, key) => { + return +
+
+ {`${profile.name}-avatar`} +
+
+
+

{profile.name}

+ {profile.mentor === "true" && } +
+ {profile.skill} +
+
+
+ +
+ 245 MENT +
+
+ + })} + + +
+
+ +
+
+ ) +} + +export default Post \ No newline at end of file diff --git a/frontend/nextjs/src/app/dapp/p/create/_components/create-post-form.tsx b/frontend/nextjs/src/app/dapp/p/create/_components/create-post-form.tsx new file mode 100644 index 0000000..83cbf49 --- /dev/null +++ b/frontend/nextjs/src/app/dapp/p/create/_components/create-post-form.tsx @@ -0,0 +1,87 @@ +"use client" +import React from 'react' +import * as z from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" + +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { useToast } from "@/components/ui/use-toast" +import { Textarea } from "@/components/ui/textarea" +import RichTextEditor from '@/components/ui/rich-text-editor' + +const formSchema = z.object({ + postTitle: z.string().min(1, { + message: "Title can't be empty", + }), + postBody: z.string() +}) + +const CreatePostForm = () => { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + postTitle: "", + postBody: "", + }, + }) + const { toast } = useToast() + + function onSubmit(values: z.infer) { + console.log(values) + toast({ + description: "Your post has been created", + + }) + } + + return ( +
+
+ + ( + + Title + + + + + + )} + /> + + ( + + Body + + + + + + )} + /> +
+ +
+ + +
+ ) +} + +export default CreatePostForm \ No newline at end of file diff --git a/frontend/nextjs/src/app/dapp/p/create/page.tsx b/frontend/nextjs/src/app/dapp/p/create/page.tsx new file mode 100644 index 0000000..49cd8b2 --- /dev/null +++ b/frontend/nextjs/src/app/dapp/p/create/page.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { HiCheckBadge, HiOutlineHandThumbUp, HiOutlineHandThumbDown, HiOutlineShare, HiOutlineFire } from 'react-icons/hi2' +import Image from 'next/image' +import Link from 'next/link' +import { Button } from '@/components/ui/button' +import { Card, CardHeader, CardContent, CardTitle, CardDescription, CardFooter } from '@/components/ui/card' +import { Separator } from '@/components/ui/separator' +import { Badge } from '@/components/ui/badge' +import { ScrollArea } from '@/components/ui/scroll-area' +import { RightSidebar } from '../../_components/right-sidebar' +import CreatePostForm from './_components/create-post-form' + +const CreatePost = () => { + + return ( +
+
+ +

New Post

+ +
+
+ + {/* leave empty */} + +
+ ) +} + +export default CreatePost \ No newline at end of file diff --git a/frontend/nextjs/src/app/dapp/page.tsx b/frontend/nextjs/src/app/dapp/page.tsx index 3e2f678..33ce875 100644 --- a/frontend/nextjs/src/app/dapp/page.tsx +++ b/frontend/nextjs/src/app/dapp/page.tsx @@ -6,14 +6,14 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import PostCard from "@/components/ui/post-card"; import { Separator } from "@/components/ui/separator"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { RightSidebar } from "./components/right-sidebar"; +import { RightSidebar } from "./_components/right-sidebar"; import Image from "next/image"; import { HiCheckBadge, HiOutlineFire } from "react-icons/hi2" import { Badge } from "@/components/ui/badge"; import Link from "next/link"; import { useUser } from "@/lib/hooks/user"; -const dummyPosts = [ +export const dummyPosts = [ { author: { name: "Naval", diff --git a/frontend/nextjs/src/app/dapp/profile/layout.tsx b/frontend/nextjs/src/app/dapp/profile/layout.tsx new file mode 100644 index 0000000..721212f --- /dev/null +++ b/frontend/nextjs/src/app/dapp/profile/layout.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { Metadata } from 'next' +import { ScrollArea } from '@/components/ui/scroll-area' +import { RightSidebar } from '../_components/right-sidebar' + +export const metadata: Metadata = { + title: 'View profile', + } + +type Props = { + children: React.ReactNode +} + +const ProfileLayout = ({ children }: Props) => { + return ( +
+
+ + {children} + +
+ + {/* leave empty */} + +
+ ) +} + +export default ProfileLayout \ No newline at end of file diff --git a/frontend/nextjs/src/app/dapp/profile/page.tsx b/frontend/nextjs/src/app/dapp/profile/page.tsx new file mode 100644 index 0000000..96dc4df --- /dev/null +++ b/frontend/nextjs/src/app/dapp/profile/page.tsx @@ -0,0 +1,114 @@ +"use client" +import { Button } from '@/components/ui/button' +import { Card, CardHeader, CardContent, CardTitle, CardDescription, CardFooter } from '@/components/ui/card' +import { Separator } from '@/components/ui/separator' +import React from 'react' +import { HiCheckBadge, HiOutlineHandThumbUp, HiOutlineHandThumbDown, HiOutlineShare, HiOutlineCog6Tooth, HiOutlineFire } from 'react-icons/hi2' +import Image from "next/image" +import { Badge } from '@/components/ui/badge' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import PostCard from '@/components/ui/post-card' +import { dummyPosts } from '../page' + +const Profile = () => { + const profile = { + name: "Naval", + avatar: "https://images.unsplash.com/photo-1640960543409-dbe56ccc30e2?q=80&w=1480&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + mentor: "true", + skill: "UX Design", + about: "I’m an experienced CEO. I applied for 1001 positions. This is what happened.", + + } + + return ( +
+
+
+
+
+ {`${profile.name}-avatar`} +
+
+
+

{profile.name}

+ {profile.mentor === "true" && } +
+ {profile.skill} +
+
+ +
+ +
+ {profile.about} +
+ +
+
+
+ +
+ 245 MENT +
+
+ +
+
+ 5300 Followers +
+
+ +
+
+ 244 Following +
+
+
+
+
+ +
+ + + My Posts + Wallet + Notification Settings + + +
+ {dummyPosts.map((post, key) => { + return <> + + + + })} +
+
+ +
+

Wallet

+
+
+ +
+

Notification Settings

+
+
+
+
+ +
+ ) +} + +export default Profile \ No newline at end of file diff --git a/frontend/nextjs/src/app/layout.tsx b/frontend/nextjs/src/app/layout.tsx index 5fe0568..77b28ec 100644 --- a/frontend/nextjs/src/app/layout.tsx +++ b/frontend/nextjs/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next' import { Inter } from 'next/font/google' import './globals.css' import { ThemeProvider } from '@/components/ui/theme-provider' +import { Toaster } from "@/components/ui/toaster" const inter = Inter({ subsets: ["latin"] }); @@ -28,6 +29,7 @@ export default async function RootLayout({ disableTransitionOnChange > {children} + diff --git a/frontend/nextjs/src/components/ui/form.tsx b/frontend/nextjs/src/components/ui/form.tsx new file mode 100644 index 0000000..3aa0c7c --- /dev/null +++ b/frontend/nextjs/src/components/ui/form.tsx @@ -0,0 +1,176 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +
@@ -77,7 +78,6 @@ const PostCard = ({data}: any) => { - ) } diff --git a/frontend/nextjs/src/components/ui/rich-text-editor.tsx b/frontend/nextjs/src/components/ui/rich-text-editor.tsx new file mode 100644 index 0000000..9ab9b4b --- /dev/null +++ b/frontend/nextjs/src/components/ui/rich-text-editor.tsx @@ -0,0 +1,42 @@ +"use client" +import React from 'react' +import { Textarea } from './textarea' +import { useEditor, EditorContent } from "@tiptap/react" +import StarterKit from "@tiptap/starter-kit" +import Heading from "@tiptap/extension-heading" +import Placeholder from "@tiptap/extension-placeholder" +import Toolbar from './toolbar' + +const RichTextEditor = (props: any) => { + const { value, placeholder, onChange } = props + const editor = useEditor({ + extensions: [ + StarterKit, + Placeholder.configure({ placeholder: 'Answer a question or explain a concept', }), + Heading.configure({ + HTMLAttributes: { + class: "text-xl font-bold", + levels: [2] + } + }) + ], + content: value, + editorProps: { + attributes: { + class: "flex min-h-[250px] w-full rounded-md border border-alt-stroke bg-accent-shade px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", + } + }, + onUpdate({ editor }) { + onChange(editor.getHTML()) + console.log(editor.getHTML()) + } + }) + return ( +
+ + +
+ ) +} + +export default RichTextEditor \ No newline at end of file diff --git a/frontend/nextjs/src/components/ui/textarea.tsx b/frontend/nextjs/src/components/ui/textarea.tsx new file mode 100644 index 0000000..3fbb6ed --- /dev/null +++ b/frontend/nextjs/src/components/ui/textarea.tsx @@ -0,0 +1,24 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface TextareaProps + extends React.TextareaHTMLAttributes {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +