Skip to content

Commit

Permalink
Better user authentication (#7)
Browse files Browse the repository at this point in the history
* add wip useUser hook

* add hook useLocalStorage

* feat: unify authentication with new useUser hook

* chore: update dependencies

* fix logout

* improve dahsboard

* move register logic to useUser store
  • Loading branch information
paoloose authored Nov 27, 2024
1 parent 1237b77 commit d5a001e
Show file tree
Hide file tree
Showing 18 changed files with 738 additions and 4,786 deletions.
5,031 changes: 349 additions & 4,682 deletions app/package-lock.json

Large diffs are not rendered by default.

22 changes: 10 additions & 12 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,36 @@
"prettier:write": "prettier --write \"**/*.{ts,tsx}\""
},
"dependencies": {
"@mantine/core": "7.11.2",
"@mantine/form": "^7.14.1",
"@mantine/hooks": "^7.11.2",
"@mantine/core": "7.14.2",
"@mantine/form": "^7.14.2",
"@mantine/hooks": "^7.14.2",
"@mantinex/mantine-logo": "^1.0.1",
"@react-three/drei": "^9.116.3",
"@react-three/drei": "^9.117.3",
"@react-three/fiber": "^8.17.10",
"@react-three/rapier": "^1.5.0",
"@tabler/icons-react": "3.17.0",
"@tanstack/react-query": "^5.61.0",
"axios": "^1.7.7",
"axios": "^1.7.8",
"debounce": "^2.2.0",
"ecctrl": "^1.0.91",
"ecctrl": "^1.0.92",
"konva": "^9.3.16",
"lucide-react": "^0.460.0",
"lucide-react": "^0.461.0",
"r3f-perf": "^7.2.3",
"react": "^18.3.1",
"react-confetti": "^6.1.0",
"react-dom": "^18.3.1",
"react-konva": "^18.2.10",
"react-responsive-masonry": "^2.4.1",
"react-router-dom": "^6.23.1",
"react-use": "^17.5.1",
"swr": "^2.2.5",
"use-image": "^1.1.1",
"wouter": "^3.3.1",
"wouter": "3.3.5",
"zod": "^3.23.8",
"zustand": "^5.0.1"
},
"devDependencies": {
"@react-three/eslint-plugin": "^0.1.1",
"@types/react": "^18.3.3",
"@types/react": "18.3.12",
"@types/react-dom": "^18.3.0",
"@types/react-responsive-masonry": "^2.1.3",
"@typescript-eslint/eslint-plugin": "^7.13.1",
Expand All @@ -67,8 +67,6 @@
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.3.2",
"prop-types": "^15.8.1",
"storybook": "^8.1.10",
"storybook-dark-mode": "^4.0.2",
"stylelint": "^16.6.1",
"stylelint-config-standard-scss": "^13.1.0",
"typescript": "^5.5.2",
Expand Down
Binary file added app/public/assets/examples/thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions app/src/components/Overlays/LoadingScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Center, Stack, Text } from "@mantine/core";
import { AppIcon } from "../AppIcon";

export const LoadingScreen = () => {
return (
<Center h={'100vh'}>
<Stack align="center">
<AppIcon animated size={100} />
<Text size="xl" mt={18}>Cargando metagallery...</Text>
</Stack>
</Center>
);
}
51 changes: 43 additions & 8 deletions app/src/components/UserButton/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Avatar, Menu } from '@mantine/core';
import { useUser } from '@/stores/useUser';
import { Avatar, Group, Menu, Text, UnstyledButton } from '@mantine/core';
import { IconLogout, IconSettings } from '@tabler/icons-react';
import { useLocation } from 'wouter';

Expand All @@ -19,8 +20,11 @@ const MenuItem = ({ icon: Icon, label, onClick }: MenuItemProps) => (
);

export const UserButton = () => {
const { user } = useUser();
const [, setLocation] = useLocation();

if (!user) return null;

const menuItems = [
{
icon: IconSettings,
Expand All @@ -31,8 +35,7 @@ export const UserButton = () => {
icon: IconLogout,
label: 'Cerrar sesión',
onClick: () => {
localStorage.removeItem('metagallery-token');
setLocation('/');
useUser.getState().logout();
},
},
];
Expand All @@ -43,13 +46,45 @@ export const UserButton = () => {
offset={15}
>
<Menu.Target>
<Avatar
src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-1.png"
radius="xl"
style={{ cursor: 'pointer' }}
/>
<UnstyledButton
style={{
borderRadius: 'var(--mantine-radius-sm)',
}}
>
<Group gap="xs" mr={4}>
<Avatar
src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-1.png"
radius="xl"
style={{ cursor: 'pointer' }}
/>
<div style={{ flex: 1, width: 130 }}>
<Text size="sm" fw={500} style={{ overflow: 'hidden', textOverflow: 'ellipsis', }}>
{user.displayname}
</Text>
<Text c="dimmed" size="xs" style={{ overflow: 'hidden', textOverflow: 'ellipsis', }}>
{user.mail}
</Text>
</div>
</Group>
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown>
<Group gap="xs" mr={4} m={6}>
<Avatar
src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-1.png"
radius="xl"
style={{ cursor: 'pointer' }}
size={64}
/>
<div style={{ flex: 1, width: 200 }}>
<Text size="xl" fw={500} style={{ overflow: 'hidden', textOverflow: 'ellipsis', }}>
{user.displayname}
</Text>
<Text c="dimmed" size="xs" style={{ overflow: 'hidden', textOverflow: 'ellipsis', }}>
{user.mail}
</Text>
</div>
</Group>
{menuItems.map((item, index) => (
<MenuItem
key={index}
Expand Down
3 changes: 3 additions & 0 deletions app/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { IconProps } from '@tabler/icons-react';

// Local Storage token key
export const TOKEN_LC_KEY = 'token';

export const primaryIconProps: IconProps = {
size: '18px',
stroke: 1.5,
Expand Down
68 changes: 68 additions & 0 deletions app/src/hooks/useLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { panicIfNull } from '@/utils';
import { useCallback, useEffect, useState } from 'react';
import { z } from 'zod';

type UseLocalStorageProps<S extends z.ZodType<any>, O> = {
schema?: S,
otherwise: O,
};

/**
* Try to parse the value from localStorage with the given schema.
* Returns null when no value, invalid value, or invalid JSON.
*/
const parseValue = <T>(value: any, schema: z.ZodType<T>): T | null => {
try {
const unmarshalled = JSON.parse(value);
return schema.parse(unmarshalled);
} catch {
return null;
}
};

export function useLocalStorage<S extends z.ZodType<z.infer<S>> = z.ZodNull, O = z.infer<S> | null>(
key: string, { schema, otherwise }: UseLocalStorageProps<S, O>
) {
const [value, _setValue] = useState(() => {
const item = window.localStorage.getItem(key);
const parsedItem = schema ? (item !== null ? parseValue(item, schema) : null) : item;

if (parsedItem == null) {
window.localStorage.setItem(key, JSON.stringify(otherwise));
return otherwise;
}
return parsedItem;
});

const setValue = (val: (O | z.TypeOf<S>) | null, opts?: { strict: boolean }) => {
if (val === null) {
window.localStorage.removeItem(key);
_setValue(otherwise);
return;
}

let valToSerialize: O | z.TypeOf<S> = val;
if (opts && opts.strict && schema) {
valToSerialize = schema.parse(val);
}
window.localStorage.setItem(key, JSON.stringify(valToSerialize));
panicIfNull(valToSerialize); // unreachable, to make typescript happy
_setValue(valToSerialize);
};

const onLocalStorageChange = useCallback((e: StorageEvent) => {
if (e.key === key) {
const parsed = schema ? parseValue(e.newValue, schema) : e.newValue;
_setValue(parsed ?? otherwise);
}
}, [key, schema, otherwise]);

useEffect(() => {
window.addEventListener('storage', onLocalStorageChange);
return () => {
window.removeEventListener('storage', onLocalStorageChange);
};
}, []);

return [value, setValue] as const;
}
30 changes: 21 additions & 9 deletions app/src/pages/Dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,55 @@
import React, { useState } from 'react';
import { useState } from 'react';
import { Menu, Search, Share2, Plus, Layout, Edit } from "lucide-react";
import { Link, useLocation } from 'wouter';
import styles from "./GalleryDashboard.module.css";
import { UserButton } from "@/components/UserButton";
import { NewGalleryButton } from '@/components/NewGalleryButton';
import { useUser } from '@/stores/useUser';
import { useMantineTheme } from '@mantine/core';

const galleries = [
{
title: "Salón cúpula",
description: "Una colección que refleja el alma de la expresión",
image: "/placeholder.svg",
image: "/assets/examples/thumbnail.png",
},
{
title: "Habitación de los muros",
description: "Muros y más muros",
image: "/placeholder.svg",
image: "/assets/examples/thumbnail.png",
},
{
title: "Salón vintage",
description: "La colección perfecta para tu arte vintage",
image: "/placeholder.svg",
image: "/assets/examples/thumbnail.png",
},
{
title: "Golden room",
description: "Un espacio al estilo del golden hour",
image: "/placeholder.svg",
image: "/assets/examples/thumbnail.png",
},
{
title: "Arte conexo",
description: "Conectando el arte con la realidad",
image: "/placeholder.svg",
image: "/assets/examples/thumbnail.png",
},
{
title: "Ronda de 3D",
description: "Experiencia inmersiva en 3D",
image: "/placeholder.svg",
image: "/assets/examples/thumbnail.png",
},
];

export const GalleryDashboard = () => {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const theme = useMantineTheme();
const [, setLocation] = useLocation();
const { user } = useUser();

if (!user) {
setLocation('/');
return null;
}

const toggleSidebar = () => {
setIsSidebarOpen(!isSidebarOpen);
Expand Down Expand Up @@ -74,7 +84,9 @@ export const GalleryDashboard = () => {
</div>
</div>

<header className={styles.header}>
<header className={styles.header} style={{
backgroundColor: theme.white,
}}>
<div className={styles.headerContent}>
<button
onClick={toggleSidebar}
Expand All @@ -92,7 +104,7 @@ export const GalleryDashboard = () => {

<main className={styles.main}>
<div className={styles.profileSection}>
<h1 className={styles.profileName}>Rodrigo Alva</h1>
<h1 className={styles.profileName}>{user.displayname}</h1>
<div className={styles.searchContainer}>
<Search className={styles.searchIcon} />
<input
Expand Down
14 changes: 10 additions & 4 deletions app/src/pages/Dashboard/GalleryDashboard.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
min-height: 100vh;
background-color: white;
position: relative;
margin-bottom: 32px;
}

.overlay {
Expand Down Expand Up @@ -70,6 +71,9 @@
}

.header {
top: 0;
z-index: 6969;
position: sticky;
border-bottom: 1px solid #e5e5e5;
}

Expand Down Expand Up @@ -147,6 +151,7 @@
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
padding-bottom: 80px;
}

.profileSection {
Expand Down Expand Up @@ -231,6 +236,7 @@

.galleryGrid {
display: grid;
justify-items: center;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
Expand All @@ -240,13 +246,13 @@
border-radius: 0.5rem;
overflow: hidden;
border: 1px solid #e5e5e5;
width: 350px;
height: 275px;
max-width: 350px;
max-height: 275px;
}

.galleryImage {
width: 100%;
height: auto;
height: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
}
Expand All @@ -256,7 +262,7 @@
inset: 0;
background-color: rgba(0, 0, 0, 0.4);
color: white;
opacity: 0;
opacity: 0.9;
transition: opacity 0.3s ease;
display: flex;
flex-direction: column;
Expand Down
Loading

0 comments on commit d5a001e

Please sign in to comment.