add tanstack/react-query

This commit is contained in:
smanylov
2025-12-02 17:11:53 +07:00
parent d89cc35fca
commit 6d47fa7fbb
15 changed files with 209 additions and 94 deletions
+27
View File
@@ -9,6 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@tailwindcss/postcss": "^4.1.17", "@tailwindcss/postcss": "^4.1.17",
"@tanstack/react-query": "^5.90.11",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"jose": "^6.1.2", "jose": "^6.1.2",
"next": "16.0.3", "next": "16.0.3",
@@ -1884,6 +1885,32 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/@tanstack/query-core": {
"version": "5.90.11",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.11.tgz",
"integrity": "sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.90.11",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.11.tgz",
"integrity": "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.90.11"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18 || ^19"
}
},
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.1", "version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+2 -1
View File
@@ -5,11 +5,12 @@
"scripts": { "scripts": {
"dev": "next dev -p 2999", "dev": "next dev -p 2999",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start -p 2999",
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/postcss": "^4.1.17", "@tailwindcss/postcss": "^4.1.17",
"@tanstack/react-query": "^5.90.11",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"jose": "^6.1.2", "jose": "^6.1.2",
"next": "16.0.3", "next": "16.0.3",
+7 -7
View File
@@ -1,14 +1,14 @@
'use server' 'use server'
import { SignupFormSchema, loginFormSchema } from '@/app/lib/definitions'; import { SignupFormSchema, loginFormSchema } from '@/app/lib/definitions';
import { createSession, deleteSession, getSessionToken } from '@/app/lib/session'; import { createSession, deleteSession, getSessionData } from '@/app/lib/session';
import { redirect } from 'next/navigation' import { redirect } from 'next/navigation'
export async function logout() { export async function logout() {
const token = await getSessionToken(); const token = await getSessionData('token');
try { try {
await fetch('http://localhost:8080/api/auth/logout', { await fetch('http://localhost/api/auth/logout', {
method: 'POST', method: 'POST',
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -39,7 +39,7 @@ export async function authorization(
try { try {
const { email, password } = validatedFields.data; const { email, password } = validatedFields.data;
const response = await fetch('http://localhost:8080/api/auth/login', { const response = await fetch('http://localhost/api/auth/login', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
email: email, email: email,
@@ -52,7 +52,7 @@ export async function authorization(
}); });
if (response.ok) { if (response.ok) {
let parsed = await response.json(); let parsed = await response.json();
await createSession(parsed.token); await createSession(parsed.token, parsed.email);
} else { } else {
throw new Error('Something went wrong. from server'); throw new Error('Something went wrong. from server');
} }
@@ -96,7 +96,7 @@ export async function registration(
try { try {
const { full_name, email, password } = validatedFields.data; const { full_name, email, password } = validatedFields.data;
const response = await fetch('http://localhost:8080/api/auth/register', { const response = await fetch('http://localhost/api/auth/register', {
method: 'POST', method: 'POST',
body: JSON.stringify({ fullName: full_name, email, password }), body: JSON.stringify({ fullName: full_name, email, password }),
headers: { headers: {
@@ -107,7 +107,7 @@ export async function registration(
if (response.ok) { if (response.ok) {
let parsed = await response.json(); let parsed = await response.json();
await createSession(parsed.token); await createSession(parsed.token, email);
} else { } else {
throw new Error('Something went wrong. from server'); throw new Error('Something went wrong. from server');
} }
-43
View File
@@ -1,43 +0,0 @@
'use client'
import { createContext, useContext, ReactNode } from 'react'
interface Data {
id: number
name: string
family: string
}
interface DataContextType {
dataContext: Data | null
isLoading: boolean
error: string | null
}
const DataContext = createContext<DataContextType | undefined>(undefined)
export function DataProvider({
children,
dataContext
}: {
children: ReactNode
dataContext: any
}) {
return (
<DataContext.Provider value={{
dataContext,
isLoading: false,
error: null
}}>
{children}
</DataContext.Provider>
)
}
export function useDataContext() {
const context = useContext(DataContext)
if (context === undefined) {
throw new Error('useDataContext must be used within a DataProvider')
}
return context
}
+4 -1
View File
@@ -1,3 +1,4 @@
import Providers from './providers/providers'
import type { Metadata } from "next"; import type { Metadata } from "next";
import "./styles/globals.css"; import "./styles/globals.css";
import "./styles/global-styles.scss" import "./styles/global-styles.scss"
@@ -15,7 +16,9 @@ export default function RootLayout({
return ( return (
<html lang="ru"> <html lang="ru">
<body className={``}> <body className={``}>
{children} <Providers>
{children}
</Providers>
</body> </body>
</html> </html>
); );
+13 -14
View File
@@ -1,4 +1,5 @@
'use server' 'use server'
import { getSessionData } from '@/app/lib/session';
const FRUITS_URL = 'https://www.fruityvice.com/api/fruit/'; const FRUITS_URL = 'https://www.fruityvice.com/api/fruit/';
const ALL = 'all' const ALL = 'all'
@@ -25,26 +26,24 @@ export async function fetchFruit(id: number) {
} }
} }
export async function testConnect() { export async function getUserData() {
const userEmail = await getSessionData('email');
const token = await getSessionData('token');
try { try {
const response = await fetch('http://localhost:8080/api/auth/register', { const response = await fetch(`http://localhost/api/user?email=${userEmail}`, {
method: 'POST', method: 'GET',
body: JSON.stringify({
"fullName": "Vladislav Sergevich",
"email": "s2@e.ru",
"password": "1121231412"
}),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept": "application/json" "Accept": "application/json",
"Authorization": `Bearer ${token}`,
} }
}); });
let parsed = await response.json(); if (response.ok) {
return await response.json();
return parsed; }
} catch (error) { } catch (error) {
throw error; console.error('error');
} }
} }
+9 -6
View File
@@ -25,9 +25,13 @@ export async function decrypt(session: string | undefined = '') {
} }
} }
export async function createSession(token: string) { export async function createSession(token: string, email: string) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
const session = await encrypt({ token, expiresAt }); const session = await encrypt({
token,
email,
expiresAt
});
const cookieStore = await cookies(); const cookieStore = await cookies();
cookieStore.set('session', session, { cookieStore.set('session', session, {
@@ -39,7 +43,7 @@ export async function createSession(token: string) {
}) })
} }
export async function getSessionToken(): Promise<string | null> { export async function getSessionData(type: 'token' | 'email'): Promise<string | null> {
const cookieStore = await cookies(); const cookieStore = await cookies();
const sessionCookie = cookieStore.get('session')?.value; const sessionCookie = cookieStore.get('session')?.value;
@@ -49,15 +53,14 @@ export async function getSessionToken(): Promise<string | null> {
const payload = await decrypt(sessionCookie); const payload = await decrypt(sessionCookie);
if (payload && typeof payload === 'object' && 'token' in payload) { if (payload && typeof payload === 'object' && type in payload) {
return payload.token as string; return payload[type] as string;
} }
return null; return null;
} }
export async function deleteSession() { export async function deleteSession() {
const cookieStore = await cookies() const cookieStore = await cookies()
cookieStore.delete('session') cookieStore.delete('session')
+1 -1
View File
@@ -11,7 +11,7 @@ export default function Home() {
<div className={styles.page}> <div className={styles.page}>
<main className={styles.main}> <main className={styles.main}>
<div className={styles.intro}> <div className={styles.intro}>
Home page11 Home page
</div> </div>
<Link key={'dashboard'} <Link key={'dashboard'}
href={'/pages/dashboard'} href={'/pages/dashboard'}
+4
View File
@@ -1,4 +1,5 @@
import { Metadata } from 'next'; import { Metadata } from 'next';
import Test from '@/app/ui/test';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'emptypage', title: 'emptypage',
@@ -11,6 +12,9 @@ export default function Home() {
<div> <div>
emptypage emptypage
</div> </div>
<div>
<Test />
</div>
</main> </main>
</div> </div>
); );
+14 -9
View File
@@ -1,18 +1,23 @@
import NavLinks from '@/app/ui/nav-links' import NavLinks from '@/app/ui/nav-links'
import styles from '@/app/page.module.scss' import styles from '@/app/page.module.scss'
import HeaderPanel from '../ui/header/headerPanel'; import HeaderPanel from '../ui/header/headerPanel';
import { DataProvider } from '@/app/context/data-context'; import { getQueryClient } from '@/app/providers/getQueryClient';
import { fetchFruit } from '@/app/lib/action'; import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { getSessionToken } from '@/app/lib/session'; import { getUserData } from '@/app/lib/action';
export default async function Layout({ children }: { children: React.ReactNode }) { export default async function Layout({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();
const fruitsData = await fetchFruit(100); await Promise.all([
const token = await getSessionToken(); queryClient.prefetchQuery({
queryKey: ['userData'],
queryFn: () => getUserData()
})
])
return ( return (
<DataProvider dataContext={fruitsData}> <div className="flex">
<div className="flex"> <HydrationBoundary state={dehydrate(queryClient)}>
<NavLinks /> <NavLinks />
<div className={`${styles.mainContainter}`}> <div className={`${styles.mainContainter}`}>
<HeaderPanel /> <HeaderPanel />
@@ -20,7 +25,7 @@ export default async function Layout({ children }: { children: React.ReactNode }
{children} {children}
</main> </main>
</div> </div>
</div> </HydrationBoundary>
</DataProvider> </div>
); );
} }
+31
View File
@@ -0,0 +1,31 @@
import { QueryClient } from '@tanstack/react-query'
import { cache } from 'react'
const getQueryClientDefaultConfig = () => ({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
refetchOnWindowFocus: false,
retry: 1,
},
},
})
export function makeQueryClient() {
return new QueryClient(getQueryClientDefaultConfig())
}
export const getQueryClient = cache(() => {
if (typeof window === 'undefined') {
return new QueryClient(getQueryClientDefaultConfig())
} else {
// @ts-ignore
if (!globalThis.__APP_QUERY_CLIENT__) {
// @ts-ignore
globalThis.__APP_QUERY_CLIENT__ = makeQueryClient()
}
// @ts-ignore
return globalThis.__APP_QUERY_CLIENT__
}
})
+36
View File
@@ -0,0 +1,36 @@
'use client'
import {
isServer,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
export function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
})
}
let browserQueryClient: QueryClient | undefined = undefined
function getQueryClient() {
if (isServer) {
return makeQueryClient()
} else {
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}
export default function Providers({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
+1 -3
View File
@@ -1,9 +1,7 @@
'use client' 'use client'
import { testConnect } from '@/app/lib/action';
async function clickHandler() { async function clickHandler() {
testConnect(); console.log('click')
} }
export default function ButtonTest() { export default function ButtonTest() {
+30 -9
View File
@@ -3,12 +3,23 @@
import { useState, useRef } from 'react'; import { useState, useRef } from 'react';
import { useClickOutside } from '@/app/hooks/useClickOutside'; import { useClickOutside } from '@/app/hooks/useClickOutside';
import Link from 'next/link'; import Link from 'next/link';
import { useDataContext } from '@/app/context/data-context'; import { getUserData } from '@/app/lib/action';
import { useQuery } from '@tanstack/react-query';
export default function UserMenuButton() { export default function UserMenuButton() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement | null>(null); const menuRef = useRef<HTMLDivElement | null>(null);
const { dataContext } = useDataContext() const {
data: userData,
isLoading,
isError,
error
} = useQuery({
queryKey: ['userData'],
queryFn: () => {
return getUserData();
}
})
useClickOutside(menuRef, () => { useClickOutside(menuRef, () => {
@@ -17,7 +28,6 @@ export default function UserMenuButton() {
const handleButtonClick = () => { const handleButtonClick = () => {
if (!isOpen) { if (!isOpen) {
console.log(dataContext);
setIsOpen(true); setIsOpen(true);
} else { } else {
setIsOpen(false); setIsOpen(false);
@@ -30,18 +40,29 @@ export default function UserMenuButton() {
onClick={handleButtonClick} onClick={handleButtonClick}
ref={menuRef} ref={menuRef}
> >
<div className="user-avatar" id="user-menu-btn" title="Валерка"> <div
User className="user-avatar"
id="user-menu-btn"
title={userData?.fullName ? userData.fullName : ''}
>
{userData?.fullName ? userData.fullName : ''}
</div> </div>
<div className={`user-dropdown ${isOpen ? 'show' : ''}`} id="user-dropdown"> <div className={`user-dropdown ${isOpen ? 'show' : ''}`} id="user-dropdown">
<div className="user-info-header"> <div className="user-info-header">
<div className="user-avatar-large">User</div> <div className="user-avatar-large">
{userData?.fullName ? userData.fullName : ''}
</div>
<div className="user-details"> <div className="user-details">
<div className="user-name">Валерка</div> <div className="user-name">
<div className="user-email">va@no-copy.ru</div> {userData?.fullName ? userData.fullName : ''}
</div>
<div className="user-email">
{userData?.email ? userData.email : ''}
</div>
<div className="user-subscription"> <div className="user-subscription">
<span className="subscription-badge"> <span className="subscription-badge">
СТАРТ</span> {userData?.subscriptionType ? userData.subscriptionType : ''}
</span>
</div> </div>
</div> </div>
</div> </div>
+30
View File
@@ -0,0 +1,30 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import { getUserData } from '@/app/lib/action';
export default function HomePage() {
const {
data: userData,
isLoading,
isError,
error,
} = useQuery({
queryKey: ['userData'],
queryFn: () => {
return getUserData();
},
});
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error.message}</div>;
return (
<div>
<h1>user email</h1>
<span>
{userData.email}
</span>
</div>
);
}