updated profile page
This commit is contained in:
parent
cac19385a9
commit
dd24fd8ae4
10 changed files with 2401 additions and 31 deletions
|
@ -1,6 +1,17 @@
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: '*.googleusercontent.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'jtp-donutshop.s3.amazonaws.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
2210
donutshop_ecommerce/package-lock.json
generated
2210
donutshop_ecommerce/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -10,6 +10,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/mongodb-adapter": "^3.1.0",
|
"@auth/mongodb-adapter": "^3.1.0",
|
||||||
|
"@aws-sdk/client-s3": "^3.576.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"framer-motion": "^11.1.8",
|
"framer-motion": "^11.1.8",
|
||||||
"mongodb": "^6.6.1",
|
"mongodb": "^6.6.1",
|
||||||
|
@ -18,10 +19,12 @@
|
||||||
"next-auth": "^4.24.7",
|
"next-auth": "^4.24.7",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-icons": "^5.2.0",
|
"react-icons": "^5.2.0",
|
||||||
"react-scroll-parallax": "^3.4.5",
|
"react-scroll-parallax": "^3.4.5",
|
||||||
"react-slick": "^0.30.2",
|
"react-slick": "^0.30.2",
|
||||||
"slick-carousel": "^1.8.1"
|
"slick-carousel": "^1.8.1",
|
||||||
|
"uniqid": "^5.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.12.12",
|
"@types/node": "20.12.12",
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { Schema, model, models } from "mongoose";
|
import { Schema, model, models } from "mongoose";
|
||||||
import bcrypt from 'bcrypt'
|
|
||||||
|
|
||||||
|
|
||||||
const UserSchema = new Schema({
|
const UserSchema = new Schema({
|
||||||
|
name:{
|
||||||
|
type:String
|
||||||
|
},
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -10,19 +12,11 @@ const UserSchema = new Schema({
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
|
||||||
validate: pass => {
|
|
||||||
if (!pass?.length || pass.length < 5) {
|
|
||||||
new Error('Password must be at least five characters');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
image:{
|
||||||
|
type: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
}, {timestamps: true});
|
}, {timestamps: true});
|
||||||
|
|
||||||
UserSchema.post('validate', function (user) {
|
|
||||||
const passwordNothashed = user.password;
|
|
||||||
const salt = bcrypt.genSaltSync(10);
|
|
||||||
user.password = bcrypt.hashSync(passwordNothashed, salt)
|
|
||||||
});
|
|
||||||
|
|
||||||
export const User = models?.User || model('User', UserSchema);
|
export const User = models?.User || model('User', UserSchema);
|
24
donutshop_ecommerce/src/app/api/profile/route.js
Normal file
24
donutshop_ecommerce/src/app/api/profile/route.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import mongoose from "mongoose"
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import {authOptions} from "../auth/[...nextauth]/route"
|
||||||
|
import { User } from "../models/User";
|
||||||
|
|
||||||
|
export async function PUT(req){
|
||||||
|
mongoose.connect(process.env.MONGO_URL)
|
||||||
|
const data = await req.json();
|
||||||
|
const session = await getServerSession(authOptions)
|
||||||
|
const email = session.user.email
|
||||||
|
const update = {};
|
||||||
|
if('name' in data){
|
||||||
|
update.name = data.name
|
||||||
|
}
|
||||||
|
if('image' in data){
|
||||||
|
update.image = data.image
|
||||||
|
}
|
||||||
|
if(Object.keys(update).length > 0){
|
||||||
|
await User.updateOne({email}, update);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return Response.json(true)
|
||||||
|
}
|
|
@ -1,9 +1,21 @@
|
||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
import { User } from "../models/User.js";
|
import { User } from "../models/User.js";
|
||||||
|
import bcrypt from 'bcrypt'
|
||||||
|
|
||||||
export async function POST(req){
|
export async function POST(req){
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
mongoose.connect(process.env.MONGO_URL);
|
mongoose.connect(process.env.MONGO_URL);
|
||||||
|
|
||||||
|
const pass = body.password
|
||||||
|
if (!pass?.length || pass.length < 5) {
|
||||||
|
new Error('Password must be at least five characters');
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordNothashed = pass;
|
||||||
|
const salt = bcrypt.genSaltSync(10);
|
||||||
|
body.password = bcrypt.hashSync(passwordNothashed, salt)
|
||||||
|
|
||||||
|
|
||||||
const createdUser = await User.create(body)
|
const createdUser = await User.create(body)
|
||||||
return Response.json(createdUser);
|
return Response.json(createdUser);
|
||||||
}
|
}
|
39
donutshop_ecommerce/src/app/api/upload/route.js
Normal file
39
donutshop_ecommerce/src/app/api/upload/route.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import {PutObjectCommand, S3Client} from "@aws-sdk/client-s3"
|
||||||
|
import uniqid from "uniqid";
|
||||||
|
|
||||||
|
|
||||||
|
export async function POST(req){
|
||||||
|
const data = await req.formData();
|
||||||
|
if(data.get('file')){
|
||||||
|
//upload files --> aws service
|
||||||
|
const file = data.get('file')
|
||||||
|
const s3Client = new S3Client({
|
||||||
|
region: 'us-east-2',
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.AWS_ACCESS_KEY,
|
||||||
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const ext = file.name.split('.').slice(-1)[0];
|
||||||
|
const newFileName = uniqid() + '.' +ext;
|
||||||
|
|
||||||
|
const chunks = [];
|
||||||
|
for await(const chunk of file.stream()){
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
const bucket = 'jtp-donutshop'
|
||||||
|
|
||||||
|
|
||||||
|
await s3Client.send(new PutObjectCommand({
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: newFileName,
|
||||||
|
ACL: 'public-read',
|
||||||
|
ContentType: file.type,
|
||||||
|
Body: buffer,
|
||||||
|
}));
|
||||||
|
const link = 'https://'+bucket+'.s3.amazonaws.com/'+newFileName;
|
||||||
|
return Response.json(link);
|
||||||
|
}
|
||||||
|
return Response.json(true);
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ body{
|
||||||
}
|
}
|
||||||
/*===== general setup =====*/
|
/*===== general setup =====*/
|
||||||
input[type="email"], input[type="password"], input[type="text"]{
|
input[type="email"], input[type="password"], input[type="text"]{
|
||||||
@apply border p-2 border-gray-400 block my-2 w-full rounded-md outline-none border-[#DCA0AE]
|
@apply border p-2 block my-2 w-full rounded-md outline-none border-[#DCA0AE]
|
||||||
}
|
}
|
||||||
|
|
||||||
/*===== strokel =====*/
|
/*===== strokel =====*/
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Inter } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import Header from "../components/layout/Header";
|
import Header from "../components/layout/Header";
|
||||||
import AppProvider from "../components/AppContext";
|
import AppProvider from "../components/AppContext";
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
@ -16,6 +17,7 @@ export default function RootLayout({ children }) {
|
||||||
<link rel="shortcut icon" href="/favicon.png" />
|
<link rel="shortcut icon" href="/favicon.png" />
|
||||||
<body className={inter.className} suppressHydrationWarning={true}>
|
<body className={inter.className} suppressHydrationWarning={true}>
|
||||||
<AppProvider>
|
<AppProvider>
|
||||||
|
<Toaster/>
|
||||||
<Header/>
|
<Header/>
|
||||||
{children}
|
{children}
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
|
|
|
@ -1,13 +1,78 @@
|
||||||
"use client"
|
"use client"
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
|
import Image from 'next/image';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import React from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
|
||||||
const ProfilePage = () => {
|
const ProfilePage = () => {
|
||||||
|
//use session
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const {status} = session;
|
const {status} = session;
|
||||||
|
//profile update
|
||||||
|
const [userName, setUserName] = useState('');
|
||||||
|
const [image, setImage] = useState('');
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(status === 'authenticated'){
|
||||||
|
setUserName(session.data.user.name)
|
||||||
|
setImage(session.data.user.image)
|
||||||
|
}
|
||||||
|
},[session, status])
|
||||||
|
|
||||||
|
|
||||||
|
async function handleInfoUpdate(ev){
|
||||||
|
ev.preventDefault()
|
||||||
|
|
||||||
|
const savingPromise = new Promise(async (resolve, reject) => {
|
||||||
|
const response = await fetch('/api/profile', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({name:userName, image}),
|
||||||
|
});
|
||||||
|
if(response.ok)
|
||||||
|
resolve()
|
||||||
|
else
|
||||||
|
reject()
|
||||||
|
});
|
||||||
|
await toast.promise(savingPromise, {
|
||||||
|
loading: 'Saving',
|
||||||
|
success: 'Profile is saved!',
|
||||||
|
error: 'Fail to update profile',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImgchange(ev){
|
||||||
|
const files = ev.target.files;
|
||||||
|
if(files?.length === 1){
|
||||||
|
const data = new FormData;
|
||||||
|
data.set('file', files[0]);
|
||||||
|
|
||||||
|
|
||||||
|
const uploadPromise = fetch('/api/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
}).then(response => {
|
||||||
|
if(response.ok){
|
||||||
|
return response.json().then(link => {
|
||||||
|
setImage(link)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new Error('Something went wrong!')
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
await toast.promise(uploadPromise, {
|
||||||
|
loading: 'Uploading',
|
||||||
|
success: 'An image is uploaded',
|
||||||
|
error: 'Fail to upload image!',
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if(status === 'loading'){
|
if(status === 'loading'){
|
||||||
return 'Loading...'
|
return 'Loading...'
|
||||||
|
@ -17,10 +82,26 @@ const ProfilePage = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='px-5'>
|
<div className='px-5'>
|
||||||
<p className='text-center text-2xl font-semibold uppercase mb-5 text-[#FF5580]'>Profile</p>
|
<p className='text-center text-2xl font-semibold uppercase mb-5 text-[#FF5580]'>Profile</p>
|
||||||
|
<div className='max-w-md mx-auto'>
|
||||||
|
<div className='flex flex-col gap-2 justify-center items-center max-w-[100px] mx-auto'>
|
||||||
|
{image && (
|
||||||
|
<Image src={image} width={100} height={100} alt={'user-image'} className='rounded-full'/>
|
||||||
|
)}
|
||||||
|
<label>
|
||||||
|
<input type='file' onChange={handleImgchange} className='hidden'/>
|
||||||
|
<span className='px-2 py-1 border border-[#DCA0AE] rounded-md text-sm cursor-pointer'>Edit</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<form className='mt-5' onSubmit={handleInfoUpdate}>
|
||||||
|
<input type='text' placeholder='Name' value={userName} onChange={ev => setUserName(ev.target.value)}/>
|
||||||
|
<input type='text' value={session.data.user.email} disabled={true} className='disabled:bg-gray-200 disabled:text-gray-400'/>
|
||||||
|
<button className='rounded-md bg-[#DCA0AE] text-white hover:opacity-80 duration-300 p-2 font-semibold block w-full disabled:cursor-not-allowed'>Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue