Commit c3002714 authored by Ли Джен Сеп's avatar Ли Джен Сеп 💬

less 90

parent cbfe9973
......@@ -18,7 +18,8 @@
"next": "15.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-redux": "^9.2.0"
"react-redux": "^9.2.0",
"zustand": "^5.0.8"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
......@@ -28,5 +29,11 @@
"eslint": "^9",
"eslint-config-next": "15.1.0",
"typescript": "^5"
},
"pnpm": {
"onlyBuiltDependencies": [
"sharp",
"unrs-resolver"
]
}
}
......@@ -38,6 +38,9 @@ importers:
react-redux:
specifier: ^9.2.0
version: 9.2.0(@types/react@19.1.10)(react@19.1.1)(redux@5.0.1)
zustand:
specifier: ^5.0.8
version: 5.0.8(@types/react@19.1.10)(immer@10.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1))
devDependencies:
'@eslint/eslintrc':
specifier: ^3
......@@ -1955,6 +1958,24 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
zustand@5.0.8:
resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
immer: '>=9.0.6'
react: '>=18.0.0'
use-sync-external-store: '>=1.2.0'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
use-sync-external-store:
optional: true
snapshots:
'@babel/code-frame@7.27.1':
......@@ -4128,3 +4149,10 @@ snapshots:
yaml@1.10.2: {}
yocto-queue@0.1.0: {}
zustand@5.0.8(@types/react@19.1.10)(immer@10.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)):
optionalDependencies:
'@types/react': 19.1.10
immer: 10.1.1
react: 19.1.1
use-sync-external-store: 1.5.0(react@19.1.1)
.wrapper {
min-height: 100dvh;
display: grid;
place-items: center;
padding: 24px;
background: #eaf3fc;
}
.card {
width: 100%;
max-width: 420px;
background: #ffffff;
border: 1px solid #cfe0f5;
border-radius: 16px;
padding: 24px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
}
.title {
margin: 0 0 16px 0;
font-size: 22px;
font-weight: 700;
color: #1e3a8a;
}
.field {
margin-bottom: 12px;
}
.label {
display: block;
font-size: 13px;
color: #1e40af;
margin-bottom: 6px;
}
.input {
width: 100%;
background: #f0f6ff;
border: 1px solid #93c5fd;
color: #1e3a8a;
border-radius: 10px;
padding: 10px 12px;
outline: none;
transition: border-color 0.15s ease, background-color 0.15s ease;
}
.input:focus {
border-color: #2563eb;
background-color: #e0f0ff;
}
.button {
width: 100%;
margin-top: 8px;
padding: 12px;
border: none;
border-radius: 10px;
background: #2563eb;
color: white;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
}
.button:hover:not(:disabled) {
background: #1d4ed8;
}
.button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.secondary {
width: 100%;
margin-top: 8px;
padding: 10px;
border-radius: 10px;
background: transparent;
border: 1px solid #93c5fd;
color: #1e40af;
cursor: pointer;
transition: background-color 0.2s ease;
}
.secondary:hover {
background-color: #e0f0ff;
}
.hint {
text-align: center;
margin-top: 14px;
color: #1e3a8a;
font-size: 14px;
}
.link {
color: #2563eb;
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
.error {
margin: 6px 0 0;
color: #dc2626;
font-size: 12px;
}
.saved {
margin-top: 16px;
padding: 12px;
border-radius: 10px;
background: #f0f6ff;
border: 1px solid #93c5fd;
color: #1e3a8a;
font-size: 14px;
}
"use client";
import { FormEvent, useState } from "react";
import styles from "./LoginPage.module.css";
import Link from "next/link";
import { useUserStore } from "@/features/userStore";
import { axiosApiClient } from "@/helpers/axiosClient";
import { useRouter } from "next/navigation";
export default function Page() {
const router = useRouter();
const { user, setUser } = useUserStore();
const [userName, setUserName] = useState(user?.userName ?? "");
const [password, setPassword] = useState("");
const onSubmit = async (e: FormEvent) => {
e.preventDefault();
const newUser = {
userName,
password,
};
const { data } = await axiosApiClient.post("/auth/login", newUser);
setUser(data);
router.push("/");
};
return (
<main className={styles.wrapper}>
<form onSubmit={onSubmit} className={styles.card} noValidate>
<h1 className={styles.title}>Логин</h1>
<div className={styles.field}>
<label className={styles.label} htmlFor="userName">
Логин
</label>
<input
id="name"
className={styles.input}
value={userName}
onChange={(e) => setUserName(e.target.value)}
placeholder="name"
/>
{!userName && <p className={styles.error}>Введите логин</p>}
</div>
<div className={styles.field}>
<label className={styles.label} htmlFor="password">
Пароль
</label>
<input
id="password"
type="password"
className={styles.input}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••"
/>
{password && password.length < 6 && <p className={styles.error}>Минимум 6 символов</p>}
</div>
<button className={styles.button}>Зарегистрироваться</button>
<p className={styles.hint}>
Уже есть аккаунт?
<Link href="/login" className={styles.link}>
Войти
</Link>
</p>
</form>
</main>
);
}
.wrapper {
min-height: 100dvh;
display: grid;
place-items: center;
padding: 24px;
background: #eaf3fc;
}
.card {
width: 100%;
max-width: 420px;
background: #ffffff;
border: 1px solid #cfe0f5;
border-radius: 16px;
padding: 24px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
}
.title {
margin: 0 0 16px 0;
font-size: 22px;
font-weight: 700;
color: #1e3a8a;
}
.field {
margin-bottom: 12px;
}
.label {
display: block;
font-size: 13px;
color: #1e40af;
margin-bottom: 6px;
}
.input {
width: 100%;
background: #f0f6ff;
border: 1px solid #93c5fd;
color: #1e3a8a;
border-radius: 10px;
padding: 10px 12px;
outline: none;
transition: border-color 0.15s ease, background-color 0.15s ease;
}
.input:focus {
border-color: #2563eb;
background-color: #e0f0ff;
}
.button {
width: 100%;
margin-top: 8px;
padding: 12px;
border: none;
border-radius: 10px;
background: #2563eb;
color: white;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
}
.button:hover:not(:disabled) {
background: #1d4ed8;
}
.button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.secondary {
width: 100%;
margin-top: 8px;
padding: 10px;
border-radius: 10px;
background: transparent;
border: 1px solid #93c5fd;
color: #1e40af;
cursor: pointer;
transition: background-color 0.2s ease;
}
.secondary:hover {
background-color: #e0f0ff;
}
.hint {
text-align: center;
margin-top: 14px;
color: #1e3a8a;
font-size: 14px;
}
.link {
color: #2563eb;
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
.error {
margin: 6px 0 0;
color: #dc2626;
font-size: 12px;
}
.saved {
margin-top: 16px;
padding: 12px;
border-radius: 10px;
background: #f0f6ff;
border: 1px solid #93c5fd;
color: #1e3a8a;
font-size: 14px;
}
"use client";
import { FormEvent, useState } from "react";
import styles from "./RegisterPage.module.css";
import Link from "next/link";
import { useUserStore } from "@/features/userStore";
import { axiosApiClient } from "@/helpers/axiosClient";
import { useRouter } from "next/navigation";
export default function Page() {
const router = useRouter();
const { user, setUser } = useUserStore();
const [userName, setUserName] = useState(user?.userName ?? "");
const [displayName, setDisplayName] = useState(user?.displayName ?? "");
const [password, setPassword] = useState("");
const onSubmit = async (e: FormEvent) => {
e.preventDefault();
const newUser = {
userName,
displayName,
password,
};
const { data } = await axiosApiClient.post("/auth/register", newUser);
setUser(data);
router.push("/");
};
return (
<main className={styles.wrapper}>
<form onSubmit={onSubmit} className={styles.card} noValidate>
<h1 className={styles.title}>Регистрация</h1>
<div className={styles.field}>
<label className={styles.label} htmlFor="userName">
Логин
</label>
<input
id="name"
className={styles.input}
value={userName}
onChange={(e) => setUserName(e.target.value)}
placeholder="name"
/>
{!userName && <p className={styles.error}>Введите логин</p>}
</div>
<div className={styles.field}>
<label className={styles.label} htmlFor="displayName">
Отображаемое имя
</label>
<input
id="displayName"
className={styles.input}
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="displayName"
/>
</div>
<div className={styles.field}>
<label className={styles.label} htmlFor="password">
Пароль
</label>
<input
id="password"
type="password"
className={styles.input}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••"
/>
{password && password.length < 6 && <p className={styles.error}>Минимум 6 символов</p>}
</div>
<button className={styles.button}>Зарегистрироваться</button>
<p className={styles.hint}>
Уже есть аккаунт?
<Link href="/login" className={styles.link}>
Войти
</Link>
</p>
</form>
</main>
);
}
import Products from '@/containers/Products/Products'
import Products from "@/containers/Products/Products";
const Page = () => {
return <Products />
}
// return <Products />
return null;
};
export default Page
export default Page;
'use client'
"use client";
import { AppBar, Box, styled, Toolbar, Typography } from '@mui/material'
import Link from 'next/link'
import * as React from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useUserStore } from "@/features/userStore";
import { AppBar, Toolbar, Typography, Box, Button, Avatar, Menu, MenuItem, Divider } from "@mui/material";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import { axiosApiClient } from "@/helpers/axiosClient";
import { useState } from "react";
const StyledLink = styled(Link)(() => ({
color: 'inherit',
textDecoration: 'none',
['&:hover']: { color: 'inherit' },
}))
export default function AppToolbar() {
const router = useRouter();
const { user, clearUser } = useUserStore();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleOpen = (e: React.MouseEvent<HTMLElement>) => setAnchorEl(e.currentTarget);
const handleClose = () => setAnchorEl(null);
const handleProfile = () => {
handleClose();
router.push("/profile");
};
const handleLogout = async () => {
await axiosApiClient.get("/auth/logout");
handleClose();
clearUser();
router.push("/login");
};
const AppToolbar = () => {
return (
<>
<AppBar position={'fixed'}>
<Toolbar>
<Typography variant={'h6'} component={StyledLink} href={'/'}>
<AppBar position="sticky" color="primary" sx={{ bgcolor: "#2563eb" }}>
<Toolbar sx={{ maxWidth: 1120, mx: "auto", width: "100%" }}>
<Typography variant="h6" component={Link} href="/" style={{ textDecoration: "none", color: "inherit" }}>
Computer parts shop
</Typography>
</Toolbar>
</AppBar>
<Box component={Toolbar} marginBottom={2} />
<Box sx={{ flexGrow: 1 }} />
{!user ? (
<Button
component={Link}
href="/login"
color="inherit"
variant="outlined"
sx={{
bgcolor: "white",
color: "#2563eb",
borderColor: "#dbeafe",
"&:hover": { bgcolor: "#e0f2fe" },
}}
>
Login
</Button>
) : (
<>
<Button
color="inherit"
onClick={handleOpen}
aria-haspopup="menu"
aria-controls={open ? "user-menu" : undefined}
aria-expanded={open ? "true" : undefined}
startIcon={
<Avatar
sx={{ width: 28, height: 28 }}
src={`https://api.dicebear.com/8.x/initials/svg?seed=${encodeURIComponent(
user.userName,
)}`}
alt={user.displayName ?? user.userName}
/>
}
endIcon={<KeyboardArrowDownIcon />}
sx={{
textTransform: "none",
fontWeight: 600,
bgcolor: "#1d4ed8",
border: "1px solid #93c5fd",
px: 1.25,
"&:hover": { bgcolor: "#1e40af" },
}}
>
{user.displayName ?? user.userName}
</Button>
<Menu
id="user-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
transformOrigin={{ vertical: "top", horizontal: "right" }}
slotProps={{ paper: { sx: { mt: 1, minWidth: 200 } } }}
>
<MenuItem onClick={handleProfile}>Профиль</MenuItem>
<Divider />
<MenuItem onClick={handleLogout} sx={{ color: "error.main" }}>
Выйти
</MenuItem>
</Menu>
</>
)
)}
</Toolbar>
</AppBar>
);
}
export default AppToolbar
'use client'
"use client";
import { fetchProducts } from '@/features/productsSlice'
import { useAppDispatch, useAppSelector } from '@/store/hooks'
import { Button, Grid2, styled, Typography } from '@mui/material'
import Link from 'next/link'
import { useEffect } from 'react'
import { shallowEqual } from 'react-redux'
import { ProductItem } from './ProductItem'
import { fetchProducts } from "@/features/productsSlice";
import { useAppDispatch, useAppSelector } from "@/store/hooks";
import { Button, Grid2, styled, Typography } from "@mui/material";
import Link from "next/link";
import { useEffect } from "react";
import { shallowEqual } from "react-redux";
import { ProductItem } from "./ProductItem";
const StyledLink = styled(Link)(() => ({
color: 'inherit',
textDecoration: 'none',
['&:hover']: { color: 'inherit' },
}))
color: "inherit",
textDecoration: "none",
["&:hover"]: { color: "inherit" },
}));
const Products = () => {
const dispatch = useAppDispatch()
const { products } = useAppSelector(state => state.products, shallowEqual)
const dispatch = useAppDispatch();
const { products } = useAppSelector((state) => state.products, shallowEqual);
console.log(products);
useEffect(() => {
dispatch(fetchProducts())
}, [dispatch])
dispatch(fetchProducts());
}, [dispatch]);
return (
<Grid2 container direction={'column'} spacing={2}>
<Grid2
container
direction={'row'}
justifyContent={'space-between'}
alignItems={'center'}
>
<Grid2 container direction={"column"} spacing={2}>
<Grid2 container direction={"row"} justifyContent={"space-between"} alignItems={"center"}>
<Grid2>
<Typography variant={'h4'}>Products</Typography>
<Typography variant={"h4"}>Products</Typography>
</Grid2>
<Grid2>
<Button color='primary' component={StyledLink} href={'/newProduct'}>
<Button color="primary" component={StyledLink} href={"/newProduct"}>
Add product
</Button>
</Grid2>
</Grid2>
<Grid2 container direction={'row'} spacing={1}>
{products?.map(item => (
<Grid2 container direction={"row"} spacing={1}>
{products?.map((item) => (
<ProductItem key={item.id} product={item} />
))}
</Grid2>
</Grid2>
)
}
);
};
export default Products
export default Products;
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
export type TUser = {
userName: string;
displayName?: string;
token?: string;
};
type TSlice = {
user?: TUser | null;
setUser: (u: TUser) => void;
clearUser: () => void;
};
export const useUserStore = create<TSlice>()(
persist(
(set) => ({
user: null,
setUser: (u) => set({ user: u }),
clearUser: () => set({ user: null }),
}),
{
name: "user-store",
storage: createJSONStorage(() => localStorage),
},
),
);
import axios from 'axios'
import axios from "axios";
export const axiosApiClient = axios.create({
baseURL: process.env.SERVER_URL,
})
baseURL: "http://localhost:8000",
});
......@@ -30,6 +30,7 @@
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"lodash": "^4.17.21",
"multer": "^2.0.2",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
......@@ -47,6 +48,7 @@
"@types/bcrypt": "^6.0.0",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.20",
"@types/multer": "^2.0.0",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
......
......@@ -38,6 +38,9 @@ importers:
class-validator:
specifier: ^0.14.2
version: 0.14.2
lodash:
specifier: ^4.17.21
version: 4.17.21
multer:
specifier: ^2.0.2
version: 2.0.2
......@@ -84,6 +87,9 @@ importers:
'@types/jest':
specifier: ^29.5.14
version: 29.5.14
'@types/lodash':
specifier: ^4.17.20
version: 4.17.20
'@types/multer':
specifier: ^2.0.0
version: 2.0.0
......@@ -1071,6 +1077,9 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/lodash@4.17.20':
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
'@types/methods@1.1.4':
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
......@@ -4708,6 +4717,8 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/lodash@4.17.20': {}
'@types/methods@1.1.4': {}
'@types/mime@1.3.5': {}
......
......@@ -21,4 +21,9 @@ export class AuthController {
secret(@Headers('Authorization') token: string) {
return this.authService.secret(token);
}
@Get('logout')
async logout(@Headers('Authorization') authToken: string) {
await this.authService.logout(authToken);
}
}
......@@ -5,6 +5,7 @@ import { User } from 'src/user/entities/user.entity';
import { Repository } from 'typeorm';
import { LoginAuthDto } from './dto/login.dto';
import { RegisterAuthDto } from './dto/register.dto';
import { omit } from 'lodash';
const SALT_WORK_FACTOR = 10;
......@@ -27,7 +28,10 @@ export class AuthService {
user.generateToken();
return await this.userRepo.save(user);
const userWithToken = await this.userRepo.save(user);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
return omit(userWithToken, 'password');
}
async login(loginAuthDto: LoginAuthDto) {
......@@ -40,11 +44,21 @@ export class AuthService {
const isMatch = await user.comparePassword(loginAuthDto.password);
if (!isMatch) throw new Error('Invalid userName or password');
return user;
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
return omit(user, ['password']);
}
async secret(token: string) {
const user = await this.userRepo.findOneBy({ token });
return user;
}
async logout(token: string) {
const existUser = await this.userRepo.findOneBy({ token });
if (!existUser) throw new NotFoundException('User not found');
existUser.token = null;
await this.userRepo.update({ token }, existUser);
}
}
......@@ -15,8 +15,8 @@ export class User {
@Column()
password: string;
@Column({ nullable: true })
token: string;
@Column({ type: 'varchar', nullable: true })
token: string | null;
async comparePassword(password: string): Promise<boolean> {
return await bcrypt.compare(password, this.password);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment