├── .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 |
23 |
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { ProjectInterface } from "@/common.types";
2 | import Categories from "@/components/Categories";
3 | import LoadMore from "@/components/LoadMore";
4 | import ProjectCard from "@/components/ProjectCard";
5 | import { fetchAllProjects } from "@/lib/actions";
6 |
7 | type ProjectSearch = {
8 | projectSearch: {
9 | edges: { node: ProjectInterface }[];
10 | pageInfo: {
11 | hasPreviousPage: boolean;
12 | hasNextPage: boolean;
13 | startCursor: string;
14 | endCursor: string;
15 | }
16 | }
17 | }
18 |
19 | type SearchParams = {
20 | category?: string;
21 | endcursor?: string;
22 | }
23 |
24 | type Props = {
25 | searchParams: SearchParams
26 | }
27 |
28 | export const dynamic = 'force-dynamic';
29 | export const dynamicParams = true;
30 | export const revalidate = 0;
31 |
32 | const Home = async ({ searchParams: { category, endcursor }}: Props) => {
33 | const data = await fetchAllProjects(category, endcursor) as ProjectSearch;
34 |
35 | const projectsToDisplay = data?.projectSearch?.edges || [];
36 |
37 | if(projectsToDisplay.length === 0) {
38 | return (
39 |
40 |
41 |
42 | No projects found, go create some first.
43 |
44 | )
45 | }
46 |
47 | const pagination = data?.projectSearch?.pageInfo;
48 |
49 | return (
50 |
51 |
52 |
53 |
54 | {projectsToDisplay.map(({ node }: { node: ProjectInterface }) => (
55 |
64 | ))}
65 |
66 |
67 |
73 |
74 | )
75 | }
76 |
77 | export default Home;
--------------------------------------------------------------------------------
/app/profile/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { UserProfile } from '@/common.types'
2 | import ProfilePage from '@/components/ProfilePage'
3 | import { getUserProjects } from '@/lib/actions'
4 |
5 | type Props = {
6 | params: {
7 | id: string
8 | }
9 | }
10 |
11 | const UserProfile = async ({ params }: Props) => {
12 | const result = await getUserProjects(params.id, 100) as { user: UserProfile }
13 |
14 | if(!result?.user) {
15 | return Failed to fetch user info
16 | }
17 |
18 | return
19 | }
20 |
21 | export default UserProfile
--------------------------------------------------------------------------------
/app/project/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image"
2 | import Link from "next/link"
3 |
4 | import { getCurrentUser } from "@/lib/session"
5 | import { getProjectDetails } from "@/lib/actions"
6 | import Modal from "@/components/Modal"
7 | // import ProjectActions from "@/components/ProjectActions"
8 | import RelatedProjects from "@/components/RelatedProjects"
9 | import { ProjectInterface } from "@/common.types"
10 | import ProjectActions from "@/components/ProjectActions"
11 |
12 | const Project = async ({ params: { id } }: { params: { id: string } }) => {
13 | const session = await getCurrentUser()
14 | const result = await getProjectDetails(id) as { project?: ProjectInterface}
15 |
16 | if (!result?.project) return (
17 | Failed to fetch project info
18 | )
19 |
20 | const projectDetails = result?.project
21 |
22 | const renderLink = () => `/profile/${projectDetails?.createdBy?.id}`
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
36 |
37 |
38 |
39 |
40 | {projectDetails?.title}
41 |
42 |
43 |
44 | {projectDetails?.createdBy?.name}
45 |
46 |
47 |
48 | {projectDetails?.category}
49 |
50 |
51 |
52 |
53 |
54 | {session?.user?.email === projectDetails?.createdBy?.email && (
55 |
58 | )}
59 |
60 |
61 |
70 |
71 |
72 |
73 | {projectDetails?.description}
74 |
75 |
76 |
77 |
78 | 🖥 Github
79 |
80 |
81 |
82 | 🚀 Live Site
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | )
104 | }
105 |
106 | export default Project
107 |
--------------------------------------------------------------------------------
/common.types.ts:
--------------------------------------------------------------------------------
1 | import { User, Session } from 'next-auth'
2 |
3 | export type FormState = {
4 | title: string;
5 | description: string;
6 | image: string;
7 | liveSiteUrl: string;
8 | githubUrl: string;
9 | category: string;
10 | };
11 |
12 | export interface ProjectInterface {
13 | title: string;
14 | description: string;
15 | image: string;
16 | liveSiteUrl: string;
17 | githubUrl: string;
18 | category: string;
19 | id: string;
20 | createdBy: {
21 | name: string;
22 | email: string;
23 | avatarUrl: string;
24 | id: string;
25 | };
26 | }
27 |
28 | export interface UserProfile {
29 | id: string;
30 | name: string;
31 | email: string;
32 | description: string | null;
33 | avatarUrl: string;
34 | githubUrl: string | null;
35 | linkedinUrl: string | null;
36 | projects: {
37 | edges: { node: ProjectInterface }[];
38 | pageInfo: {
39 | hasPreviousPage: boolean;
40 | hasNextPage: boolean;
41 | startCursor: string;
42 | endCursor: string;
43 | };
44 | };
45 | }
46 |
47 | export interface SessionInterface extends Session {
48 | user: User & {
49 | id: string;
50 | name: string;
51 | email: string;
52 | avatarUrl: string;
53 | };
54 | }
55 |
56 | export interface ProjectForm {
57 | title: string;
58 | description: string;
59 | image: string;
60 | liveSiteUrl: string;
61 | githubUrl: string;
62 | category: string;
63 | }
--------------------------------------------------------------------------------
/components/AuthProviders.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { getProviders, signIn } from 'next-auth/react';
4 | import { useState, useEffect } from 'react';
5 | import Button from './Button';
6 |
7 | type Provider = {
8 | id: string;
9 | name: string;
10 | type: string;
11 | signinUrl: string;
12 | callbackUrl: string;
13 | signinUrlParams?: Record | null;
14 | }
15 |
16 | type Providers = Record;
17 |
18 | const AuthProviders = () => {
19 | const [providers, setProviders] = useState(null);
20 |
21 | useEffect(() => {
22 | const fetchProviders = async () => {
23 | const res = await getProviders();
24 |
25 | console.log(res);
26 |
27 | setProviders(res);
28 | }
29 |
30 | fetchProviders();
31 | }, []);
32 |
33 | if(providers) {
34 | return (
35 |
36 | {Object.values(providers).map((provider: Provider, i) => (
37 |
44 | )
45 | }
46 | }
47 |
48 | export default AuthProviders
--------------------------------------------------------------------------------
/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import { MouseEventHandler } from "react";
3 |
4 | type Props = {
5 | title: string;
6 | leftIcon?: string | null;
7 | rightIcon?: string | null;
8 | handleClick?: MouseEventHandler;
9 | isSubmitting?: boolean;
10 | type?: 'button' | 'submit';
11 | bgColor?: string;
12 | textColor?: string;
13 | }
14 |
15 | const Button = ({ title, leftIcon, rightIcon, handleClick, isSubmitting, type, bgColor, textColor }: Props) => {
16 | return (
17 |
30 | )
31 | }
32 |
33 | export default Button
--------------------------------------------------------------------------------
/components/Categories.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { usePathname, useRouter, useSearchParams } from 'next/navigation';
4 |
5 | import { categoryFilters } from '@/constants';
6 |
7 | const Categories = () => {
8 | const router = useRouter();
9 | const pathName = usePathname();
10 | const searchParams = useSearchParams();
11 |
12 | const category = searchParams.get('category');
13 |
14 | const handleTags = (filter: string) => {
15 | router.push(`${pathName}?category=${filter}`);
16 | }
17 |
18 | return (
19 |
20 |
21 | {categoryFilters.map((filter) => (
22 |
32 | ))}
33 |
34 |
35 | )
36 | }
37 |
38 | export default Categories
--------------------------------------------------------------------------------
/components/CustomMenu.tsx:
--------------------------------------------------------------------------------
1 | import { Menu } from '@headlessui/react';
2 | import Image from 'next/image'
3 |
4 | type Props = {
5 | title: string;
6 | state: string;
7 | filters: Array;
8 | setState: (value: string) => void;
9 | }
10 |
11 | const CustomMenu = ({ title, state, filters, setState }: Props) => {
12 | return (
13 |
14 |
17 |
44 |
45 | )
46 | }
47 |
48 | export default CustomMenu
--------------------------------------------------------------------------------
/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { footerLinks } from '@/constants'
2 | import Image from 'next/image'
3 | import Link from 'next/link';
4 |
5 | type ColumnProps = {
6 | title: string;
7 | links: Array;
8 | }
9 |
10 | const FooterColumn = ({ title, links }: ColumnProps) => (
11 |
12 |
{title}
13 |
14 | {links.map((link) => {link})}
15 |
16 |
17 | )
18 |
19 | const Footer = () => {
20 | return (
21 |
59 | )
60 | }
61 |
62 | export default Footer
--------------------------------------------------------------------------------
/components/FormField.tsx:
--------------------------------------------------------------------------------
1 | type Props = {
2 | type?: string;
3 | title: string;
4 | state: string;
5 | placeholder: string;
6 | isTextArea?: boolean;
7 | setState: (value: string) => void;
8 | }
9 |
10 | const FormField = ({ type, title, state, placeholder, isTextArea, setState }: Props) => {
11 | return (
12 |
13 |
16 |
17 | {isTextArea ? (
18 |
36 | )
37 | }
38 |
39 | export default FormField
--------------------------------------------------------------------------------
/components/LoadMore.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from 'next/navigation';
4 |
5 | import Button from './Button';
6 |
7 | type Props = {
8 | startCursor: string;
9 | endCursor: string;
10 | hasPreviousPage: boolean;
11 | hasNextPage: boolean;
12 | }
13 |
14 | const LoadMore = ({ startCursor, endCursor, hasPreviousPage, hasNextPage }: Props) => {
15 | const router = useRouter();
16 |
17 | const handleNavigation = (direction: string) => {
18 | const currentParams = new URLSearchParams(window.location.search);
19 |
20 | if(direction === 'next' && hasNextPage) {
21 | currentParams.delete("startcursor")
22 | currentParams.set("endcursor", endCursor);
23 | } else if (direction === "first" && hasPreviousPage) {
24 | currentParams.delete("endcursor")
25 | currentParams.set("startcursor", startCursor);
26 | }
27 |
28 | const newSearchParams = currentParams.toString();
29 | const newPathname = `${window.location.pathname}?${newSearchParams}`
30 |
31 | router.push(newPathname);
32 | }
33 |
34 | return (
35 |
36 | {hasPreviousPage && (
37 |
43 | )
44 | }
45 |
46 | export default LoadMore
--------------------------------------------------------------------------------
/components/Modal.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useCallback, useRef, ReactNode } from 'react';
4 | import { useRouter } from 'next/navigation';
5 | import Image from 'next/image';
6 |
7 | const Modal = ({ children }: { children: ReactNode }) => {
8 | const overlay = useRef(null);
9 | const wrapper = useRef(null);
10 | const router = useRouter();
11 |
12 | const onDismiss = useCallback(() => {
13 | router.push('/');
14 | }, [router]);
15 |
16 | const handleClick = useCallback((e: React.MouseEvent) => {
17 | if((e.target === overlay.current) && onDismiss) {
18 | onDismiss();
19 | }
20 | }, [onDismiss, overlay]);
21 |
22 |
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 | {children}
31 |
32 |
33 | )
34 | }
35 |
36 | export default Modal
--------------------------------------------------------------------------------
/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import { NavLinks } from '@/constants'
2 | import Image from 'next/image'
3 | import Link from 'next/link'
4 | import AuthProviders from './AuthProviders';
5 | import { getCurrentUser } from '@/lib/session';
6 | import { signOut } from 'next-auth/react';
7 | import ProfileMenu from './ProfileMenu';
8 |
9 | const Navbar = async () => {
10 | const session = await getCurrentUser();
11 |
12 | return (
13 |
46 | )
47 | }
48 |
49 | export default Navbar
--------------------------------------------------------------------------------
/components/ProfileMenu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link";
4 | import Image from "next/image";
5 | import { signOut } from "next-auth/react";
6 | import { Fragment, useState } from "react";
7 | import { Menu, Transition } from "@headlessui/react";
8 |
9 | import { SessionInterface } from "@/common.types";
10 |
11 | const ProfileMenu = ({ session }: { session: SessionInterface }) => {
12 | const [openModal, setOpenModal] = useState(false);
13 |
14 | return (
15 |
16 |
78 |
79 | )
80 | }
81 |
82 | export default ProfileMenu
--------------------------------------------------------------------------------
/components/ProfilePage.tsx:
--------------------------------------------------------------------------------
1 | import { ProjectInterface, UserProfile } from '@/common.types'
2 | import Image from 'next/image'
3 |
4 | import Link from 'next/link'
5 | import Button from "./Button";
6 | import ProjectCard from './ProjectCard';
7 |
8 | type Props = {
9 | user: UserProfile;
10 | }
11 |
12 | const ProfilePage = ({ user }: Props) => (
13 |
14 |
15 |
16 |
17 |
{user?.name}
18 |
I’m Software Engineer at JSM 👋
19 |
20 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {user?.projects?.edges?.length > 0 ? (
34 |
41 | ) : (
42 |
49 | )}
50 |
51 |
52 |
53 | Recent Work
54 |
55 |
56 | {user?.projects?.edges?.map(
57 | ({ node }: { node: ProjectInterface }) => (
58 |
67 | )
68 | )}
69 |
70 |
71 |
72 | )
73 |
74 | export default ProfilePage
--------------------------------------------------------------------------------
/components/ProjectActions.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { deleteProject, fetchToken } from '@/lib/actions'
4 | import Image from 'next/image'
5 | import Link from 'next/link'
6 | import { useRouter } from 'next/navigation'
7 | import React, { useState } from 'react'
8 |
9 | const ProjectActions = ({ projectId }: { projectId: string }) => {
10 | const router = useRouter();
11 | const [isDeleting, setIsDeleting] = useState(false);
12 |
13 | const handleDeleteProject = async () => {
14 | setIsDeleting(true);
15 |
16 | const { token } = await fetchToken();
17 |
18 | try {
19 | await deleteProject(projectId, token);
20 |
21 | router.push('/');
22 | } catch (error) {
23 | console.log(error);
24 | } finally {
25 | setIsDeleting(false);
26 | }
27 | }
28 |
29 |
30 | return (
31 | <>
32 |
33 |
34 |
35 |
36 |
41 |
42 |
43 | >
44 | )
45 | }
46 |
47 | export default ProjectActions
--------------------------------------------------------------------------------
/components/ProjectCard.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Image from "next/image";
4 | import Link from "next/link";
5 | import { useEffect, useState } from "react";
6 |
7 | type Props = {
8 | id: string;
9 | image: string;
10 | title: string;
11 | name: string;
12 | avatarUrl: string;
13 | userId: string;
14 | }
15 |
16 | const ProjectCard = ({ id, image, title, name, avatarUrl, userId }: Props) => {
17 | const [randomLikes, setRandomLikes] = useState(0);
18 | const [randomViews, setRandomViews] = useState('');
19 |
20 | useEffect(() => {
21 | setRandomLikes(Math.floor(Math.random() * 10000))
22 | setRandomViews(String((Math.floor(Math.random() * 10000) / 1000).toFixed(1) + 'k'))
23 | }, []);
24 |
25 |
26 | return (
27 |
28 |
29 |
36 |
37 |
40 |
41 |
42 |
43 |
44 |
54 |
55 |
56 |
57 |
58 |
59 |
{randomLikes}
60 |
61 |
62 |
63 |
{randomViews}
64 |
65 |
66 |
67 |
68 | )
69 | }
70 |
71 | export default ProjectCard
--------------------------------------------------------------------------------
/components/ProjectForm.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { ProjectInterface, SessionInterface } from "@/common.types"
4 | import Image from "next/image";
5 | import { ChangeEvent, useState } from "react";
6 | import FormField from "./FormField";
7 | import { categoryFilters } from "@/constants";
8 | import CustomMenu from "./CustomMenu";
9 | import Button from "./Button";
10 | import { createNewProject, fetchToken, updateProject } from "@/lib/actions";
11 | import { useRouter } from "next/navigation";
12 |
13 | type Props = {
14 | type: string,
15 | session: SessionInterface,
16 | project?: ProjectInterface
17 | }
18 |
19 | const ProjectForm = ({ type, session, project }: Props) => {
20 | const router = useRouter();
21 |
22 | const handleFormSubmit = async (e: React.FormEvent) => {
23 | e.preventDefault();
24 |
25 | setIsSubmitting(true);
26 |
27 | const { token } = await fetchToken();
28 |
29 | try {
30 | if(type === 'create') {
31 | await createNewProject(form, session?.user?.id, token);
32 |
33 | router.push('/');
34 | }
35 |
36 | if(type === 'edit') {
37 | await updateProject(form, project?.id as string, token)
38 |
39 | router.push('/')
40 | }
41 | } catch (error) {
42 | console.log(error)
43 | } finally {
44 | setIsSubmitting(false);
45 | }
46 | };
47 |
48 | const handleChangeImage = (e: ChangeEvent) => {
49 | e.preventDefault();
50 |
51 | const file = e.target.files?.[0];
52 |
53 | if(!file) return;
54 |
55 | if(!file.type.includes('image')) {
56 | return alert('Please upload an image file');
57 | }
58 |
59 | const reader = new FileReader();
60 |
61 | reader.readAsDataURL(file);
62 |
63 | reader.onload = () => {
64 | const result = reader.result as string;
65 |
66 | handleStateChange('image', result);
67 | }
68 | };
69 |
70 | const handleStateChange = (fieldName: string, value: string) => {
71 | setform((prevState) =>
72 | ({ ...prevState, [fieldName]: value}))
73 | }
74 |
75 | const [isSubmitting, setIsSubmitting] = useState(false);
76 | const [form, setform] = useState({
77 | title: project?.title || '',
78 | description: project?.description || '',
79 | image: project?.image || '',
80 | liveSiteUrl: project?.liveSiteUrl || '',
81 | githubUrl: project?.githubUrl || '',
82 | category: project?.category || '',
83 | })
84 |
85 | return (
86 |
159 | )
160 | }
161 |
162 | export default ProjectForm
--------------------------------------------------------------------------------
/components/RelatedProjects.tsx:
--------------------------------------------------------------------------------
1 | import { UserProfile, ProjectInterface } from "@/common.types";
2 | import { getUserProjects } from "@/lib/actions";
3 | import Image from "next/image";
4 | import Link from "next/link";
5 |
6 | type Props = {
7 | userId: string;
8 | projectId: string;
9 | }
10 |
11 | const RelatedProjects = async ({ userId, projectId }: Props) => {
12 | const result = await getUserProjects(userId) as { user?: UserProfile }
13 |
14 | const filteredProjects = result?.user?.projects?.edges?.filter(({ node }: { node: ProjectInterface }) => node?.id !== projectId)
15 |
16 | console.log(filteredProjects)
17 |
18 | if(filteredProjects?.length === 0) return null;
19 |
20 | return (
21 |
22 |
23 |
More by {result?.user?.name}
24 |
28 | View All
29 |
30 |
31 |
32 |
33 | {filteredProjects?.map(({ node }: { node: ProjectInterface }) => (
34 |
35 |
36 |
37 |
38 |
41 |
42 |
43 | ))}
44 |
45 |
46 | )
47 | }
48 |
49 | export default RelatedProjects
--------------------------------------------------------------------------------
/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const NavLinks = [
2 | { href: '/', key: 'Inspiration', text: 'Inspiration' },
3 | { href: '/', key: 'Find Projects', text: 'Find Projects' },
4 | { href: '/', key: 'Learn Development', text: 'Learn Development' },
5 | { href: '/', key: 'Career Advancement', text: 'Career Advancement' },
6 | { href: '/', key: 'Hire Developers', text: 'Hire Developers' }
7 | ];
8 |
9 | export const categoryFilters = [
10 | "Frontend",
11 | "Backend",
12 | "Full-Stack",
13 | "Mobile",
14 | "UI/UX",
15 | "Game Dev",
16 | "DevOps",
17 | "Data Science",
18 | "Machine Learning",
19 | "Cybersecurity",
20 | "Blockchain",
21 | "E-commerce",
22 | "Chatbots"
23 | ]
24 |
25 | export const footerLinks = [
26 | {
27 | title: 'For developers',
28 | links: [
29 | 'Go Pro!',
30 | 'Explore development work',
31 | 'Development blog',
32 | 'Code podcast',
33 | 'Open-source projects',
34 | 'Refer a Friend',
35 | 'Code of conduct',
36 | ],
37 | },
38 | {
39 | title: 'Hire developers',
40 | links: [
41 | 'Post a job opening',
42 | 'Post a freelance project',
43 | 'Search for developers',
44 | ],
45 | },
46 | {
47 | title: 'Brands',
48 | links: [
49 | 'Advertise with us',
50 | ],
51 | },
52 | {
53 | title: 'Company',
54 | links: [
55 | 'About',
56 | 'Careers',
57 | 'Support',
58 | 'Media kit',
59 | 'Testimonials',
60 | 'API',
61 | 'Terms of service',
62 | 'Privacy policy',
63 | 'Cookie policy',
64 | ],
65 | },
66 | {
67 | title: 'Directories',
68 | links: [
69 | 'Development jobs',
70 | 'Developers for hire',
71 | 'Freelance developers for hire',
72 | 'Tags',
73 | 'Places',
74 | ],
75 | },
76 | {
77 | title: 'Development assets',
78 | links: [
79 | 'Code Marketplace',
80 | 'GitHub Marketplace',
81 | 'NPM Registry',
82 | 'Packagephobia',
83 | ],
84 | },
85 | {
86 | title: 'Development Resources',
87 | links: [
88 | 'Freelancing',
89 | 'Development Hiring',
90 | 'Development Portfolio',
91 | 'Development Education',
92 | 'Creative Process',
93 | 'Development Industry Trends',
94 | ],
95 | },
96 | ];
97 |
98 |
--------------------------------------------------------------------------------
/grafbase/.env:
--------------------------------------------------------------------------------
1 | NEXTAUTH_SECRET=Yp1jd6zPakYbLYxvl3PRo2/vdZwZQnxS+G+0YaHMy2o=
--------------------------------------------------------------------------------
/grafbase/grafbase.config.ts:
--------------------------------------------------------------------------------
1 | import { g, auth, config } from '@grafbase/sdk'
2 |
3 | // @ts-ignore
4 | const User = g.model('User', {
5 | name: g.string().length({ min: 2, max: 20 }),
6 | email: g.string().unique(),
7 | avatarUrl: g.url(),
8 | description: g.string().optional(),
9 | githubUrl: g.url().optional(),
10 | linkedinUrl: g.url().optional(),
11 | // @ts-ignore
12 | projects: g.relation(() => Project).list().optional(),
13 | }).auth((rules) => {
14 | rules.public().read()
15 | })
16 |
17 | // @ts-ignore
18 | const Project = g.model('Project', {
19 | title: g.string().length({ min: 3 }),
20 | description: g.string(),
21 | image: g.url(),
22 | liveSiteUrl: g.url(),
23 | githubUrl: g.url(),
24 | category: g.string().search(),
25 | createdBy: g.relation(() => User),
26 | }).auth((rules) => {
27 | rules.public().read(),
28 | rules.private().create().delete().update();
29 | })
30 |
31 | const jwt = auth.JWT({
32 | issuer: 'grafbase',
33 | secret: g.env('NEXTAUTH_SECRET'),
34 | })
35 |
36 | export default config({
37 | schema: g,
38 | auth: {
39 | providers: [jwt],
40 | rules: (rules) => rules.private(),
41 | }
42 | })
43 |
--------------------------------------------------------------------------------
/graphql/index.ts:
--------------------------------------------------------------------------------
1 | export const createProjectMutation = `
2 | mutation CreateProject($input: ProjectCreateInput!) {
3 | projectCreate(input: $input) {
4 | project {
5 | id
6 | title
7 | description
8 | createdBy {
9 | email
10 | name
11 | }
12 | }
13 | }
14 | }
15 | `;
16 |
17 | export const updateProjectMutation = `
18 | mutation UpdateProject($id: ID!, $input: ProjectUpdateInput!) {
19 | projectUpdate(by: { id: $id }, input: $input) {
20 | project {
21 | id
22 | title
23 | description
24 | createdBy {
25 | email
26 | name
27 | }
28 | }
29 | }
30 | }
31 | `;
32 |
33 | export const deleteProjectMutation = `
34 | mutation DeleteProject($id: ID!) {
35 | projectDelete(by: { id: $id }) {
36 | deletedId
37 | }
38 | }
39 | `;
40 |
41 | export const createUserMutation = `
42 | mutation CreateUser($input: UserCreateInput!) {
43 | userCreate(input: $input) {
44 | user {
45 | name
46 | email
47 | avatarUrl
48 | description
49 | githubUrl
50 | linkedinUrl
51 | id
52 | }
53 | }
54 | }
55 | `;
56 |
57 | export const projectsQuery = `
58 | query getProjects($category: String, $endCursor: String) {
59 | projectSearch(first: 8, after: $endCursor, filter: {category: {eq: $category}}) {
60 | pageInfo {
61 | hasNextPage
62 | hasPreviousPage
63 | startCursor
64 | endCursor
65 | }
66 | edges {
67 | node {
68 | title
69 | githubUrl
70 | description
71 | liveSiteUrl
72 | id
73 | image
74 | category
75 | createdBy {
76 | id
77 | email
78 | name
79 | avatarUrl
80 | }
81 | }
82 | }
83 | }
84 | }
85 | `;
86 |
87 | export const getProjectByIdQuery = `
88 | query GetProjectById($id: ID!) {
89 | project(by: { id: $id }) {
90 | id
91 | title
92 | description
93 | image
94 | liveSiteUrl
95 | githubUrl
96 | category
97 | createdBy {
98 | id
99 | name
100 | email
101 | avatarUrl
102 | }
103 | }
104 | }
105 | `;
106 |
107 | export const getUserQuery = `
108 | query GetUser($email: String!) {
109 | user(by: { email: $email }) {
110 | id
111 | name
112 | email
113 | avatarUrl
114 | description
115 | githubUrl
116 | linkedinUrl
117 | }
118 | }
119 | `;
120 |
121 | export const getProjectsOfUserQuery = `
122 | query getUserProjects($id: ID!, $last: Int = 4) {
123 | user(by: { id: $id }) {
124 | id
125 | name
126 | email
127 | description
128 | avatarUrl
129 | githubUrl
130 | linkedinUrl
131 | projects(last: $last) {
132 | edges {
133 | node {
134 | id
135 | title
136 | image
137 | }
138 | }
139 | }
140 | }
141 | }
142 | `;
--------------------------------------------------------------------------------
/lib/actions.ts:
--------------------------------------------------------------------------------
1 | import { ProjectForm } from '@/common.types';
2 | import { createProjectMutation, createUserMutation, deleteProjectMutation, getProjectByIdQuery, getProjectsOfUserQuery, getUserQuery, projectsQuery, updateProjectMutation } from '@/graphql';
3 | import { GraphQLClient } from 'graphql-request';
4 |
5 | const isProduction = process.env.NODE_ENV === 'production';
6 | const apiUrl = isProduction ? process.env.NEXT_PUBLIC_GRAFBASE_API_URL || '' : 'http://127.0.0.1:4000/graphql';
7 | const apiKey = isProduction ? process.env.NEXT_PUBLIC_GRAFBASE_API_KEY || '' : 'letmein';
8 | const serverUrl = isProduction ? process.env.NEXT_PUBLIC_SERVER_URL : 'http://localhost:3000';
9 |
10 | const client = new GraphQLClient(apiUrl);
11 |
12 | const makeGraphQLRequest = async (query: string, variables = {}) => {
13 | try {
14 | return await client.request(query, variables)
15 | } catch (error) {
16 | throw error;
17 | }
18 | }
19 |
20 | export const getUser = (email: string) => {
21 | client.setHeader('x-api-key', apiKey);
22 | return makeGraphQLRequest(getUserQuery, { email })
23 | }
24 |
25 | export const createUser = (name: string, email: string, avatarUrl: string) => {
26 | client.setHeader('x-api-key', apiKey);
27 | const variables = {
28 | input: {
29 | name, email, avatarUrl
30 | }
31 | }
32 |
33 | return makeGraphQLRequest(createUserMutation, variables)
34 | }
35 |
36 | export const fetchToken = async () => {
37 | try {
38 | const response = await fetch(`${serverUrl}/api/auth/token`);
39 | return response.json();
40 | } catch (error) {
41 | throw error;
42 | }
43 | }
44 |
45 | export const uploadImage = async (imagePath: string) => {
46 | try {
47 | const response = await fetch(`${serverUrl}/api/upload`, {
48 | method: 'POST',
49 | body: JSON.stringify({ path: imagePath })
50 | })
51 |
52 | return response.json();
53 | } catch (error) {
54 | throw error;
55 | }
56 | }
57 |
58 | export const createNewProject = async (form: ProjectForm, creatorId: string, token: string) => {
59 | const imageUrl = await uploadImage(form.image);
60 |
61 | if(imageUrl.url) {
62 | client.setHeader("Authorization", `Bearer ${token}`)
63 |
64 | const variables = {
65 | input: {
66 | ...form,
67 | image: imageUrl.url,
68 | createdBy: {
69 | link: creatorId
70 | }
71 | }
72 | }
73 |
74 | return makeGraphQLRequest(createProjectMutation, variables)
75 | }
76 | }
77 |
78 | export const fetchAllProjects = async (category?: string, endCursor?: string) => {
79 | client.setHeader('x-api-key', apiKey);
80 |
81 | return makeGraphQLRequest(projectsQuery, { category, endCursor })
82 | }
83 |
84 | export const getProjectDetails = (id: string) => {
85 | client.setHeader('x-api-key', apiKey);
86 | return makeGraphQLRequest(getProjectByIdQuery, { id })
87 | }
88 |
89 | export const getUserProjects = (id: string, last?: number) => {
90 | client.setHeader('x-api-key', apiKey);
91 | return makeGraphQLRequest(getProjectsOfUserQuery, { id, last })
92 | }
93 |
94 |
95 | export const deleteProject = (id: string, token: string) => {
96 | client.setHeader("Authorization", `Bearer ${token}`);
97 |
98 | return makeGraphQLRequest(deleteProjectMutation, { id })
99 | }
100 |
101 | export const updateProject = async (form: ProjectForm, projectId: string, token: string) => {
102 | function isBase64DataURL(value: string) {
103 | const base64Regex = /^data:image\/[a-z]+;base64,/;
104 | return base64Regex.test(value);
105 | }
106 |
107 | let updatedForm = { ...form};
108 |
109 | const isUploadingNewImage = isBase64DataURL(form.image);
110 |
111 | if(isUploadingNewImage) {
112 | const imageUrl = await uploadImage(form.image);
113 |
114 | if(imageUrl.url) {
115 | updatedForm = {
116 | ...form,
117 | image: imageUrl.url
118 | }
119 | }
120 | }
121 |
122 | const variables = {
123 | id: projectId,
124 | input: updatedForm,
125 | }
126 |
127 | client.setHeader("Authorization", `Bearer ${token}`);
128 |
129 | return makeGraphQLRequest(updateProjectMutation, variables)
130 | }
--------------------------------------------------------------------------------
/lib/session.ts:
--------------------------------------------------------------------------------
1 | import { getServerSession } from 'next-auth/next';
2 | import { NextAuthOptions, User } from 'next-auth';
3 | import { AdapterUser } from 'next-auth/adapters';
4 | import GoogleProvider from 'next-auth/providers/google';
5 | import jsonwebtoken from 'jsonwebtoken';
6 | import { JWT } from 'next-auth/jwt';
7 |
8 | import { SessionInterface, UserProfile } from '@/common.types';
9 | import { createUser, getUser } from './actions';
10 |
11 | export const authOptions: NextAuthOptions = {
12 | providers: [
13 | GoogleProvider({
14 | clientId: process.env.GOOGLE_CLIENT_ID!,
15 | clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
16 | })
17 | ],
18 | jwt: {
19 | encode: ({ secret, token }) => {
20 | const encodedToken = jsonwebtoken.sign({
21 | ...token,
22 | iss: 'grafbase',
23 | exp: Math.floor(Date.now() / 1000) + 60 * 60
24 | }, secret)
25 |
26 | return encodedToken;
27 | },
28 | decode: async ({ secret, token }) => {
29 | const decodedToken = jsonwebtoken.verify(token!, secret) as JWT;
30 |
31 | return decodedToken;
32 | }
33 | },
34 | theme: {
35 | colorScheme: 'light',
36 | logo: '/logo.png'
37 | },
38 | callbacks: {
39 | async session({ session }) {
40 | const email = session?.user?.email as string;
41 |
42 | try {
43 | const data = await getUser(email) as { user?: UserProfile }
44 |
45 | const newSession = {
46 | ...session,
47 | user: {
48 | ...session.user,
49 | ...data?.user
50 | }
51 | }
52 |
53 | return newSession;
54 | } catch (error) {
55 | console.log('Error retrieving user data', error);
56 | return session;
57 | }
58 | },
59 | async signIn({ user }: { user: AdapterUser | User }) {
60 | try {
61 | const userExists = await getUser(user?.email as string) as { user?: UserProfile }
62 |
63 | if (!userExists.user) {
64 | await createUser(
65 | user.name as string,
66 | user.email as string,
67 | user.image as string
68 | );
69 | }
70 |
71 | return true
72 | } catch (error: any) {
73 | console.log(error);
74 | return false;
75 | }
76 | }
77 | }
78 | }
79 |
80 | export async function getCurrentUser() {
81 | const session = await getServerSession(authOptions) as SessionInterface;
82 |
83 | return session;
84 | }
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | domains: ['lh3.googleusercontent.com', 'res.cloudinary.com']
5 | },
6 | experimental: {
7 | serverComponentsExternalPackages: ['cloudinary', 'graphql-request']
8 | }
9 | }
10 |
11 | module.exports = nextConfig
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs13_flexibble",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@headlessui/react": "^1.7.15",
13 | "@types/jsonwebtoken": "^9.0.2",
14 | "@types/node": "20.3.1",
15 | "@types/react": "18.2.13",
16 | "@types/react-dom": "18.2.6",
17 | "autoprefixer": "10.4.14",
18 | "cloudinary": "^1.37.2",
19 | "graphql-request": "^6.1.0",
20 | "jsonwebtoken": "^9.0.0",
21 | "next": "13.4.7",
22 | "next-auth": "^4.22.1",
23 | "postcss": "8.4.24",
24 | "react": "18.2.0",
25 | "react-dom": "18.2.0",
26 | "tailwindcss": "3.3.2",
27 | "typescript": "5.1.3"
28 | },
29 | "devDependencies": {
30 | "@grafbase/sdk": "^0.1.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/arrow-down.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/close.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/dot.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/email.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/eye.svg:
--------------------------------------------------------------------------------
1 |
5 |
6 |
--------------------------------------------------------------------------------
/public/hearth-purple.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/public/hearth-white.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/hearth.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/logo-purple.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/magnifying-glass.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/minus.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/next copy.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/pencile.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/plus-round.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/plus.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/profile-post.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianhajdin/grafbase_flexibble/0c740b9bb7973ebf0fee78949c11dd6feb22b11e/public/profile-post.png
--------------------------------------------------------------------------------
/public/save.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/share.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/socials.svg:
--------------------------------------------------------------------------------
1 |
12 |
13 |
--------------------------------------------------------------------------------
/public/trash.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/upload.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/public/vercel copy.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
5 | './components/**/*.{js,ts,jsx,tsx,mdx}',
6 | './app/**/*.{js,ts,jsx,tsx,mdx}',
7 | ],
8 | theme: {
9 | extend: {
10 | colors: {
11 | 'nav-border': '#EBEAEA',
12 | 'light-white': '#FAFAFB',
13 | 'light-white-100': '#F1F4F5',
14 | 'light-white-200': '#d7d7d7',
15 | 'light-white-300': '#F3F3F4',
16 | 'light-white-400': '#E2E5F1',
17 | 'light-white-500': '#E4E4E4',
18 | gray: '#4D4A4A',
19 | 'gray-100': '#3d3d4e',
20 | 'black-100': '#252525',
21 | 'primary-purple': '#9747FF',
22 | 'gray-50': '#D9D9D9',
23 | },
24 | boxShadow: {
25 | menu: '0px 159px 95px rgba(13,12,34,0.01), 0px 71px 71px rgba(13,12,34,0.02), 0px 18px 39px rgba(13,12,34,0.02), 0px 0px 0px rgba(13,12,34,0.02)',
26 | },
27 | screens: {
28 | 'xs': '400px',
29 | },
30 | maxWidth: {
31 | '10xl': '1680px'
32 | }
33 | },
34 | },
35 | plugins: [],
36 | };
37 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------