Add settings section
This commit is contained in:
parent
443febac8d
commit
f55f2f6321
12 changed files with 509 additions and 7 deletions
90
components/Settings.tsx
Normal file
90
components/Settings.tsx
Normal file
|
@ -0,0 +1,90 @@
|
|||
import Link from 'next/link';
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function SettingsShell(props) {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div>
|
||||
<main className="relative -mt-32">
|
||||
<div>
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div className="divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
|
||||
<aside className="py-6 lg:col-span-3">
|
||||
<nav className="space-y-1">
|
||||
<Link href="/settings/profile">
|
||||
<a className={router.pathname == "/settings/profile" ? "bg-blue-50 border-blue-500 text-blue-700 hover:bg-blue-50 hover:text-blue-700 group border-l-4 px-3 py-2 flex items-center text-sm font-medium" : "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group border-l-4 px-3 py-2 flex items-center text-sm font-medium"} aria-current="page">
|
||||
<svg className={router.pathname == "/settings/profile" ? "text-blue-500 group-hover:text-blue-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6" : "text-gray-400 group-hover:text-gray-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span className="truncate">
|
||||
Profile
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
{/* <Link href="/settings/account">
|
||||
<a className={router.pathname == "/settings/account" ? "bg-blue-50 border-blue-500 text-blue-700 hover:bg-blue-50 hover:text-blue-700 group border-l-4 px-3 py-2 flex items-center text-sm font-medium" : "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group border-l-4 px-3 py-2 flex items-center text-sm font-medium"}>
|
||||
<svg className={router.pathname == "/settings/account" ? "text-blue-500 group-hover:text-blue-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6" : "text-gray-400 group-hover:text-gray-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span className="truncate">
|
||||
Account
|
||||
</span>
|
||||
</a>
|
||||
</Link> */}
|
||||
|
||||
<Link href="/settings/password">
|
||||
<a className={router.pathname == "/settings/password" ? "bg-blue-50 border-blue-500 text-blue-700 hover:bg-blue-50 hover:text-blue-700 group border-l-4 px-3 py-2 flex items-center text-sm font-medium" : "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group border-l-4 px-3 py-2 flex items-center text-sm font-medium"}>
|
||||
<svg className={router.pathname == "/settings/password" ? "text-blue-500 group-hover:text-blue-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6" : "text-gray-400 group-hover:text-gray-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
<span className="truncate">
|
||||
Password
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
{/* <Link href="/settings/notifications">
|
||||
<a className={router.pathname == "/settings/notifications" ? "bg-blue-50 border-blue-500 text-blue-700 hover:bg-blue-50 hover:text-blue-700 group border-l-4 px-3 py-2 flex items-center text-sm font-medium" : "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group border-l-4 px-3 py-2 flex items-center text-sm font-medium"}>
|
||||
<svg className={router.pathname == "/settings/notifications" ? "text-blue-500 group-hover:text-blue-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6" : "text-gray-400 group-hover:text-gray-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
</svg>
|
||||
<span className="truncate">
|
||||
Notifications
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Link href="/settings/billing">
|
||||
<a className={router.pathname == "/settings/billing" ? "bg-blue-50 border-blue-500 text-blue-700 hover:bg-blue-50 hover:text-blue-700 group border-l-4 px-3 py-2 flex items-center text-sm font-medium" : "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group border-l-4 px-3 py-2 flex items-center text-sm font-medium"}>
|
||||
<svg className={router.pathname == "/settings/billing" ? "text-blue-500 group-hover:text-blue-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6" : "text-gray-400 group-hover:text-gray-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
||||
</svg>
|
||||
<span className="truncate">
|
||||
Billing
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Link href="/settings/integrations">
|
||||
<a className={router.pathname == "/settings/integrations" ? "bg-blue-50 border-blue-500 text-blue-700 hover:bg-blue-50 hover:text-blue-700 group border-l-4 px-3 py-2 flex items-center text-sm font-medium" : "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group border-l-4 px-3 py-2 flex items-center text-sm font-medium"}>
|
||||
<svg className={router.pathname == "/settings/integrations" ? "text-blue-500 group-hover:text-blue-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6" : "text-gray-400 group-hover:text-gray-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 14v6m-3-3h6M6 10h2a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2zm10 0h2a2 2 0 002-2V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v2a2 2 0 002 2zM6 20h2a2 2 0 002-2v-2a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="truncate">
|
||||
Integrations
|
||||
</span>
|
||||
</a>
|
||||
</Link> */}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -21,7 +21,7 @@ export default function Shell(props) {
|
|||
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<img className="h-6" src="calendso-white.svg" alt="Calendso" />
|
||||
<img className="h-6" src="/calendso-white.svg" alt="Calendso" />
|
||||
</div>
|
||||
<div className="hidden md:block">
|
||||
<div className="ml-10 flex items-baseline space-x-4">
|
||||
|
@ -37,8 +37,8 @@ export default function Shell(props) {
|
|||
<Link href="/integrations">
|
||||
<a className={router.pathname.startsWith("/integrations") ? "bg-gray-700 text-white px-3 py-2 rounded-md text-sm font-medium" : "text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"}>Integrations</a>
|
||||
</Link>
|
||||
<Link href="/">
|
||||
<a className={router.pathname.startsWith("/team") ? "bg-gray-700 text-white px-3 py-2 rounded-md text-sm font-medium" : "text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"}>Team</a>
|
||||
<Link href="/settings">
|
||||
<a className={router.pathname.startsWith("/settings") ? "bg-gray-700 text-white px-3 py-2 rounded-md text-sm font-medium" : "text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"}>Settings</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
47
pages/api/auth/changepw.ts
Normal file
47
pages/api/auth/changepw.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { hashPassword, verifyPassword } from '../../../lib/auth';
|
||||
import { getSession } from 'next-auth/client';
|
||||
import prisma from '../../../lib/prisma';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({req: req});
|
||||
|
||||
if (!session) {
|
||||
res.status(401).json({message: "Not authenticated"});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Add user ID to user session object
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: session.user.email,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
password: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) { res.status(404).json({message: 'User not found'}); return; }
|
||||
|
||||
const oldPassword = req.body.oldPassword;
|
||||
const newPassword = req.body.newPassword;
|
||||
const currentPassword = user.password;
|
||||
|
||||
const passwordsMatch = await verifyPassword(oldPassword, currentPassword);
|
||||
|
||||
if (!passwordsMatch) { res.status(403).json({message: 'Incorrect password'}); return; }
|
||||
|
||||
const hashedPassword = await hashPassword(newPassword);
|
||||
|
||||
const updateUser = await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json({message: 'Password updated successfully'});
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import prisma from '../../../lib/prisma'
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import prisma from '../../../lib/prisma';
|
||||
const {google} = require('googleapis');
|
||||
|
||||
const credentials = process.env.GOOGLE_API_CREDENTIALS;
|
||||
|
|
42
pages/api/user/profile.ts
Normal file
42
pages/api/user/profile.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { getSession } from 'next-auth/client';
|
||||
import prisma from '../../../lib/prisma';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({req: req});
|
||||
|
||||
if (!session) {
|
||||
res.status(401).json({message: "Not authenticated"});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Add user ID to user session object
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: session.user.email,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
password: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) { res.status(404).json({message: 'User not found'}); return; }
|
||||
|
||||
const username = req.body.username;
|
||||
const name = req.body.name;
|
||||
const description = req.body.description;
|
||||
|
||||
const updateUser = await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
username: username,
|
||||
name: name,
|
||||
bio: description
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json({message: 'Profile updated successfully'});
|
||||
}
|
|
@ -18,7 +18,7 @@ export default function Error() {
|
|||
<div>
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
|
||||
<svg className="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
|
|
43
pages/auth/logout.tsx
Normal file
43
pages/auth/logout.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function Logout() {
|
||||
|
||||
return (
|
||||
<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<Head>
|
||||
<title>Logged out - Calendso</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
||||
<div>
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
|
||||
<svg className="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
||||
You've been logged out
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
We hope to see you again soon!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<Link href="/auth/login">
|
||||
<a className="inline-flex justify-center w-full rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:text-sm">
|
||||
Go back to the login page
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
19
pages/settings/index.tsx
Normal file
19
pages/settings/index.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { useRouter } from 'next/router';
|
||||
import { signIn, useSession, getSession } from 'next-auth/client';
|
||||
|
||||
export default function Settings() {
|
||||
const [ session, loading ] = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
if (loading) {
|
||||
return <p className="text-gray-400">Loading...</p>;
|
||||
} else {
|
||||
if (!session) {
|
||||
window.location.href = "/auth/login";
|
||||
}
|
||||
}
|
||||
|
||||
router.push('/settings/profile');
|
||||
|
||||
return null;
|
||||
}
|
104
pages/settings/password.tsx
Normal file
104
pages/settings/password.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useRef } from 'react';
|
||||
import prisma from '../../lib/prisma';
|
||||
import Shell from '../../components/Shell';
|
||||
import SettingsShell from '../../components/Settings';
|
||||
import { signIn, useSession, getSession } from 'next-auth/client';
|
||||
|
||||
export default function Settings(props) {
|
||||
const [ session, loading ] = useSession();
|
||||
const oldPasswordRef = useRef();
|
||||
const newPasswordRef = useRef();
|
||||
|
||||
if (loading) {
|
||||
return <p className="text-gray-400">Loading...</p>;
|
||||
} else {
|
||||
if (!session) {
|
||||
window.location.href = "/auth/login";
|
||||
}
|
||||
}
|
||||
|
||||
async function changePasswordHandler(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const enteredOldPassword = oldPasswordRef.current.value;
|
||||
const enteredNewPassword = newPasswordRef.current.value;
|
||||
|
||||
// TODO: Add validation
|
||||
|
||||
const response = await fetch('/api/auth/changepw', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({oldPassword: enteredOldPassword, newPassword: enteredNewPassword}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
console.log(response);
|
||||
}
|
||||
|
||||
return(
|
||||
<Shell title="Password">
|
||||
<Head>
|
||||
<title>Change Password | Calendso</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<SettingsShell>
|
||||
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={changePasswordHandler}>
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900">Change Password</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Change the password for your Calendso account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex">
|
||||
<div className="w-1/2 mr-2">
|
||||
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">Current Password</label>
|
||||
<div className="mt-1">
|
||||
<input ref={oldPasswordRef} type="password" name="current_password" id="current_password" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="Your old password" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1/2 ml-2">
|
||||
<label htmlFor="new_password" className="block text-sm font-medium text-gray-700">New Password</label>
|
||||
<div className="mt-1">
|
||||
<input ref={newPasswordRef} type="password" name="new_password" id="new_password" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="Your super secure new password" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="mt-8" />
|
||||
<div className="py-4 flex justify-end">
|
||||
<button type="button" className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="ml-2 bg-blue-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</SettingsShell>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
const session = await getSession(context);
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: session.user.email,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
props: {user}, // will be passed to the page component as props
|
||||
}
|
||||
}
|
157
pages/settings/profile.tsx
Normal file
157
pages/settings/profile.tsx
Normal file
|
@ -0,0 +1,157 @@
|
|||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useRef } from 'react';
|
||||
import prisma from '../../lib/prisma';
|
||||
import Shell from '../../components/Shell';
|
||||
import SettingsShell from '../../components/Settings';
|
||||
import { signIn, useSession, getSession } from 'next-auth/client';
|
||||
|
||||
export default function Settings(props) {
|
||||
const [ session, loading ] = useSession();
|
||||
const usernameRef = useRef();
|
||||
const nameRef = useRef();
|
||||
const descriptionRef = useRef();
|
||||
|
||||
if (loading) {
|
||||
return <p className="text-gray-400">Loading...</p>;
|
||||
} else {
|
||||
if (!session) {
|
||||
window.location.href = "/auth/login";
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProfileHandler(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const enteredUsername = usernameRef.current.value;
|
||||
const enteredName = nameRef.current.value;
|
||||
const enteredDescription = descriptionRef.current.value;
|
||||
|
||||
// TODO: Add validation
|
||||
|
||||
const response = await fetch('/api/user/profile', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({username: enteredUsername, name: enteredName, description: enteredDescription}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
console.log(response);
|
||||
}
|
||||
|
||||
return(
|
||||
<Shell title="Profile">
|
||||
<Head>
|
||||
<title>Profile | Calendso</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<SettingsShell>
|
||||
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={updateProfileHandler}>
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900">Profile</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Review and change your public page details.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col lg:flex-row">
|
||||
<div className="flex-grow space-y-6">
|
||||
<div className="flex">
|
||||
<div className="w-1/2 mr-2">
|
||||
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
|
||||
Username
|
||||
</label>
|
||||
<div className="mt-1 rounded-md shadow-sm flex">
|
||||
<span className="bg-gray-50 border border-r-0 border-gray-300 rounded-l-md px-3 inline-flex items-center text-gray-500 sm:text-sm">
|
||||
{window.location.hostname}/
|
||||
</span>
|
||||
<input ref={usernameRef} type="text" name="username" id="username" autoComplete="username" className="focus:ring-blue-500 focus:border-blue-500 flex-grow block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300" defaultValue={props.user.username} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1/2 ml-2">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">Full name</label>
|
||||
<input ref={nameRef} type="text" name="name" id="name" autoComplete="given-name" placeholder="Your name" className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" value={props.user.name} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="about" className="block text-sm font-medium text-gray-700">
|
||||
About
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<textarea ref={descriptionRef} id="about" name="about" placeholder="A little something about yourself." rows={3} className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md">{props.user.bio}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex-grow lg:mt-0 lg:ml-6 lg:flex-grow-0 lg:flex-shrink-0">
|
||||
<p className="mb-2 text-sm font-medium text-gray-700" aria-hidden="true">
|
||||
Photo
|
||||
</p>
|
||||
<div className="mt-1 lg:hidden">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 inline-block rounded-full overflow-hidden h-12 w-12" aria-hidden="true">
|
||||
<img className="rounded-full h-full w-full" src={props.user.avatar} alt="" />
|
||||
</div>
|
||||
<div className="ml-5 rounded-md shadow-sm">
|
||||
<div className="group relative border border-gray-300 rounded-md py-2 px-3 flex items-center justify-center hover:bg-gray-50 focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-blue-500">
|
||||
<label htmlFor="user_photo" className="relative text-sm leading-4 font-medium text-gray-700 pointer-events-none">
|
||||
<span>Change</span>
|
||||
<span className="sr-only"> user photo</span>
|
||||
</label>
|
||||
<input id="user_photo" name="user_photo" type="file" className="absolute w-full h-full opacity-0 cursor-pointer border-gray-300 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden relative rounded-full overflow-hidden lg:block">
|
||||
{props.user.avatar && <img className="relative rounded-full w-40 h-40" src={props.user.avatar} alt="" />}
|
||||
{!props.user.avatar && <div className="relative bg-blue-600 rounded-full w-40 h-40"></div>}
|
||||
<label htmlFor="user-photo" className="absolute inset-0 w-full h-full bg-black bg-opacity-75 flex items-center justify-center text-sm font-medium text-white opacity-0 hover:opacity-100 focus-within:opacity-100">
|
||||
<span>Change</span>
|
||||
<span className="sr-only"> user photo</span>
|
||||
<input type="file" id="user-photo" name="user-photo" className="absolute inset-0 w-full h-full opacity-0 cursor-pointer border-gray-300 rounded-md" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="mt-8" />
|
||||
<div className="py-4 flex justify-end">
|
||||
<button type="button" className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="ml-2 bg-blue-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</SettingsShell>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
const session = await getSession(context);
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: session.user.email,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
bio: true,
|
||||
avatar: true
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
props: {user}, // will be passed to the page component as props
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ export default function Success(props) {
|
|||
<div>
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
|
||||
<svg className="h-6 w-6 text-green-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
|
|
Loading…
Reference in a new issue