├── .eslintrc.json ├── src ├── app │ ├── @post │ │ ├── default.tsx │ │ ├── loading.tsx │ │ └── (.)p │ │ │ ├── components │ │ │ └── ModalContent.tsx │ │ │ ├── [postId] │ │ │ ├── page.tsx │ │ │ └── loading.tsx │ │ │ └── loading.tsx │ ├── favicon.ico │ ├── loading.tsx │ ├── api │ │ └── auth │ │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── p │ │ ├── loading.tsx │ │ ├── [postId] │ │ │ ├── loading.tsx │ │ │ ├── components │ │ │ │ └── PostContent.tsx │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── s │ │ ├── loading.tsx │ │ ├── [storyId] │ │ │ ├── loading.tsx │ │ │ ├── DeleteButton.tsx │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── u │ │ ├── loading.tsx │ │ ├── [userId] │ │ │ ├── loading.tsx │ │ │ ├── components │ │ │ │ ├── UserTags.tsx │ │ │ │ └── UserPosts.tsx │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (auth) │ │ ├── loading.tsx │ │ ├── signin │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── signup │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── search │ │ ├── loading.tsx │ │ ├── page.tsx │ │ ├── layout.tsx │ │ └── components │ │ │ └── SearchBox.tsx │ ├── bookmarks │ │ ├── loading.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── account │ │ ├── loading.tsx │ │ ├── edit │ │ │ └── loading.tsx │ │ ├── help │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── view │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── enotifications │ │ │ └── loading.tsx │ │ ├── notifications │ │ │ └── loading.tsx │ │ └── layout.tsx │ ├── page.tsx │ └── layout.tsx ├── helpers │ ├── index.ts │ └── isImageValid.ts ├── types │ ├── audience.d.ts │ ├── story.d.ts │ ├── bookmark.d.ts │ ├── post.d.ts │ ├── post-id.d.ts │ ├── user.d.ts │ └── next-auth.d.ts ├── actions │ ├── getSession.ts │ ├── getUsers.ts │ ├── profileCount.ts │ ├── getStoryById.ts │ ├── getStories.ts │ ├── deletePost.ts │ ├── getPostById.ts │ ├── scheduleStory.ts │ ├── deleteStory.ts │ ├── getUserBookmarks.ts │ ├── createStory.ts │ ├── getUserPosts.ts │ ├── getParamsUser.ts │ ├── getCurrentUser.ts │ ├── getPosts.ts │ ├── getViewCount.ts │ ├── index.ts │ ├── like copy.ts │ ├── follow copy.ts │ ├── follow.ts │ ├── createPost.ts │ ├── like.ts │ └── bookmark.ts ├── store │ ├── index.ts │ ├── story.ts │ ├── post.ts │ └── following.ts ├── lib │ ├── scheduler.ts │ ├── post.ts │ ├── db.ts │ ├── routes.ts │ ├── utils.ts │ ├── auth.ts │ ├── test.tsx │ └── stories.ts ├── hooks │ ├── use-alert-modal.ts │ ├── story-modal.ts │ ├── use-options-modal.ts │ ├── use-search-modal.ts │ ├── use-create-post-modal.ts │ ├── use-create-story-modal.ts │ ├── index.ts │ ├── use-auth-toast.tsx │ └── use-on-click-outside.ts ├── components │ ├── Spinner.tsx │ ├── sidebar │ │ ├── Followbar.tsx │ │ ├── Overview.tsx │ │ └── AccountSidebar.tsx │ ├── Providers.tsx │ ├── ui │ │ ├── Label.tsx │ │ ├── Textarea.tsx │ │ ├── Separator.tsx │ │ ├── Input.tsx │ │ ├── Toaster.tsx │ │ ├── sonner.tsx │ │ ├── Checkbox.tsx │ │ ├── Slider.tsx │ │ ├── Switch.tsx │ │ ├── HoverCard.tsx │ │ ├── Popover.tsx │ │ ├── Avatar.tsx │ │ ├── RadioGroup.tsx │ │ ├── Tooltip.tsx │ │ ├── Tabs.tsx │ │ └── Button.tsx │ ├── user │ │ ├── UserNav.tsx │ │ └── UserAvatar.tsx │ ├── SkeletonLoader.tsx │ ├── post │ │ ├── Post.tsx │ │ ├── PostComment.tsx │ │ ├── PostImage.tsx │ │ └── PostActions.tsx │ ├── stories │ │ └── Story.tsx │ ├── Loader.tsx │ ├── auth │ │ ├── SignUp.tsx │ │ ├── SignIn.tsx │ │ └── AuthForm.tsx │ ├── feed │ │ ├── PostFeed.tsx │ │ └── GeneralFeed.tsx │ ├── Header.tsx │ ├── Modal.tsx │ ├── MobileHeader.tsx │ ├── modals │ │ ├── AlertModal.tsx │ │ └── SearchModal.tsx │ ├── StoryModal.tsx │ ├── Actions.tsx │ └── LikeButton.tsx └── styles │ └── globals.css ├── public ├── svg │ ├── image.png │ ├── menu.svg │ ├── user.svg │ ├── search.svg │ ├── heart-fill.svg │ ├── bookmark.svg │ ├── bookmark-fill.svg │ ├── add.svg │ ├── post.svg │ ├── home.svg │ ├── link.svg │ ├── chat.svg │ ├── shield.svg │ ├── send.svg │ ├── camera.svg │ ├── bell.svg │ ├── users.svg │ ├── heart.svg │ ├── compass.svg │ └── google.svg ├── icons │ ├── logo.png │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest ├── vercel.svg └── next.svg ├── .vscode └── settings.json ├── postcss.config.js ├── next.config.js ├── components.json ├── .gitignore ├── tsconfig.json ├── LICENSE ├── package.json ├── tailwind.config.ts └── .github └── workflows └── nextjs.yml /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/@post/default.tsx: -------------------------------------------------------------------------------- 1 | export default function Default() { 2 | return null; 3 | } -------------------------------------------------------------------------------- /public/svg/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahandmohammadrehzaii/unity/HEAD/public/svg/image.png -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahandmohammadrehzaii/unity/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /public/icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahandmohammadrehzaii/unity/HEAD/public/icons/logo.png -------------------------------------------------------------------------------- /public/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahandmohammadrehzaii/unity/HEAD/public/icons/favicon.ico -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import { isImageValid } from "./isImageValid"; 2 | 3 | export { 4 | isImageValid, 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "WillLuke.nextjs.addTypesOnSave": true, 3 | "WillLuke.nextjs.hasPrompted": true 4 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahandmohammadrehzaii/unity/HEAD/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahandmohammadrehzaii/unity/HEAD/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /src/types/audience.d.ts: -------------------------------------------------------------------------------- 1 | import { Audience } from "@prisma/client"; 2 | 3 | export type AudienceType = "PUBLIC" | "PRIVATE"; -------------------------------------------------------------------------------- /public/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahandmohammadrehzaii/unity/HEAD/public/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahandmohammadrehzaii/unity/HEAD/public/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahandmohammadrehzaii/unity/HEAD/public/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/types/story.d.ts: -------------------------------------------------------------------------------- 1 | import type { Story, User, Bookmark, Like } from "@prisma/client"; 2 | 3 | export type ExtendedStory = Story & { 4 | author: User; 5 | } -------------------------------------------------------------------------------- /src/helpers/isImageValid.ts: -------------------------------------------------------------------------------- 1 | export const isImageValid = (url: string) => { 2 | const regex = /^(http(s)?:\/\/)?[a-zA-Z0-9-.]+\.[a-zA-Z]{2,6}([-a-zA-Z0-9@:%_+.~#?&//=]*)$/; 3 | return regex.test(url); 4 | }; 5 | -------------------------------------------------------------------------------- /src/app/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Loader } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/actions/getSession.ts: -------------------------------------------------------------------------------- 1 | import { authOptions } from "@/lib/auth"; 2 | import { getServerSession } from "next-auth"; 3 | 4 | export default async function getSession() { 5 | return await getServerSession(authOptions); 6 | } -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { authOptions } from "@/lib/auth"; 2 | import NextAuth from "next-auth/next"; 3 | 4 | const handler = NextAuth(authOptions); 5 | 6 | export { handler as GET, handler as POST }; -------------------------------------------------------------------------------- /src/app/p/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/s/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/u/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/(auth)/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/@post/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/search/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/bookmarks/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/p/[postId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/s/[storyId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/u/[userId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/(auth)/signin/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/(auth)/signup/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/types/bookmark.d.ts: -------------------------------------------------------------------------------- 1 | import type { Post, Like, Bookmark } from "@prisma/client"; 2 | 3 | export type ExtendedBookmark = Bookmark & { 4 | post: Post & { 5 | bookmarks: Bookmark[]; 6 | likes: Like[]; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/account/loading.tsx: -------------------------------------------------------------------------------- 1 | import { SkeletonLoader } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/account/edit/loading.tsx: -------------------------------------------------------------------------------- 1 | import { SkeletonLoader } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/account/help/loading.tsx: -------------------------------------------------------------------------------- 1 | import { SkeletonLoader } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/account/view/loading.tsx: -------------------------------------------------------------------------------- 1 | import { SkeletonLoader } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/account/enotifications/loading.tsx: -------------------------------------------------------------------------------- 1 | import { SkeletonLoader } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/app/account/notifications/loading.tsx: -------------------------------------------------------------------------------- 1 | import { SkeletonLoader } from '@/components' 2 | import React from 'react' 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Loading 11 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { usePostStore } from "./post"; 2 | import { useFollowingStore } from "./following"; 3 | import { useStoryStore } from "./story"; 4 | 5 | 6 | export { 7 | usePostStore, 8 | useFollowingStore, 9 | useStoryStore, 10 | } -------------------------------------------------------------------------------- /public/icons/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ["localhost", "res.cloudinary.com", "lh3.googleusercontent.com"], 5 | }, 6 | experimental: { 7 | serverActions: true, 8 | }, 9 | }; 10 | 11 | module.exports = nextConfig; 12 | -------------------------------------------------------------------------------- /src/store/story.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface StoryStore { 4 | caption: string; 5 | setCaption: (input: string) => void; 6 | } 7 | 8 | export const useStoryStore = create((set) => ({ 9 | caption: "", 10 | setCaption: (input: string) => set({ caption: input }), 11 | })); -------------------------------------------------------------------------------- /src/store/post.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface PostStore { 4 | postText: string; 5 | setPostText: (input: string) => void; 6 | } 7 | 8 | export const usePostStore = create((set) => ({ 9 | postText: "", 10 | setPostText: (input: string) => set({ postText: input }), 11 | })); -------------------------------------------------------------------------------- /src/lib/scheduler.ts: -------------------------------------------------------------------------------- 1 | import { deleteOldStories } from "@/actions"; 2 | 3 | const startScheduler = () => { 4 | const scheduler = () => { 5 | deleteOldStories(); 6 | setTimeout(scheduler, 1000 * 60 * 60 * 24); // Run every hour 7 | }; 8 | 9 | scheduler(); 10 | }; 11 | 12 | export default startScheduler; 13 | -------------------------------------------------------------------------------- /src/types/post.d.ts: -------------------------------------------------------------------------------- 1 | import type { Post, User, Bookmark, Like } from "@prisma/client"; 2 | 3 | export type ExtendedPost = Post & { 4 | author: User & { 5 | posts: Post[] & { 6 | likes: Like[]; 7 | bookmarks: Bookmark[]; 8 | }; 9 | }; 10 | bookmarks: Bookmark[]; 11 | likes: Like[]; 12 | } -------------------------------------------------------------------------------- /src/lib/post.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const PostSchema = z.object({ 4 | content: z.string().min(1).max(1000), 5 | image: z.string().min(1).max(1000), 6 | location: z.string().min(1).max(20), 7 | audience: z.enum(['PUBLIC', 'PRIVATE']).optional(), 8 | }); 9 | 10 | export type PostValues = z.infer; -------------------------------------------------------------------------------- /src/app/p/[postId]/components/PostContent.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/router"; 4 | import React, { useState } from 'react' 5 | 6 | const PostContent = () => { 7 | 8 | const router = useRouter(); 9 | 10 | return ( 11 |
12 | Content 13 |
14 | ) 15 | } 16 | 17 | export default PostContent 18 | -------------------------------------------------------------------------------- /public/svg/menu.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /src/hooks/use-alert-modal.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface AlertModalProps { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | } 8 | 9 | export const useAlertModal = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }), 13 | })); -------------------------------------------------------------------------------- /src/hooks/story-modal.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface StoryModalProps { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | } 8 | 9 | export const useStoryModal = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }), 13 | })); 14 | -------------------------------------------------------------------------------- /src/hooks/use-options-modal.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface OptionsModalProps { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | } 8 | 9 | export const useOptionsModal = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }), 13 | })); -------------------------------------------------------------------------------- /src/hooks/use-search-modal.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface SearchModalProps { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | } 8 | 9 | export const useSearchModal = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }), 13 | })); 14 | -------------------------------------------------------------------------------- /src/store/following.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface FollowingStore { 4 | followingIds: string[]; 5 | setFollowingIds: (newFollowingIds: string[]) => void; 6 | } 7 | 8 | export const useFollowingStore = create((set) => ({ 9 | followingIds: [], 10 | setFollowingIds: (newFollowingIds) => set({ followingIds: newFollowingIds }), 11 | })); 12 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2 } from "lucide-react" 2 | import React from 'react' 3 | 4 | const Spinner = () => { 5 | return ( 6 |
7 | 8 |
9 | ) 10 | } 11 | 12 | export default Spinner 13 | -------------------------------------------------------------------------------- /src/hooks/use-create-post-modal.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface CreatePostModalProps { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | } 8 | 9 | export const useCreatePostModal = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }), 13 | })); 14 | -------------------------------------------------------------------------------- /src/hooks/use-create-story-modal.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface CreateStoryModalProps { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | } 8 | 9 | export const useCreateStoryModal = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }), 13 | })); 14 | -------------------------------------------------------------------------------- /src/app/search/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { getUsers } from "@/actions"; 3 | import SearchBox from "./components/SearchBox"; 4 | 5 | export default async function SearchPage() { 6 | 7 | const users = await getUsers(); 8 | 9 | return ( 10 |
11 | 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/types/post-id.d.ts: -------------------------------------------------------------------------------- 1 | import type { Post, User, Bookmark, Like } from "@prisma/client"; 2 | 3 | export type IdPost = Post & { 4 | author: User & { 5 | posts: Post[] & { 6 | likes: Like[]; 7 | bookmarks: Bookmark[]; 8 | }; 9 | }; 10 | bookmarks: Array; 13 | likes: Array; 16 | }; -------------------------------------------------------------------------------- /src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | declare global { 4 | var cachedPrisma: PrismaClient | undefined; 5 | } 6 | 7 | let prisma: PrismaClient; 8 | 9 | if (process.env.NODE_ENV === 'production') { 10 | prisma = new PrismaClient() 11 | } else { 12 | if (!global.cachedPrisma) { 13 | global.cachedPrisma = new PrismaClient() 14 | } 15 | prisma = global.cachedPrisma 16 | } 17 | 18 | export const db = prisma; -------------------------------------------------------------------------------- /public/svg/user.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /public/svg/search.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /public/svg/heart-fill.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /src/actions/getUsers.ts: -------------------------------------------------------------------------------- 1 | import { getAuthSession } from "@/lib/auth"; 2 | import { db } from "@/lib/db"; 3 | 4 | const getUsers = async () => { 5 | try { 6 | 7 | const session = await getAuthSession(); 8 | 9 | const allUsers = await db.user.findMany(); 10 | 11 | const users = allUsers?.filter((user) => user.id !== session?.user?.id); 12 | 13 | return users; 14 | 15 | } catch (error) { 16 | console.log(error); 17 | return null; 18 | } 19 | }; 20 | 21 | 22 | export default getUsers; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # todo 39 | /todo.txt 40 | -------------------------------------------------------------------------------- /public/svg/bookmark.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /src/app/s/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default async function StoryLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode 7 | }) { 8 | 9 | // TODO: Change the title of the all pages accprdingly 10 | 11 | return ( 12 |
13 |
14 |
15 | {children} 16 |
17 |
18 | ) 19 | }; 20 | -------------------------------------------------------------------------------- /src/app/u/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default async function UserPageLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode 7 | }) { 8 | 9 | // TODO: Change the title of the all pages accprdingly 10 | 11 | return ( 12 |
13 |
14 |
15 | {children} 16 |
17 |
18 | ) 19 | }; 20 | -------------------------------------------------------------------------------- /src/actions/profileCount.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { db } from "@/lib/db"; 4 | 5 | const profileCount = async (userId: string) => { 6 | 7 | try { 8 | 9 | const profileView = await db.profileView.create({ 10 | data: { 11 | visitedAt: new Date(), 12 | userId: userId 13 | }, 14 | }) 15 | 16 | return profileView; 17 | 18 | } catch (error) { 19 | console.error('Error incrementing profile view count:', error); 20 | throw error; 21 | } 22 | 23 | }; 24 | 25 | export default profileCount; -------------------------------------------------------------------------------- /src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getAuthSession } from '@/lib/auth'; 2 | import { redirect } from 'next/navigation'; 3 | import React from 'react'; 4 | 5 | export default async function Layout({ 6 | children, 7 | }: { 8 | children: React.ReactNode, 9 | }) { 10 | 11 | const session = await getAuthSession(); 12 | 13 | if (session?.user) { 14 | redirect("/"); 15 | } 16 | 17 | return ( 18 |
19 | {children} 20 |
21 | ) 22 | }; 23 | -------------------------------------------------------------------------------- /src/actions/getStoryById.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { ExtendedStory } from "@/types/story"; 3 | 4 | const getStoryById = async (storyId: string) => { 5 | try { 6 | 7 | const story = await db.story.findUnique({ 8 | where: { 9 | id: storyId, 10 | }, 11 | include: { 12 | author: true, 13 | } 14 | }) 15 | 16 | return story as ExtendedStory; 17 | 18 | } catch (error) { 19 | console.log(error); 20 | return null; 21 | } 22 | }; 23 | 24 | export default getStoryById; -------------------------------------------------------------------------------- /src/actions/getStories.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { ExtendedStory } from "@/types/story"; 3 | 4 | const getStories = async () => { 5 | try { 6 | 7 | const stories = await db.story.findMany({ 8 | orderBy: { 9 | createdAt: "desc" 10 | }, 11 | include: { 12 | author: true, 13 | } 14 | }); 15 | 16 | return stories as ExtendedStory[]; 17 | } catch (error) { 18 | console.log("Unable to fetch stories", error); 19 | return []; 20 | } 21 | }; 22 | 23 | export default getStories; -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svg/bookmark-fill.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/actions/deletePost.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { getAuthSession } from "@/lib/auth"; 4 | import { db } from "@/lib/db"; 5 | import { revalidatePath } from "next/cache"; 6 | 7 | export const deletePost = async (postId: string) => { 8 | 9 | const session = await getAuthSession(); 10 | 11 | try { 12 | 13 | await db.post.delete({ 14 | where: { 15 | id: postId, 16 | authorId: session?.user.id!, 17 | } 18 | }); 19 | 20 | } catch (error) { 21 | console.log("Error creating story", error); 22 | } 23 | 24 | revalidatePath("/"); 25 | }; -------------------------------------------------------------------------------- /src/actions/getPostById.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { IdPost } from "@/types/post-id"; 3 | 4 | const getPostById = async (postId: string) => { 5 | try { 6 | 7 | const post = await db.post.findUnique({ 8 | where: { 9 | id: postId, 10 | }, 11 | include: { 12 | author: true, 13 | likes: true, 14 | bookmarks: true, 15 | } 16 | }); 17 | 18 | return post as IdPost; 19 | 20 | } catch (error) { 21 | console.log(error); 22 | return null; 23 | } 24 | }; 25 | 26 | export default getPostById; -------------------------------------------------------------------------------- /src/app/bookmarks/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Sidebar } from '@/components'; 2 | import { getAuthSession } from '@/lib/auth'; 3 | import React from 'react'; 4 | 5 | export default async function BookmarkPageLayout({ 6 | children, 7 | }: { 8 | children: React.ReactNode 9 | }) { 10 | 11 | const session = await getAuthSession(); 12 | 13 | return ( 14 |
15 |
16 | 17 | {children} 18 |
19 |
20 | ) 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/sidebar/Followbar.tsx: -------------------------------------------------------------------------------- 1 | import { getUsers, getViewCount } from '@/actions'; 2 | import { getAuthSession } from '@/lib/auth'; 3 | import Overview from './Overview'; 4 | 5 | const Followbar = async () => { 6 | 7 | const session = await getAuthSession(); 8 | 9 | const users = await getUsers(); 10 | 11 | return ( 12 |
13 |
14 | 15 |
16 |
17 | ) 18 | } 19 | 20 | export default Followbar 21 | -------------------------------------------------------------------------------- /src/app/search/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Header, Sidebar } from '@/components'; 2 | import { getAuthSession } from '@/lib/auth'; 3 | import React from 'react'; 4 | 5 | export default async function SearchPageLayout({ 6 | children, 7 | }: { 8 | children: React.ReactNode 9 | }) { 10 | 11 | const session = await getAuthSession(); 12 | 13 | return ( 14 |
15 |
16 | 17 | {children} 18 |
19 |
20 | ) 21 | }; 22 | -------------------------------------------------------------------------------- /public/svg/add.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 8 | 10 | -------------------------------------------------------------------------------- /public/svg/post.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 8 | 10 | -------------------------------------------------------------------------------- /src/actions/scheduleStory.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | 3 | const deleteOldStories = async () => { 4 | 5 | const fortyEightHoursAgo = new Date(); 6 | fortyEightHoursAgo.setHours(fortyEightHoursAgo.getHours() - 48); // Extend to 48 hours 7 | 8 | try { 9 | await db.story.deleteMany({ 10 | where: { 11 | createdAt: { 12 | lt: fortyEightHoursAgo, 13 | }, 14 | }, 15 | }); 16 | 17 | console.log("Old stories deleted successfully."); 18 | } catch (error) { 19 | console.error("Error deleting old stories:", error); 20 | } 21 | }; 22 | 23 | export default deleteOldStories; -------------------------------------------------------------------------------- /public/svg/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/actions/deleteStory.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { getAuthSession } from "@/lib/auth"; 4 | import { db } from "@/lib/db"; 5 | import { revalidatePath } from "next/cache"; 6 | 7 | export const deleteStory = async (storyId: string) => { 8 | 9 | const session = await getAuthSession(); 10 | 11 | try { 12 | 13 | await db.story.delete({ 14 | where: { 15 | authorId: session?.user.id!, 16 | id: storyId! as string, 17 | // author: session?.user! as any, 18 | } 19 | }); 20 | 21 | } catch (error) { 22 | console.log("Error creating story", error); 23 | } 24 | 25 | revalidatePath("/"); 26 | }; -------------------------------------------------------------------------------- /src/actions/getUserBookmarks.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { ExtendedBookmark } from "@/types/bookmark"; 3 | 4 | const getUserBookmarks = async (userId: string) => { 5 | try { 6 | 7 | const bookmarks = await db.bookmark.findMany({ 8 | where: { 9 | authorId: userId 10 | }, 11 | orderBy: { 12 | createdAt: "desc" 13 | }, 14 | include: { 15 | post: true, 16 | } 17 | }); 18 | 19 | return bookmarks as ExtendedBookmark[]; 20 | 21 | } catch (error) { 22 | console.log(error); 23 | return null; 24 | } 25 | }; 26 | 27 | export default getUserBookmarks; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/types/user.d.ts: -------------------------------------------------------------------------------- 1 | import type { Session, User } from 'next-auth' 2 | import type { JWT } from 'next-auth/jwt' 3 | import type { Post, Story, Like, Bookmark, Comment, ProfileView } from '@prisma/client' 4 | 5 | type UserId = string 6 | 7 | export type ExtendedUser = User & { 8 | id: UserId 9 | username?: string | null 10 | bio?: string | null 11 | coverImage?: string | null 12 | profileImage?: string | null 13 | followingIds?: UserId[] 14 | followersIds?: UserId[] 15 | posts: Post[] & { 16 | likes: Like[]; 17 | bookmarks: Bookmark[]; 18 | }[]; 19 | likes: Like[]; 20 | bookmarks: Bookmark[]; 21 | comments: Comment[]; 22 | story: Story[]; 23 | profile: ProfileView[]; 24 | } -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { toast } from './use-toast'; 2 | import { useCreatePostModal } from './use-create-post-modal'; 3 | import { useAlertModal } from './use-alert-modal'; 4 | import { useOnClickOutside } from './use-on-click-outside'; 5 | import { useOptionsModal } from './use-options-modal'; 6 | import { useCustomToast } from './use-auth-toast'; 7 | import { useCreateStoryModal } from './use-create-story-modal'; 8 | import { useStoryModal } from './story-modal'; 9 | import { useSearchModal } from "./use-search-modal"; 10 | 11 | export { 12 | toast, 13 | useCreatePostModal, 14 | useAlertModal, 15 | useOnClickOutside, 16 | useOptionsModal, 17 | useCustomToast, 18 | useCreateStoryModal, 19 | useStoryModal, 20 | useSearchModal, 21 | } -------------------------------------------------------------------------------- /public/svg/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/s/[storyId]/DeleteButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components"; 4 | import { Trash } from "lucide-react"; 5 | import { useRouter } from "next/navigation"; 6 | import { FC } from "react"; 7 | 8 | interface DeleteButtonProps { 9 | storyId: string; 10 | delete: (storyId: string) => Promise; 11 | } 12 | 13 | const DeleteButton: FC = ({ 14 | storyId, delete: handleDelete 15 | }) => { 16 | 17 | const router = useRouter(); 18 | 19 | return ( 20 | 26 | ) 27 | } 28 | 29 | 30 | export default DeleteButton; -------------------------------------------------------------------------------- /src/app/p/[postId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { bookmark, deletePost, follow, getPostById, like } from "@/actions"; 2 | import { Modal, PostContent, Sidebar } from '@/components'; 3 | import { getAuthSession } from '@/lib/auth'; 4 | import { IdPost } from "@/types/post-id"; 5 | 6 | export default async function PostPage({ 7 | params 8 | }: { 9 | params: { postId: string } 10 | }) { 11 | 12 | const session = await getAuthSession(); 13 | 14 | const post: IdPost | null = await getPostById(params?.postId); 15 | 16 | return ( 17 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /public/svg/chat.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 9 | -------------------------------------------------------------------------------- /src/components/Providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from 'react'; 4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 5 | import { SessionProvider } from 'next-auth/react'; 6 | import NextTopLoader from 'nextjs-toploader'; 7 | 8 | const Providers = ({ children }: { children: React.ReactNode }) => { 9 | 10 | const queryClient = new QueryClient(); 11 | 12 | return ( 13 | 14 | 15 | 16 | {children} 17 | 18 | 19 | ) 20 | }; 21 | 22 | export default Providers; -------------------------------------------------------------------------------- /src/app/@post/(.)p/components/ModalContent.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Modal } from "@/components"; 4 | import { useRouter } from "next/navigation"; 5 | import { FC, useEffect, useState } from "react"; 6 | 7 | interface ModalContentProps { 8 | 9 | } 10 | 11 | const ModalContent: FC = ({ }) => { 12 | 13 | const router = useRouter(); 14 | 15 | const [isOpen, setIsOpen] = useState(false); 16 | 17 | useEffect(() => { 18 | setIsOpen(true); 19 | }, []); 20 | 21 | const handleClose = () => { 22 | setIsOpen(false); 23 | router.back(); 24 | }; 25 | 26 | return ( 27 |
28 | Post Modal Content 29 |
30 | ); 31 | } 32 | 33 | export default ModalContent; -------------------------------------------------------------------------------- /src/actions/createStory.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { getAuthSession } from "@/lib/auth"; 4 | import { db } from "@/lib/db"; 5 | import { revalidatePath } from "next/cache"; 6 | 7 | export const createStory = async (postData: FormData) => { 8 | // "use server"; 9 | 10 | const session = await getAuthSession(); 11 | 12 | const content = postData.get("content"); 13 | const image = postData.get("image"); 14 | 15 | try { 16 | 17 | await db.story.create({ 18 | data: { 19 | content: content as string, 20 | image: image as string, 21 | authorId: session?.user.id! as string, 22 | } 23 | }); 24 | 25 | } catch (error) { 26 | console.log("Error creating story", error); 27 | } 28 | 29 | revalidatePath("/"); 30 | }; -------------------------------------------------------------------------------- /src/app/@post/(.)p/[postId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { bookmark, deletePost, follow, getPostById, like } from '@/actions'; 2 | import { Modal } from '@/components'; 3 | import { getAuthSession } from '@/lib/auth'; 4 | import { ExtendedPost } from '@/types/post'; 5 | import { IdPost } from '@/types/post-id'; 6 | import React from 'react'; 7 | 8 | export default async function PostPage({ 9 | params 10 | }: { 11 | params: { postId: string } 12 | }) { 13 | 14 | const session = await getAuthSession(); 15 | 16 | const post: IdPost | null = await getPostById(params?.postId); 17 | 18 | return ( 19 | 27 | ) 28 | }; -------------------------------------------------------------------------------- /src/hooks/use-auth-toast.tsx: -------------------------------------------------------------------------------- 1 | import { buttonVariants } from "@/components/ui/Button"; 2 | import Link from "next/link"; 3 | import { toast } from "./use-toast"; 4 | 5 | export const useCustomToast = () => { 6 | const authToast = () => { 7 | const { dismiss } = toast({ 8 | title: 'Login required!', 9 | description: 'You must be logged in to perform this action.', 10 | variant: 'destructive', 11 | action: ( 12 | dismiss()} 15 | className={buttonVariants({ variant: 'outline', className: "text-slate-600" })} 16 | > 17 | Login 18 | 19 | ) 20 | }) 21 | } 22 | 23 | return { authToast }; 24 | }; -------------------------------------------------------------------------------- /src/components/ui/Label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/actions/getUserPosts.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { ExtendedPost } from "@/types/post"; 3 | 4 | const getUserPosts = async (userId: string) => { 5 | try { 6 | 7 | const posts = await db.post.findMany({ 8 | where: { 9 | authorId: userId 10 | }, 11 | include: { 12 | author: { 13 | include: { 14 | posts: true 15 | } 16 | }, 17 | likes: true, 18 | bookmarks: true 19 | }, 20 | orderBy: { 21 | createdAt: "desc" 22 | } 23 | }); 24 | 25 | return posts as ExtendedPost[]; 26 | 27 | } catch (error) { 28 | console.log(error); 29 | return null; 30 | } 31 | }; 32 | 33 | export default getUserPosts; -------------------------------------------------------------------------------- /src/components/ui/Textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |