Skip to content

Instantly share code, notes, and snippets.

@yskooo
Forked from adrianhajdin/ProfileMenu.tsx
Created June 25, 2023 01:00
Show Gist options
  • Save yskooo/8bc7782396dba8f94185e8333fd9ab4b to your computer and use it in GitHub Desktop.
Save yskooo/8bc7782396dba8f94185e8333fd9ab4b to your computer and use it in GitHub Desktop.
Build and Deploy a Full Stack Next.js 13 Application | React, Next JS 13, TypeScript, Tailwind CSS
import { User, Session } from 'next-auth'
export type FormState = {
title: string;
description: string;
image: string;
liveSiteUrl: string;
githubUrl: string;
category: string;
};
export interface ProjectInterface {
title: string;
description: string;
image: string;
liveSiteUrl: string;
githubUrl: string;
category: string;
id: string;
createdBy: {
name: string;
email: string;
avatarUrl: string;
id: string;
};
}
export interface UserProfile {
id: string;
name: string;
email: string;
description: string | null;
avatarUrl: string;
githubUrl: string | null;
linkedinUrl: string | null;
projects: {
edges: { node: ProjectInterface }[];
pageInfo: {
hasPreviousPage: boolean;
hasNextPage: boolean;
startCursor: string;
endCursor: string;
};
};
}
export interface SessionInterface extends Session {
user: User & {
id: string;
name: string;
email: string;
avatarUrl: string;
};
}
export interface ProjectForm {
title: string;
description: string;
image: string;
liveSiteUrl: string;
githubUrl: string;
category: string;
}
export const NavLinks = [
{ href: '/', key: 'Inspiration', text: 'Inspiration' },
{ href: '/', key: 'Find Projects', text: 'Find Projects' },
{ href: '/', key: 'Learn Development', text: 'Learn Development' },
{ href: '/', key: 'Career Advancement', text: 'Career Advancement' },
{ href: '/', key: 'Hire Developers', text: 'Hire Developers' }
];
export const categoryFilters = [
"Frontend",
"Backend",
"Full-Stack",
"Mobile",
"UI/UX",
"Game Dev",
"DevOps",
"Data Science",
"Machine Learning",
"Cybersecurity",
"Blockchain",
"E-commerce",
"Chatbots"
]
export const footerLinks = [
{
title: 'For developers',
links: [
'Go Pro!',
'Explore development work',
'Development blog',
'Code podcast',
'Open-source projects',
'Refer a Friend',
'Code of conduct',
],
},
{
title: 'Hire developers',
links: [
'Post a job opening',
'Post a freelance project',
'Search for developers',
],
},
{
title: 'Brands',
links: [
'Advertise with us',
],
},
{
title: 'Company',
links: [
'About',
'Careers',
'Support',
'Media kit',
'Testimonials',
'API',
'Terms of service',
'Privacy policy',
'Cookie policy',
],
},
{
title: 'Directories',
links: [
'Development jobs',
'Developers for hire',
'Freelance developers for hire',
'Tags',
'Places',
],
},
{
title: 'Development assets',
links: [
'Code Marketplace',
'GitHub Marketplace',
'NPM Registry',
'Packagephobia',
],
},
{
title: 'Development Resources',
links: [
'Freelancing',
'Development Hiring',
'Development Portfolio',
'Development Education',
'Creative Process',
'Development Industry Trends',
],
},
];
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Inter;
}
.flexCenter {
@apply flex justify-center items-center;
}
.flexBetween {
@apply flex justify-between items-center;
}
.flexStart {
@apply flex items-center justify-start;
}
.text-small {
@apply text-sm font-medium;
}
.paddings {
@apply lg:px-20 py-6 px-5;
}
::-webkit-scrollbar {
width: 5px;
height: 4px;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 12px;
}
.modal-head-text {
@apply md:text-5xl text-3xl font-extrabold text-left max-w-5xl w-full;
}
.no-result-text {
@apply w-full text-center my-10 px-2;
}
/* Project Details */
.user-actions_section {
@apply fixed max-md:hidden flex gap-4 flex-col right-10 top-20;
}
.user-info {
@apply flex flex-wrap whitespace-nowrap text-sm font-normal gap-2 w-full;
}
/* Home */
.projects-grid {
@apply grid xl:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-10 mt-10 w-full;
}
/* Project Actions */
.edit-action_btn {
@apply p-3 text-gray-100 bg-light-white-400 rounded-lg text-sm font-medium;
}
.delete-action_btn {
@apply p-3 text-gray-100 hover:bg-red-600 rounded-lg text-sm font-medium;
}
/* Related Project Card */
.related_project-card {
@apply flex-col rounded-2xl min-w-[210px] min-h-[197px];
}
.related_project-card_title {
@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;
}
.related_projects-grid {
@apply grid xl:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-8 mt-5;
}
/* Custom Menu */
.custom_menu-btn {
@apply gap-4 w-full rounded-md bg-light-white-100 p-4 text-base outline-none capitalize;
}
.custom_menu-items {
@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;
}
.custom_menu-item {
@apply text-left w-full px-5 py-2 text-sm hover:bg-light-white-100 self-start whitespace-nowrap capitalize;
}
/* Footer */
.footer {
@apply flex-col paddings w-full gap-20 bg-light-white;
}
.footer_copyright {
@apply max-sm:flex-col w-full text-sm font-normal;
}
.footer_column {
@apply flex-1 flex flex-col gap-3 text-sm min-w-max;
}
/* Form Field */
.form_field-input {
@apply w-full outline-0 bg-light-white-100 rounded-xl p-4;
}
/* Modal */
.modal {
@apply fixed z-10 left-0 right-0 top-0 bottom-0 mx-auto bg-black/80;
}
.modal_wrapper {
@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;
}
/* Navbar */
.navbar {
@apply py-5 px-8 border-b border-nav-border gap-4;
}
/* Profile Menu */
.profile_menu-items {
@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;
}
/* Profile Card */
.profile_card-title {
@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;
}
/* Project Form */
.form {
@apply flex-col w-full lg:pt-24 pt-12 gap-10 text-lg max-w-5xl mx-auto;
}
.form_image-container {
@apply w-full lg:min-h-[400px] min-h-[200px] relative;
}
.form_image-label {
@apply z-10 text-center w-full h-full p-20 text-gray-100 border-2 border-gray-50 border-dashed;
}
.form_image-input {
@apply absolute z-30 w-full opacity-0 h-full cursor-pointer;
}
/* Profile Projects */
.profile_projects {
@apply grid xl:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-8 mt-5;
}
export const createProjectMutation = `
mutation CreateProject($input: ProjectCreateInput!) {
projectCreate(input: $input) {
project {
id
title
description
createdBy {
email
name
}
}
}
}
`;
export const updateProjectMutation = `
mutation UpdateProject($id: ID!, $input: ProjectUpdateInput!) {
projectUpdate(by: { id: $id }, input: $input) {
project {
id
title
description
createdBy {
email
name
}
}
}
}
`;
export const deleteProjectMutation = `
mutation DeleteProject($id: ID!) {
projectDelete(by: { id: $id }) {
deletedId
}
}
`;
export const createUserMutation = `
mutation CreateUser($input: UserCreateInput!) {
userCreate(input: $input) {
user {
name
email
avatarUrl
description
githubUrl
linkedinUrl
id
}
}
}
`;
export const projectsQuery = `
query getProjects($category: String, $endCursor: String) {
projectSearch(first: 8, after: $endCursor, filter: {category: {eq: $category}}) {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
node {
title
githubUrl
description
liveSiteUrl
id
image
category
createdBy {
id
email
name
avatarUrl
}
}
}
}
}
`;
export const getProjectByIdQuery = `
query GetProjectById($id: ID!) {
project(by: { id: $id }) {
id
title
description
image
liveSiteUrl
githubUrl
category
createdBy {
id
name
email
avatarUrl
}
}
}
`;
export const getUserQuery = `
query GetUser($email: String!) {
user(by: { email: $email }) {
id
name
email
avatarUrl
description
githubUrl
linkedinUrl
}
}
`;
export const getProjectsOfUserQuery = `
query getUserProjects($id: ID!, $last: Int = 4) {
user(by: { id: $id }) {
id
name
email
description
avatarUrl
githubUrl
linkedinUrl
projects(last: $last) {
edges {
node {
id
title
image
}
}
}
}
}
`;
"use client"
import Link from "next/link";
import Image from "next/image";
import { signOut } from "next-auth/react";
import { Fragment, useState } from "react";
import { Menu, Transition } from "@headlessui/react";
import { SessionInterface } from "@/common.types";
const ProfileMenu = ({ session }: { session: SessionInterface }) => {
const [openModal, setOpenModal] = useState(false);
return (
<div className="flexCenter z-10 flex-col relative">
<Menu as="div">
<Menu.Button className="flexCenter" onMouseEnter={() => setOpenModal(true)} >
{session?.user?.image && (
<Image
src={session.user.image}
width={40}
height={40}
className="rounded-full"
alt="user profile image"
/>
)}
</Menu.Button>
<Transition
show={openModal}
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
static
className="flexStart profile_menu-items"
onMouseLeave={() => setOpenModal(false)}
>
<div className="flex flex-col items-center gap-y-4">
{session?.user?.image && (
<Image
src={session?.user?.image}
className="rounded-full"
width={80}
height={80}
alt="profile Image"
/>
)}
<p className="font-semibold">{session?.user?.name}</p>
</div>
<div className="flex flex-col gap-3 pt-10 items-start w-full">
<Menu.Item>
<Link href={`/profile/${session?.user?.id}`} className="text-sm">Work Preferences</Link>
</Menu.Item>
<Menu.Item>
<Link href={`/profile/${session?.user?.id}`} className="text-sm">Settings</Link>
</Menu.Item>
<Menu.Item>
<Link href={`/profile/${session?.user?.id}`} className="text-sm">Profile</Link>
</Menu.Item>
</div>
<div className="w-full flexStart border-t border-nav-border mt-5 pt-5">
<Menu.Item>
<button type="button" className="text-sm" onClick={() => signOut()}>
Sign out
</button>
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
)
}
export default ProfileMenu
import { ProjectInterface, UserProfile } from '@/common.types'
import Image from 'next/image'
import Link from 'next/link'
import Button from "./Button";
import ProjectCard from './ProjectCard';
type Props = {
user: UserProfile;
}
const ProfilePage = ({ user }: Props) => (
<section className='flexCenter flex-col max-w-10xl w-full mx-auto paddings'>
<section className="flexBetween max-lg:flex-col gap-10 w-full">
<div className='flex items-start flex-col w-full'>
<Image src={user?.avatarUrl} width={100} height={100} className="rounded-full" alt="user image" />
<p className="text-4xl font-bold mt-10">{user?.name}</p>
<p className="md:text-5xl text-3xl font-extrabold md:mt-10 mt-5 max-w-lg">I’m Software Engineer at JSM 👋</p>
<div className="flex mt-8 gap-5 w-full flex-wrap">
<Button
title="Follow"
leftIcon="/plus-round.svg"
bgColor="bg-light-white-400 !w-max"
textColor="text-black-100"
/>
<Link href={`mailto:${user?.email}`}>
<Button title="Hire Me" leftIcon="/email.svg" />
</Link>
</div>
</div>
{user?.projects?.edges?.length > 0 ? (
<Image
src={user?.projects?.edges[0]?.node?.image}
alt="project image"
width={739}
height={554}
className='rounded-xl object-contain'
/>
) : (
<Image
src="/profile-post.png"
width={739}
height={554}
alt="project image"
className='rounded-xl'
/>
)}
</section>
<section className="flexStart flex-col lg:mt-28 mt-16 w-full">
<p className="w-full text-left text-lg font-semibold">Recent Work</p>
<div className="profile_projects">
{user?.projects?.edges?.map(
({ node }: { node: ProjectInterface }) => (
<ProjectCard
key={`${node?.id}`}
id={node?.id}
image={node?.image}
title={node?.title}
name={user.name}
avatarUrl={user.avatarUrl}
userId={user.id}
/>
)
)}
</div>
</section>
</section>
)
export default ProfilePage
import Image from "next/image"
import Link from "next/link"
import { getCurrentUser } from "@/lib/session"
import { getProjectDetails } from "@/lib/actions"
import Modal from "@/components/Modal"
// import ProjectActions from "@/components/ProjectActions"
import RelatedProjects from "@/components/RelatedProjects"
import { ProjectInterface } from "@/common.types"
import ProjectActions from "@/components/ProjectActions"
const Project = async ({ params: { id } }: { params: { id: string } }) => {
const session = await getCurrentUser()
const result = await getProjectDetails(id) as { project?: ProjectInterface}
if (!result?.project) return (
<p className="no-result-text">Failed to fetch project info</p>
)
const projectDetails = result?.project
const renderLink = () => `/profile/${projectDetails?.createdBy?.id}`
return (
<Modal>
<section className="flexBetween gap-y-8 max-w-4xl max-xs:flex-col w-full">
<div className="flex-1 flex items-start gap-5 w-full max-xs:flex-col">
<Link href={renderLink()}>
<Image
src={projectDetails?.createdBy?.avatarUrl}
width={50}
height={50}
alt="profile"
className="rounded-full"
/>
</Link>
<div className="flex-1 flexStart flex-col gap-1">
<p className="self-start text-lg font-semibold">
{projectDetails?.title}
</p>
<div className="user-info">
<Link href={renderLink()}>
{projectDetails?.createdBy?.name}
</Link>
<Image src="/dot.svg" width={4} height={4} alt="dot" />
<Link href={`/?category=${projectDetails.category}`} className="text-primary-purple font-semibold">
{projectDetails?.category}
</Link>
</div>
</div>
</div>
{session?.user?.email === projectDetails?.createdBy?.email && (
<div className="flex justify-end items-center gap-2">
<ProjectActions projectId={projectDetails?.id} />
</div>
)}
</section>
<section className="mt-14">
<Image
src={`${projectDetails?.image}`}
className="object-cover rounded-2xl"
width={1064}
height={798}
alt="poster"
/>
</section>
<section className="flexCenter flex-col mt-20">
<p className="max-w-5xl text-xl font-normal">
{projectDetails?.description}
</p>
<div className="flex flex-wrap mt-5 gap-5">
<Link href={projectDetails?.githubUrl} target="_blank" rel="noreferrer" className="flexCenter gap-2 tex-sm font-medium text-primary-purple">
🖥 <span className="underline">Github</span>
</Link>
<Image src="/dot.svg" width={4} height={4} alt="dot" />
<Link href={projectDetails?.liveSiteUrl} target="_blank" rel="noreferrer" className="flexCenter gap-2 tex-sm font-medium text-primary-purple">
🚀 <span className="underline">Live Site</span>
</Link>
</div>
</section>
<section className="flexCenter w-full gap-8 mt-28">
<span className="w-full h-0.5 bg-light-white-200" />
<Link href={renderLink()} className="min-w-[82px] h-[82px]">
<Image
src={projectDetails?.createdBy?.avatarUrl}
className="rounded-full"
width={82}
height={82}
alt="profile image"
/>
</Link>
<span className="w-full h-0.5 bg-light-white-200" />
</section>
<RelatedProjects userId={projectDetails?.createdBy?.id} projectId={projectDetails?.id} />
</Modal>
)
}
export default Project
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
'nav-border': '#EBEAEA',
'light-white': '#FAFAFB',
'light-white-100': '#F1F4F5',
'light-white-200': '#d7d7d7',
'light-white-300': '#F3F3F4',
'light-white-400': '#E2E5F1',
'light-white-500': '#E4E4E4',
gray: '#4D4A4A',
'gray-100': '#3d3d4e',
'black-100': '#252525',
'primary-purple': '#9747FF',
'gray-50': '#D9D9D9',
},
boxShadow: {
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)',
},
screens: {
'xs': '400px',
},
maxWidth: {
'10xl': '1680px'
}
},
},
plugins: [],
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment