Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-button-bg: var(--button-background);
--color-button-hover: var(--button-hover);
}

:root {
Expand Down Expand Up @@ -91,6 +93,8 @@

.dark {
--background: oklch(0.145 0 0);
--button-background: #b1440d;
--button-hover: #933607;
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
Expand Down
18 changes: 11 additions & 7 deletions client/app/onboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ function OnBoarding() {
goToNextStep={goToNextStep}
updateFormData={updateFormData}
/>,
<GetPictures key={"picture"}/>
<GetPictures
key={"picture"}
goToNextStep={goToNextStep}
updateFormData={updateFormData}
/>,
];

useEffect(() => {
Expand All @@ -67,23 +71,23 @@ function OnBoarding() {
{/** Modal */}
{showModal && (
<>
<div className="h-screen w-full backdrop-blur-[2px] bg-white/10 overflow-hidden absolute inset-0 z-10 flex justify-center items-center">
<div className="h-screen w-full backdrop-blur-[2px] bg-white/10 overflow-hidden fixed inset-0 z-10 flex justify-center items-center">
<div
className="relative w-md space-y-6 bg-white p-10 rounded"
className="relative w-md space-y-6 p-10 rounded bg-black"
ref={modal}>
<div className="space-y-6 text-center text-black">
<div className="space-y-6 text-center">
<h1 className="text-2xl">
Let&apos;s get you set up
</h1>
<h1 className="text-md text-gray-700">
<h1 className="text-md text-gray-400">
<span className="text-xl">“</span> This will
only take about 2 minutes. It helps us set
things up just right for you.
<span className="text-xl">”</span>
</h1>
</div>
<button
className="bg-blue-500 border border-black cursor-pointer rounded-sm w-full py-1 text-xl"
className="bg-blue-500 border border-yellow-500 cursor-pointer rounded-sm w-full py-1 text-xl"
onClick={() => {
closeModal();
}}>
Expand All @@ -95,7 +99,7 @@ function OnBoarding() {
)}
<FormProvider {...methods}>
<div className="h-full flex justify-center flex-col items-center w-full">
{currentForm[currentStep]}
{currentForm[2]}
</div>
</FormProvider>
</>
Expand Down
220 changes: 208 additions & 12 deletions client/app/onboard/steps.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OnboardingFormData } from "@/validations/onboard.validation";
import { useMemo } from "react";
import Image from "next/image";
import { useEffect, useMemo, useRef, useState } from "react";
import { useFormContext } from "react-hook-form";

type props = {
Expand Down Expand Up @@ -145,15 +146,210 @@ export const GetLocation = ({ goToNextStep, updateFormData }: props) => {
);
};

type ImageState = {
preview: string;
file?: File;
};

export const GetPictures = ({ goToNextStep, updateFormData }: props) => {
const defaultProfileUrl =
"https://media.licdn.com/dms/image/v2/D4E12AQEud3Ll5MI7cQ/article-inline_image-shrink_1000_1488/article-inline_image-shrink_1000_1488/0/1660833954461?e=2147483647&v=beta&t=1e_UbOwhBjrbh6dYElFz-hUdQy2gltC1XWh2NxkigvI";
const defaultCoverUrl =
"https://i0.wp.com/linkedinheaders.com/wp-content/uploads/2018/02/mountain-clouds-header.jpg?w=1584&ssl=1";

const [showProfile, setShowProfile] = useState(false);
const [showCover, setShowCover] = useState(false);
const [profilePicture, setProfilePicture] = useState<ImageState>({
preview: defaultProfileUrl,
file: undefined,
});
const [coverPicture, setCoverPicture] = useState<ImageState>({
preview: defaultCoverUrl,
file: undefined,
});

// useEffect(() => {
// const handleClick = (e: MouseEvent) => {
// if (
// profileModal.current &&
// !profileModal.current.contains(e.target as Node)
// ) {
// setShowProfile(false);
// }
// };

export const GetPictures = () => {
return(
<>
<div className="h-full flex flex-col gap-6 items-center">
<div>
<h1>Set profile picture and cover picture</h1>
</div>
</div>
</>
)
}
// document.addEventListener("mousedown", handleClick);

// return () => {
// document.removeEventListener("mousedown", handleClick);
// };
// }, [setShowCover, setShowProfile]);

const {
setValue,
register,
formState: { errors },
} = useFormContext<OnboardingFormData>();

const onNext = () => {
setValue("profile_picture", profilePicture.file);
setValue("cover_picture", coverPicture.file);

updateFormData({
profile_picture: profilePicture.file,
cover_picture: coverPicture.file,
});
goToNextStep();
};
return (
<>
{showProfile && (
<>
<div
className="h-screen w-full backdrop-blur-[2px] bg-white/10 overflow-hidden fixed inset-0 z-10 flex justify-center items-center"
onClick={() => setShowProfile(false)}>
<div
className="relative w-md space-y-6 bg-black p-10 rounded"
onClick={(e) => e.stopPropagation()}>
<div
style={{
backgroundImage:
"radial-gradient(#666 1px, transparent 1px)",
backgroundSize: "16px 16px",
}}
className="w-full bg-white/10 p-2 flex justify-center">
<Image
src={profilePicture.preview}
height={200}
width={100}
alt="profile picture"
className="h-40 w-40 rounded"
/>
</div>
<div>
<label htmlFor="picture" className="text-xl ">
Upload Profile Picture
</label>
<input
className="border w-full mt-4 rounded px-2"
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (!file) return;
const preview =
URL.createObjectURL(file);
setProfilePicture({
preview,
file,
});
}}
id="picture"
/>
{errors && errors["profile_picture"] && (
<p>{errors["profile_picture"].message}</p>
)}
</div>
<button
className="w-full cursor-pointer bg-button-bg hover:bg-button-hover py-1 rounded-full text-xl"
onClick={() => {
setShowProfile(false);
}}>
Done
</button>
</div>
</div>
</>
)}
{showCover && (
<>
<div
className="h-screen w-full backdrop-blur-[2px] bg-white/10 overflow-hidden fixed inset-0 z-10 flex justify-center items-center"
onClick={() => setShowCover(false)}>
<div
className="relative w-md space-y-6 bg-black p-10 rounded"
onClick={(e) => e.stopPropagation()}>
<div
style={{
backgroundImage:
"radial-gradient(#666 1px, transparent 1px)",
backgroundSize: "16px 16px",
}}
className="w-full bg-white/10 p-2 flex justify-center">
<Image
src={coverPicture.preview}
height={200}
width={100}
alt="profile picture"
className="w-full object-contain rounded"
/>
</div>
<div>
<label htmlFor="picture" className="text-xl ">
Upload Cover Picture
</label>
<input
className="border w-full mt-4 rounded px-2"
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (!file) return;
const preview =
URL.createObjectURL(file);
setCoverPicture({
preview,
file,
});
}}
id="picture"
/>
{errors && errors["cover_picture"] && (
<p>{errors["cover_picture"].message}</p>
)}
</div>
<button
className="w-full cursor-pointer bg-button-bg hover:bg-button-hover py-1 rounded-full text-xl"
onClick={() => {
setShowCover(false);
}}>
Done
</button>
</div>
</div>
</>
)}
<div className="h-full flex flex-col gap-6 items-center mb-10">
<div className="text-center">
<h1>Set profile picture and cover picture</h1>
<p>Good to connect with developers</p>
</div>
<div className="relative bg-white/15 h-100 flex flex-col p-10 rounded justify-between">
<div className="relative">
<Image
src={coverPicture.preview}
onClick={() => setShowCover(true)}
width={1000}
height={400}
alt={"pr0file picture"}
className="rounded w-full cursor-pointer"
/>
<Image
className="rounded-full absolute h-8 w-8 sm:w-20 sm:h-20 md:w-32 md:h-32 lg:w-38 lg:h-38 top-1/2 left-10 cursor-pointer"
onClick={() => setShowProfile(true)}
src={profilePicture.preview}
width={200}
height={200}
alt={"cover picture"}
/>
</div>
<button
className="mt-10 py-2 text-2xl cursor-pointer rounded-full bg-amber-700"
onClick={onNext}>
Next
</button>
</div>
</div>
</>
);
};
22 changes: 22 additions & 0 deletions client/components/layouts/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { createPortal } from 'react-dom';

type Props = {
isOpen: boolean,
children: React.ReactNode
}

function Modal({isOpen, children}: Props) {
if(!isOpen) return null;

return (
createPortal(
<div className='fixed inset-0 bg-white/10 flex justify-center items-center'>
{children}
</div>,
document.body
)
)
}

export default Modal;
18 changes: 16 additions & 2 deletions client/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
trailingSlash: true,
/* config options here */
trailingSlash: true,
images: {
remotePatterns: [
{
protocol: "https",
hostname: "media.licdn.com",
pathname: "/**",
},
{
protocol: "https",
hostname: "i0.wp.com",
pathname: "/**",
},
],
},
};

export default nextConfig;
34 changes: 34 additions & 0 deletions client/utils/imageFileSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import z from "zod";

const MAX_SIZE = 5 * 1024 * 1024; // 5MB

const ALLOWED_MIME = ["image/png", "image/jpeg", "image/jpg", "image/webp"];

const ALLOWED_EXT = [".png", ".jpg", ".jpeg", ".webp"];

export const imageFileSchema = z.custom<File>(
(file) => {
if (!file) return false;
if (typeof file !== "object") return false;
if (!("name" in file)) return false;
if (!("size" in file)) return false;
if (!("type" in file)) return false;

const f = file as File;

// Size check
if (f.size > MAX_SIZE) return false;

// MIME type check
if (!ALLOWED_MIME.includes(f.type)) return false;

// Extension check (prevents renamed .exe → .png attacks)
const ext = f.name.toLowerCase().slice(f.name.lastIndexOf("."));
if (!ALLOWED_EXT.includes(ext)) return false;

return true;
},
{
message: "File must be an image (.png, .jpg, .jpeg, .webp) under 5MB",
},
);
Loading