Merge pull request 'Define a basic login form with redirection to the Dashboard' (#127) from login-register-forms into main
Reviewed-on: #127
This commit is contained in:
commit
1908b76822
30
app/[slug]/page.tsx
Normal file
30
app/[slug]/page.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/* Copyright (C) 2024 Manuel Bustillo*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import styles from '@/app/ui/home.module.css';
|
||||||
|
import LoginForm from '@/app/ui/components/login-form';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
||||||
|
|
||||||
|
localStorage.setItem('slug', (await params).slug)
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-screen flex-col p-6">
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<div className="w-1/2">
|
||||||
|
|
||||||
|
Already have an account? Sign in
|
||||||
|
|
||||||
|
<LoginForm />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-1/2">
|
||||||
|
Don't have an account? Register now!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
36
app/api/authentication.tsx
Normal file
36
app/api/authentication.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/* Copyright (C) 2024 Manuel Bustillo*/
|
||||||
|
|
||||||
|
import { getCsrfToken, getSlug } from '@/app/lib/utils';
|
||||||
|
import { User } from '@/app/lib/definitions';
|
||||||
|
|
||||||
|
export function login({ email, password, onLogin }: { email: string, password: string, onLogin: (user: User) => void }) {
|
||||||
|
console.log(email, password);
|
||||||
|
return fetch(`/api/${getSlug()}/users/sign_in`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ user: { email, password } }),
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': getCsrfToken(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data: any) => {
|
||||||
|
console.log(data);
|
||||||
|
onLogin({
|
||||||
|
id: data.id || '',
|
||||||
|
email: data.email || '',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => console.error(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout({ onLogout }: { onLogout: () => void }) {
|
||||||
|
fetch(`/api/${getSlug()}/users/sign_out`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': getCsrfToken(),
|
||||||
|
}
|
||||||
|
}).then(onLogout)
|
||||||
|
.catch((error) => console.error(error));
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
/* Copyright (C) 2024 Manuel Bustillo*/
|
/* Copyright (C) 2024 Manuel Bustillo*/
|
||||||
|
|
||||||
import { Expense } from '@/app/lib/definitions';
|
import { Expense } from '@/app/lib/definitions';
|
||||||
import { getCsrfToken } from '@/app/lib/utils';
|
import { getCsrfToken, getSlug } from '@/app/lib/utils';
|
||||||
|
|
||||||
export function loadExpenses(onLoad?: (expenses: Expense[]) => void) {
|
export function loadExpenses(onLoad?: (expenses: Expense[]) => void) {
|
||||||
fetch("/api/default/expenses")
|
fetch(`/api/${getSlug()}/expenses`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
onLoad && onLoad(data.map((record: any) => {
|
onLoad && onLoad(data.map((record: any) => {
|
||||||
@ -21,7 +21,7 @@ export function loadExpenses(onLoad?: (expenses: Expense[]) => void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function updateExpense(expense: Expense) {
|
export function updateExpense(expense: Expense) {
|
||||||
fetch(`/api/default/expenses/${expense.id}`,
|
fetch(`/api/${getSlug()}/expenses/${expense.id}`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
/* Copyright (C) 2024 Manuel Bustillo*/
|
/* Copyright (C) 2024 Manuel Bustillo*/
|
||||||
|
|
||||||
import { Group } from '@/app/lib/definitions';
|
import { Group } from '@/app/lib/definitions';
|
||||||
|
import { getSlug } from '../lib/utils';
|
||||||
|
|
||||||
export function loadGroups(onLoad?: (groups: Group[]) => void) {
|
export function loadGroups(onLoad?: (groups: Group[]) => void) {
|
||||||
fetch("/api/default/groups")
|
fetch(`/api/${getSlug()}/groups`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
onLoad && onLoad(data.map((record: any) => {
|
onLoad && onLoad(data.map((record: any) => {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
/* Copyright (C) 2024 Manuel Bustillo*/
|
/* Copyright (C) 2024 Manuel Bustillo*/
|
||||||
|
|
||||||
import { Guest } from '@/app/lib/definitions';
|
import { Guest } from '@/app/lib/definitions';
|
||||||
import { getCsrfToken } from '@/app/lib/utils';
|
import { getCsrfToken, getSlug } from '@/app/lib/utils';
|
||||||
|
|
||||||
export function loadGuests(onLoad?: (guests: Guest[]) => void) {
|
export function loadGuests(onLoad?: (guests: Guest[]) => void) {
|
||||||
fetch("/api/default/guests")
|
fetch(`/api/${getSlug()}/guests`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
onLoad && onLoad(data.map((record: any) => {
|
onLoad && onLoad(data.map((record: any) => {
|
||||||
@ -22,7 +22,7 @@ export function loadGuests(onLoad?: (guests: Guest[]) => void) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function updateGuest(guest: Guest) {
|
export function updateGuest(guest: Guest) {
|
||||||
return fetch(`/api/default/guests/${guest.id}`,
|
return fetch(`/api/${getSlug()}/guests/${guest.id}`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ guest: { name: guest.name, status: guest.status } }),
|
body: JSON.stringify({ guest: { name: guest.name, status: guest.status } }),
|
||||||
@ -35,7 +35,7 @@ export function updateGuest(guest: Guest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createGuest(name: string, group_id: string, onCreate?: () => void) {
|
export function createGuest(name: string, group_id: string, onCreate?: () => void) {
|
||||||
fetch("/api/default/guests", {
|
fetch(`/api/${getSlug()}/guests`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ name: name, group_id: group_id }),
|
body: JSON.stringify({ name: name, group_id: group_id }),
|
||||||
headers: {
|
headers: {
|
||||||
@ -51,7 +51,7 @@ export function createGuest(name: string, group_id: string, onCreate?: () => voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function destroyGuest(guest: Guest, onDestroy?: () => void) {
|
export function destroyGuest(guest: Guest, onDestroy?: () => void) {
|
||||||
fetch(`/api/default/guests/${guest.id}`, {
|
fetch(`/api/${getSlug()}/guests/${guest.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'X-CSRF-TOKEN': getCsrfToken(),
|
'X-CSRF-TOKEN': getCsrfToken(),
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
/* Copyright (C) 2024 Manuel Bustillo*/
|
/* Copyright (C) 2024 Manuel Bustillo*/
|
||||||
|
|
||||||
import { TableArrangement } from '@/app/lib/definitions';
|
import { TableArrangement } from '@/app/lib/definitions';
|
||||||
|
import { getSlug } from '../lib/utils';
|
||||||
|
|
||||||
export function loadTableSimulations(onLoad?: (tableSimulations: TableArrangement[]) => void) {
|
export function loadTableSimulations(onLoad?: (tableSimulations: TableArrangement[]) => void) {
|
||||||
fetch('/api/default/tables_arrangements')
|
fetch(`/api/${getSlug()}/tables_arrangements`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
onLoad && onLoad(data.map((record: any) => {
|
onLoad && onLoad(data.map((record: any) => {
|
||||||
|
@ -54,3 +54,8 @@ export type guestsTable = {
|
|||||||
amount: number;
|
amount: number;
|
||||||
status: 'pending' | 'paid';
|
status: 'pending' | 'paid';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
}
|
@ -6,3 +6,5 @@ export const getCsrfToken = () => {
|
|||||||
.find((row) => row.startsWith("csrf-token"))
|
.find((row) => row.startsWith("csrf-token"))
|
||||||
?.split("=")[1] || 'unknown';
|
?.split("=")[1] || 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getSlug = () => localStorage.getItem('slug') || 'default';
|
||||||
|
12
app/page.tsx
12
app/page.tsx
@ -1,12 +0,0 @@
|
|||||||
/* Copyright (C) 2024 Manuel Bustillo*/
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
import styles from '@/app/ui/home.module.css';
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return (
|
|
||||||
<main className="flex min-h-screen flex-col p-6">
|
|
||||||
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
51
app/ui/components/login-form.tsx
Normal file
51
app/ui/components/login-form.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/* Copyright (C) 2024 Manuel Bustillo*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FloatLabel } from 'primereact/floatlabel';
|
||||||
|
import { InputText } from 'primereact/inputtext';
|
||||||
|
import { login } from '../../api/authentication';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { classNames } from './button';
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { User } from '../../lib/definitions';
|
||||||
|
import { getSlug } from '@/app/lib/utils';
|
||||||
|
|
||||||
|
export default function LoginForm() {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('currentUser', JSON.stringify(currentUser));
|
||||||
|
}, [currentUser]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card flex justify-evenly py-5">
|
||||||
|
<FloatLabel>
|
||||||
|
<InputText id="email" type="email" className='rounded-sm' onChange={(e) => setEmail(e.target.value)} />
|
||||||
|
<label htmlFor="email">Email</label>
|
||||||
|
</FloatLabel>
|
||||||
|
<FloatLabel>
|
||||||
|
<InputText id="password" type="password" className='rounded-sm' onChange={(e) => setPassword(e.target.value)} />
|
||||||
|
<label htmlFor="password">Password</label>
|
||||||
|
</FloatLabel>
|
||||||
|
<button
|
||||||
|
className={classNames('primary')}
|
||||||
|
disabled={email.length == 0 || password.length == 0}
|
||||||
|
onClick={() => login({
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
onLogin: (user) => {
|
||||||
|
setCurrentUser(user);
|
||||||
|
router.push(`${getSlug()}/dashboard`)
|
||||||
|
}
|
||||||
|
})}>
|
||||||
|
Sign in
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -10,13 +10,14 @@ import {
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { getSlug } from '@/app/lib/utils';
|
||||||
|
|
||||||
// Map of links to display in the side navigation.
|
// Map of links to display in the side navigation.
|
||||||
// Depending on the size of the application, this would be stored in a database.
|
// Depending on the size of the application, this would be stored in a database.
|
||||||
const links = [
|
const links = [
|
||||||
{ name: 'Guests', href: '/dashboard/guests', icon: UserGroupIcon },
|
{ name: 'Guests', href: `/${getSlug()}/dashboard/guests`, icon: UserGroupIcon },
|
||||||
{ name: 'Expenses', href: '/dashboard/expenses', icon: BanknotesIcon },
|
{ name: 'Expenses', href: `/${getSlug()}/dashboard/expenses`, icon: BanknotesIcon },
|
||||||
{ name: 'Table distributions', href: '/dashboard/tables', icon: RectangleGroupIcon },
|
{ name: 'Table distributions', href: `/${getSlug()}/dashboard/tables`, icon: RectangleGroupIcon },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,30 +1,46 @@
|
|||||||
/* Copyright (C) 2024 Manuel Bustillo*/
|
/* Copyright (C) 2024 Manuel Bustillo*/
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import NavLinks from '@/app/ui/dashboard/nav-links';
|
import NavLinks from '@/app/ui/dashboard/nav-links';
|
||||||
import { PowerIcon } from '@heroicons/react/24/outline';
|
import { PowerIcon } from '@heroicons/react/24/outline';
|
||||||
import { gloriaHallelujah } from '@/app/ui/fonts';
|
import { gloriaHallelujah } from '@/app/ui/fonts';
|
||||||
|
import { logout } from '@/app/api/authentication';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { getSlug } from '@/app/lib/utils';
|
||||||
|
|
||||||
export default function SideNav() {
|
export default function SideNav() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col px-3 py-4 md:px-2">
|
<div className="flex h-full flex-col px-3 py-4 md:px-2">
|
||||||
<Link
|
<Link
|
||||||
className="mb-2 flex h-20 items-center justify-start rounded-md bg-blue-600 p-4 md:h-20"
|
className="mb-2 flex h-20 items-center justify-start rounded-md bg-blue-600 p-4 md:h-20"
|
||||||
href="/dashboard/guests"
|
href="/dashboard/guests"
|
||||||
>
|
>
|
||||||
<div className={`${gloriaHallelujah.className} "w-32 text-white md:w-40 antialiased` }>
|
<div className={`${gloriaHallelujah.className} "w-32 text-white md:w-40 antialiased`}>
|
||||||
<h1>Wedding Planner</h1>
|
<h1>Wedding Planner</h1>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
|
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
|
||||||
<NavLinks />
|
<NavLinks />
|
||||||
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
|
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
|
||||||
<form>
|
<span>Logged in as {JSON.parse(localStorage.getItem('currentUser') || '{}').email}</span>
|
||||||
<button className="flex h-[48px] w-full grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
|
<button
|
||||||
|
className="flex h-[48px] w-full grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3"
|
||||||
|
onClick={() => {
|
||||||
|
logout({
|
||||||
|
onLogout: () => {
|
||||||
|
localStorage.clear();
|
||||||
|
router.push(`/${getSlug()}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
<PowerIcon className="w-6" />
|
<PowerIcon className="w-6" />
|
||||||
<div className="hidden md:block">Sign Out</div>
|
<div className="hidden md:block">Sign Out</div>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -65,7 +65,7 @@ const mockGroupsAPI = ({ page }: { page: Page }) => {
|
|||||||
test('should display the list of guests', async ({ page }) => {
|
test('should display the list of guests', async ({ page }) => {
|
||||||
await mockGuestsAPI({ page });
|
await mockGuestsAPI({ page });
|
||||||
|
|
||||||
await page.goto('/dashboard/guests');
|
await page.goto('/default/dashboard/guests');
|
||||||
|
|
||||||
await expect(page.getByRole('tab', { name: 'Groups' })).toBeVisible();
|
await expect(page.getByRole('tab', { name: 'Groups' })).toBeVisible();
|
||||||
await expect(page.getByRole('tab', { name: 'Guests' })).toBeVisible();
|
await expect(page.getByRole('tab', { name: 'Guests' })).toBeVisible();
|
||||||
@ -90,7 +90,7 @@ test('should display the list of guests', async ({ page }) => {
|
|||||||
test('should display the list of groups', async ({ page }) => {
|
test('should display the list of groups', async ({ page }) => {
|
||||||
await mockGroupsAPI({ page });
|
await mockGroupsAPI({ page });
|
||||||
|
|
||||||
await page.goto('/dashboard/guests');
|
await page.goto('/default/dashboard/guests');
|
||||||
await page.getByRole('tab', { name: 'Groups' }).click();
|
await page.getByRole('tab', { name: 'Groups' }).click();
|
||||||
|
|
||||||
await expect(page.getByText('There are 2 elements in the list')).toBeVisible();
|
await expect(page.getByText('There are 2 elements in the list')).toBeVisible();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user