├── .env ├── .gitignore ├── README.md ├── app ├── api │ ├── auth │ │ ├── [...nextauth] │ │ │ └── route.ts │ │ └── token │ │ │ └── route.ts │ └── upload │ │ └── route.ts ├── create-project │ └── page.tsx ├── edit-project │ └── [id] │ │ └── page.tsx ├── favicon.ico ├── globals.css ├── layout.tsx ├── page.tsx ├── profile │ └── [id] │ │ └── page.tsx └── project │ └── [id] │ └── page.tsx ├── common.types.ts ├── components ├── AuthProviders.tsx ├── Button.tsx ├── Categories.tsx ├── CustomMenu.tsx ├── Footer.tsx ├── FormField.tsx ├── LoadMore.tsx ├── Modal.tsx ├── Navbar.tsx ├── ProfileMenu.tsx ├── ProfilePage.tsx ├── ProjectActions.tsx ├── ProjectCard.tsx ├── ProjectForm.tsx └── RelatedProjects.tsx ├── constants └── index.ts ├── grafbase ├── .env └── grafbase.config.ts ├── graphql └── index.ts ├── lib ├── actions.ts └── session.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── arrow-down.svg ├── close.svg ├── dot.svg ├── email.svg ├── eye.svg ├── hearth-purple.svg ├── hearth-white.svg ├── hearth.svg ├── logo-purple.svg ├── logo.svg ├── magnifying-glass.svg ├── minus.svg ├── next copy.svg ├── next.svg ├── pencile.svg ├── plus-round.svg ├── plus.svg ├── profile-post.png ├── save.svg ├── share.svg ├── socials.svg ├── trash.svg ├── upload.svg ├── vercel copy.svg └── vercel.svg ├── tailwind.config.js └── tsconfig.json /.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_GRAFBASE_API_URL=https://grafbaseflexibble-main-adrianhajdin.grafbase.app/graphql 2 | NEXT_PUBLIC_GRAFBASE_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2ODc0NTIzMjAsImlzcyI6ImdyYWZiYXNlIiwiYXVkIjoiMDFIM0haWTBERktBTk1NV1NFWlZaOTVDVDIiLCJqdGkiOiIwMUgzSFpZMEhLM0JUWjFOVEE2SDNBWVFYVCIsImVudiI6InByb2R1Y3Rpb24iLCJwdXJwb3NlIjoicHJvamVjdC1hcGkta2V5In0.iOzRkletbmB1yw3CG2tRDiUcYqViXLxz_4h4XkZdwL0 3 | 4 | GOOGLE_CLIENT_ID=81888112860-pp2bpof6404005gaf76hpcm1t0vr61dj.apps.googleusercontent.com 5 | GOOGLE_CLIENT_SECRET=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 6 | 7 | NEXTAUTH_SECRET=Yp1jd6zPakYbLYxvl3PRo2/vdZwZQnxS+G+0YaHMy2o= 8 | NEXTAUTH_URL=http://localhost:3000 9 | 10 | CLOUDINARY_NAME=adrian-hajdin 11 | CLOUDINARY_KEY=423376649442326 12 | CLOUDINARY_SECRET=E_HHxWHTpUYXk-A-Tcos0H3cLuI -------------------------------------------------------------------------------- /.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 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | 3 | import { authOptions } from '@/lib/session'; 4 | 5 | const handler = NextAuth(authOptions); 6 | 7 | export { handler as GET, handler as POST }; 8 | -------------------------------------------------------------------------------- /app/api/auth/token/route.ts: -------------------------------------------------------------------------------- 1 | import { getToken } from 'next-auth/jwt'; 2 | import { NextRequest, NextResponse } from 'next/server'; 3 | 4 | const secret = process.env.NEXTAUTH_SECRET; 5 | 6 | export async function GET(req: NextRequest) { 7 | const token = await getToken({ req, secret, raw: true }); 8 | 9 | return NextResponse.json({ token }, { status: 200 }) 10 | } -------------------------------------------------------------------------------- /app/api/upload/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { v2 as cloudinary } from 'cloudinary'; 3 | 4 | cloudinary.config({ 5 | cloud_name: process.env.CLOUDINARY_NAME, 6 | api_key: process.env.CLOUDINARY_KEY, 7 | api_secret: process.env.CLOUDINARY_SECRET, 8 | }); 9 | 10 | export async function POST(request: Request) { 11 | const { path } = await request.json(); 12 | 13 | if(!path) { 14 | return NextResponse.json( 15 | { message: 'Image path is required'}, 16 | { status : 400 } 17 | ) 18 | } 19 | 20 | try { 21 | const options = { 22 | use_filename: true, 23 | unique_filename: false, 24 | overwrite: true, 25 | transformation: [{ width: 1000, height: 752, crop: 'scale' }] 26 | } 27 | 28 | const result = await cloudinary.uploader.upload(path, options); 29 | 30 | return NextResponse.json(result, { status: 200 }) 31 | } catch (error) { 32 | return NextResponse.json({ message: error }, { status: 500 }) 33 | } 34 | } -------------------------------------------------------------------------------- /app/create-project/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | import Modal from "@/components/Modal" 3 | import ProjectForm from "@/components/ProjectForm" 4 | import { getCurrentUser } from "@/lib/session" 5 | 6 | const CreateProject = async () => { 7 | const session = await getCurrentUser(); 8 | 9 | if(!session?.user) redirect('/') 10 | 11 | return ( 12 | 13 |

Create a New Project

14 | 15 | 16 |
17 | ) 18 | } 19 | 20 | export default CreateProject -------------------------------------------------------------------------------- /app/edit-project/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | import Modal from "@/components/Modal" 3 | import ProjectForm from "@/components/ProjectForm" 4 | import { getCurrentUser } from "@/lib/session" 5 | import { getProjectDetails } from "@/lib/actions" 6 | import { ProjectInterface } from "@/common.types" 7 | 8 | const EditProject = async ({ params: { id }}: { params: { id: string }}) => { 9 | const session = await getCurrentUser(); 10 | 11 | if(!session?.user) redirect('/') 12 | 13 | const result = await getProjectDetails(id) as { project?: ProjectInterface } 14 | 15 | return ( 16 | 17 |

Edit Project

18 | 19 | 20 |
21 | ) 22 | } 23 | 24 | export default EditProject -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/grafbase_flexibble/0c740b9bb7973ebf0fee78949c11dd6feb22b11e/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | * { 8 | margin: 0; 9 | padding: 0; 10 | box-sizing: border-box; 11 | } 12 | 13 | body { 14 | font-family: Inter; 15 | } 16 | 17 | .flexCenter { 18 | @apply flex justify-center items-center; 19 | } 20 | 21 | .flexBetween { 22 | @apply flex justify-between items-center; 23 | } 24 | 25 | .flexStart { 26 | @apply flex items-center justify-start; 27 | } 28 | 29 | .text-small { 30 | @apply text-sm font-medium; 31 | } 32 | 33 | .paddings { 34 | @apply lg:px-20 py-6 px-5; 35 | } 36 | 37 | ::-webkit-scrollbar { 38 | width: 5px; 39 | height: 4px; 40 | } 41 | 42 | ::-webkit-scrollbar-thumb { 43 | background: #888; 44 | border-radius: 12px; 45 | } 46 | 47 | .modal-head-text { 48 | @apply md:text-5xl text-3xl font-extrabold text-left max-w-5xl w-full; 49 | } 50 | 51 | .no-result-text { 52 | @apply w-full text-center my-10 px-2; 53 | } 54 | 55 | /* Project Details */ 56 | .user-actions_section { 57 | @apply fixed max-md:hidden flex gap-4 flex-col right-10 top-20; 58 | } 59 | 60 | .user-info { 61 | @apply flex flex-wrap whitespace-nowrap text-sm font-normal gap-2 w-full; 62 | } 63 | 64 | /* Home */ 65 | .projects-grid { 66 | @apply grid xl:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-10 mt-10 w-full; 67 | } 68 | 69 | /* Project Actions */ 70 | .edit-action_btn { 71 | @apply p-3 text-gray-100 bg-light-white-400 rounded-lg text-sm font-medium; 72 | } 73 | 74 | .delete-action_btn { 75 | @apply p-3 text-gray-100 hover:bg-red-600 rounded-lg text-sm font-medium; 76 | } 77 | 78 | /* Related Project Card */ 79 | .related_project-card { 80 | @apply flex-col rounded-2xl min-w-[210px] min-h-[197px]; 81 | } 82 | 83 | .related_project-card_title { 84 | @apply justify-end items-end w-full h-1/3 bg-gradient-to-b from-transparent to-black/50 rounded-b-2xl gap-2 absolute bottom-0 right-0 font-semibold text-lg text-white p-4; 85 | } 86 | 87 | .related_projects-grid { 88 | @apply grid xl:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-8 mt-5; 89 | } 90 | 91 | /* Custom Menu */ 92 | .custom_menu-btn { 93 | @apply gap-4 w-full rounded-md bg-light-white-100 p-4 text-base outline-none capitalize; 94 | } 95 | 96 | .custom_menu-items { 97 | @apply flex-col absolute left-0 mt-2 xs:min-w-[300px] w-fit max-h-64 origin-top-right rounded-xl bg-white border border-nav-border shadow-menu overflow-y-auto; 98 | } 99 | 100 | .custom_menu-item { 101 | @apply text-left w-full px-5 py-2 text-sm hover:bg-light-white-100 self-start whitespace-nowrap capitalize; 102 | } 103 | 104 | /* Footer */ 105 | .footer { 106 | @apply flex-col paddings w-full gap-20 bg-light-white; 107 | } 108 | 109 | .footer_copyright { 110 | @apply max-sm:flex-col w-full text-sm font-normal; 111 | } 112 | 113 | .footer_column { 114 | @apply flex-1 flex flex-col gap-3 text-sm min-w-max; 115 | } 116 | 117 | /* Form Field */ 118 | .form_field-input { 119 | @apply w-full outline-0 bg-light-white-100 rounded-xl p-4; 120 | } 121 | 122 | /* Modal */ 123 | .modal { 124 | @apply fixed z-10 left-0 right-0 top-0 bottom-0 mx-auto bg-black/80; 125 | } 126 | 127 | .modal_wrapper { 128 | @apply flex justify-start items-center flex-col absolute h-[95%] w-full bottom-0 bg-white rounded-t-3xl lg:px-40 px-8 pt-14 pb-72 overflow-auto; 129 | } 130 | 131 | /* Navbar */ 132 | .navbar { 133 | @apply py-5 px-8 border-b border-nav-border gap-4; 134 | } 135 | 136 | /* Profile Menu */ 137 | .profile_menu-items { 138 | @apply flex-col absolute right-1/2 translate-x-1/2 mt-3 p-7 sm:min-w-[300px] min-w-max rounded-xl bg-white border border-nav-border shadow-menu; 139 | } 140 | 141 | /* Profile Card */ 142 | .profile_card-title { 143 | @apply justify-end items-end w-full h-1/3 bg-gradient-to-b from-transparent to-black/50 rounded-b-2xl gap-2 absolute bottom-0 right-0 font-semibold text-lg text-white p-4; 144 | } 145 | 146 | /* Project Form */ 147 | .form { 148 | @apply flex-col w-full lg:pt-24 pt-12 gap-10 text-lg max-w-5xl mx-auto; 149 | } 150 | 151 | .form_image-container { 152 | @apply w-full lg:min-h-[400px] min-h-[200px] relative; 153 | } 154 | 155 | .form_image-label { 156 | @apply z-10 text-center w-full h-full p-20 text-gray-100 border-2 border-gray-50 border-dashed; 157 | } 158 | 159 | .form_image-input { 160 | @apply absolute z-30 w-full opacity-0 h-full cursor-pointer; 161 | } 162 | 163 | /* Profile Projects */ 164 | .profile_projects { 165 | @apply grid xl:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-8 mt-5; 166 | } 167 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css'; 2 | import Navbar from '@/components/Navbar'; 3 | import Footer from '@/components/Footer'; 4 | 5 | export const metadata = { 6 | title: 'Flexibble', 7 | description: 'Showcase and discover remarable developer projects', 8 | } 9 | 10 | export default function RootLayout({ 11 | children, 12 | }: { 13 | children: React.ReactNode 14 | }) { 15 | return ( 16 | 17 | 18 | 19 |
20 | {children} 21 |
22 |