Merge branch 'bruno' of https://git.vontor.cz/Brunobrno/vontor-cz into bruno
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-31 07:36
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customuser',
|
||||||
|
name='email_verification_sent_at',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customuser',
|
||||||
|
name='email_verification_token',
|
||||||
|
field=models.CharField(blank=True, db_index=True, max_length=128, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -121,4 +121,32 @@ def send_email_test_task(email):
|
|||||||
template_name="email/test.txt",
|
template_name="email/test.txt",
|
||||||
html_template_name="email/test.html",
|
html_template_name="email/test.html",
|
||||||
context=context,
|
context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def send_password_reset_email_task(user_id):
|
||||||
|
try:
|
||||||
|
user = CustomUser.objects.get(pk=user_id)
|
||||||
|
except CustomUser.DoesNotExist:
|
||||||
|
logger.info(f"Task send_password_reset_email has failed. Invalid User ID was sent.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
uid = urlsafe_base64_encode(force_bytes(user.pk))
|
||||||
|
token = password_reset_token.make_token(user)
|
||||||
|
reset_url = f"{settings.FRONTEND_URL}/reset-password/{uid}/{token}"
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"user": _build_user_template_ctx(user),
|
||||||
|
"action_url": reset_url,
|
||||||
|
"frontend_url": settings.FRONTEND_URL,
|
||||||
|
"cta_label": "Obnovit heslo",
|
||||||
|
}
|
||||||
|
|
||||||
|
send_email_with_context(
|
||||||
|
recipients=user.email,
|
||||||
|
subject="Obnova hesla",
|
||||||
|
template_name="email/password_reset.txt",
|
||||||
|
html_template_name="email/password_reset.html",
|
||||||
|
context=context,
|
||||||
)
|
)
|
||||||
@@ -1,3 +1,28 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
|
class UserViewAnonymousTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
User = get_user_model()
|
||||||
|
self.target_user = User.objects.create_user(
|
||||||
|
username="target",
|
||||||
|
email="target@example.com",
|
||||||
|
password="pass1234",
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_anonymous_update_user_is_forbidden_and_does_not_crash(self):
|
||||||
|
url = f"/api/account/users/{self.target_user.id}/"
|
||||||
|
payload = {"username": "newname", "email": self.target_user.email}
|
||||||
|
resp = self.client.put(url, data=payload, format="json")
|
||||||
|
# Expect 403 Forbidden (permission denied), but most importantly no 500 error
|
||||||
|
self.assertEqual(resp.status_code, 403, msg=f"Unexpected status: {resp.status_code}, body={getattr(resp, 'data', resp.content)}")
|
||||||
|
|
||||||
|
def test_anonymous_retrieve_user_is_unauthorized(self):
|
||||||
|
url = f"/api/account/users/{self.target_user.id}/"
|
||||||
|
resp = self.client.get(url)
|
||||||
|
# Retrieve requires authentication per view; expect 401 Unauthorized
|
||||||
|
self.assertEqual(resp.status_code, 401, msg=f"Unexpected status: {resp.status_code}, body={getattr(resp, 'data', resp.content)}")
|
||||||
|
|||||||
@@ -229,13 +229,17 @@ class UserView(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
# Only admin or the user themselves can update or delete
|
# Only admin or the user themselves can update or delete
|
||||||
elif self.action in ['update', 'partial_update', 'destroy']:
|
elif self.action in ['update', 'partial_update', 'destroy']:
|
||||||
if self.request.user.role == 'admin':
|
user = getattr(self, 'request', None) and getattr(self.request, 'user', None)
|
||||||
|
# Admins can modify any user
|
||||||
|
if user and getattr(user, 'is_authenticated', False) and getattr(user, 'role', None) == 'admin':
|
||||||
return [OnlyRolesAllowed("admin")()]
|
return [OnlyRolesAllowed("admin")()]
|
||||||
elif self.kwargs.get('pk') and str(self.request.user.id) == self.kwargs['pk']:
|
|
||||||
|
# Users can modify their own record
|
||||||
|
if user and getattr(user, 'is_authenticated', False) and self.kwargs.get('pk') and str(getattr(user, 'id', '')) == self.kwargs['pk']:
|
||||||
return [IsAuthenticated()]
|
return [IsAuthenticated()]
|
||||||
else:
|
|
||||||
# fallback - deny access
|
# Fallback - deny access (prevents AttributeError for AnonymousUser)
|
||||||
return [OnlyRolesAllowed("admin")()]
|
return [OnlyRolesAllowed("admin")()]
|
||||||
|
|
||||||
# Any authenticated user can retrieve (view) any user's profile
|
# Any authenticated user can retrieve (view) any user's profile
|
||||||
elif self.action == 'retrieve':
|
elif self.action == 'retrieve':
|
||||||
|
|||||||
0
backend/social/chat/__init__.py
Normal file
0
backend/social/chat/__init__.py
Normal file
3
backend/social/chat/admin.py
Normal file
3
backend/social/chat/admin.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
8
backend/social/chat/apps.py
Normal file
8
backend/social/chat/apps.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ChatConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'social.chat'
|
||||||
|
|
||||||
|
label = "chat"
|
||||||
27
backend/social/chat/consumers.py
Normal file
27
backend/social/chat/consumers.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# chat/consumers.py
|
||||||
|
import json
|
||||||
|
|
||||||
|
from account.models import UserProfile
|
||||||
|
|
||||||
|
from channels.db import database_sync_to_async
|
||||||
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
|
|
||||||
|
|
||||||
|
class ChatConsumer(AsyncWebsocketConsumer):
|
||||||
|
async def connect(self):
|
||||||
|
await self.accept()
|
||||||
|
|
||||||
|
async def disconnect(self, close_code):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def receive(self, text_data):
|
||||||
|
text_data_json = json.loads(text_data)
|
||||||
|
message = text_data_json["message"]
|
||||||
|
|
||||||
|
await self.send(text_data=json.dumps({"message": message}))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@database_sync_to_async
|
||||||
|
def get_user_profile(user_id):
|
||||||
|
return UserProfile.objects.get(pk=user_id)
|
||||||
0
backend/social/chat/migrations/__init__.py
Normal file
0
backend/social/chat/migrations/__init__.py
Normal file
3
backend/social/chat/models.py
Normal file
3
backend/social/chat/models.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
8
backend/social/chat/routing.py
Normal file
8
backend/social/chat/routing.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# chat/routing.py
|
||||||
|
from django.urls import re_path
|
||||||
|
|
||||||
|
from . import consumers
|
||||||
|
|
||||||
|
websocket_urlpatterns = [
|
||||||
|
re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
|
||||||
|
]
|
||||||
3
backend/social/chat/tests.py
Normal file
3
backend/social/chat/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
3
backend/social/chat/views.py
Normal file
3
backend/social/chat/views.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
@@ -326,6 +326,8 @@ REST_FRAMEWORK = {
|
|||||||
MY_CREATED_APPS = [
|
MY_CREATED_APPS = [
|
||||||
'account',
|
'account',
|
||||||
'commerce',
|
'commerce',
|
||||||
|
|
||||||
|
'social.chat',
|
||||||
|
|
||||||
'thirdparty.downloader',
|
'thirdparty.downloader',
|
||||||
'thirdparty.stripe', # register Stripe app so its models are recognized
|
'thirdparty.stripe', # register Stripe app so its models are recognized
|
||||||
|
|||||||
@@ -3,17 +3,31 @@ import Home from "./pages/home/home";
|
|||||||
import HomeLayout from "./layouts/HomeLayout";
|
import HomeLayout from "./layouts/HomeLayout";
|
||||||
import Downloader from "./pages/downloader/Downloader";
|
import Downloader from "./pages/downloader/Downloader";
|
||||||
|
|
||||||
|
import PrivateRoute from "./routes/PrivateRoute";
|
||||||
|
|
||||||
|
import { UserContextProvider } from "./context/UserContext";
|
||||||
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<UserContextProvider>
|
||||||
|
|
||||||
{/* Layout route */}
|
{/* Layout route */}
|
||||||
<Route path="/" element={<HomeLayout />}>
|
<Route path="/" element={<HomeLayout />}>
|
||||||
<Route index element={<Home />} />
|
<Route index element={<Home />} />
|
||||||
<Route path="downloader" element={<Downloader />} />
|
<Route path="downloader" element={<Downloader />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
|
||||||
|
<Route element={<PrivateRoute />}>
|
||||||
|
{/* Protected routes go here */}
|
||||||
|
<Route path="/" element={<HomeLayout />} >
|
||||||
|
<Route path="protected-downloader" element={<Downloader />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
</UserContextProvider>
|
||||||
</Router>
|
</Router>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,7 @@ import axios from "axios";
|
|||||||
// --- ENV CONFIG ---
|
// --- ENV CONFIG ---
|
||||||
const API_BASE_URL =
|
const API_BASE_URL =
|
||||||
import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
|
import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
|
||||||
const REFRESH_URL =
|
|
||||||
import.meta.env.VITE_API_REFRESH_URL || "/api/token/refresh/";
|
|
||||||
const LOGIN_PATH = import.meta.env.VITE_LOGIN_PATH || "/login";
|
const LOGIN_PATH = import.meta.env.VITE_LOGIN_PATH || "/login";
|
||||||
|
|
||||||
|
|
||||||
@@ -30,6 +29,7 @@ function notifyError(detail: ApiErrorDetail) {
|
|||||||
function onError(handler: ApiErrorHandler) {
|
function onError(handler: ApiErrorHandler) {
|
||||||
const wrapped = handler as EventListener;
|
const wrapped = handler as EventListener;
|
||||||
window.addEventListener(ERROR_EVENT, wrapped as EventListener);
|
window.addEventListener(ERROR_EVENT, wrapped as EventListener);
|
||||||
|
|
||||||
return () => window.removeEventListener(ERROR_EVENT, wrapped);
|
return () => window.removeEventListener(ERROR_EVENT, wrapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ function onError(handler: ApiErrorHandler) {
|
|||||||
function createAxios(baseURL: string): any {
|
function createAxios(baseURL: string): any {
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
baseURL,
|
baseURL,
|
||||||
withCredentials: true, // <-- always true
|
withCredentials: true, // cookies
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
@@ -51,12 +51,14 @@ function createAxios(baseURL: string): any {
|
|||||||
const apiPublic = createAxios(API_BASE_URL);
|
const apiPublic = createAxios(API_BASE_URL);
|
||||||
const apiAuth = createAxios(API_BASE_URL);
|
const apiAuth = createAxios(API_BASE_URL);
|
||||||
|
|
||||||
|
|
||||||
// --- REQUEST INTERCEPTOR (PUBLIC) ---
|
// --- REQUEST INTERCEPTOR (PUBLIC) ---
|
||||||
// Ensure no Authorization header is ever sent by the public client
|
// Ensure no Authorization header is ever sent by the public client
|
||||||
apiPublic.interceptors.request.use(function (config: any) {
|
apiPublic.interceptors.request.use(function (config: any) {
|
||||||
if (config?.headers && (config.headers as any).Authorization) {
|
if (config?.headers && (config.headers as any).Authorization) {
|
||||||
delete (config.headers as any).Authorization;
|
delete (config.headers as any).Authorization;
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,6 +66,7 @@ apiPublic.interceptors.request.use(function (config: any) {
|
|||||||
// Do not attach Authorization header; rely on cookies set by Django.
|
// Do not attach Authorization header; rely on cookies set by Django.
|
||||||
apiAuth.interceptors.request.use(function (config: any) {
|
apiAuth.interceptors.request.use(function (config: any) {
|
||||||
(config as any)._retryCount = (config as any)._retryCount || 0;
|
(config as any)._retryCount = (config as any)._retryCount || 0;
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,16 +79,18 @@ apiAuth.interceptors.response.use(
|
|||||||
async function (error: any) {
|
async function (error: any) {
|
||||||
if (!error.response) {
|
if (!error.response) {
|
||||||
alert("Backend connection is unavailable. Please check your network.");
|
alert("Backend connection is unavailable. Please check your network.");
|
||||||
|
|
||||||
notifyError({
|
notifyError({
|
||||||
message: "Network error or backend unavailable",
|
message: "Network error or backend unavailable",
|
||||||
url: error.config?.url,
|
url: error.config?.url,
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = error.response.status;
|
const status = error.response.status;
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
clearTokens(); // optional: clear cookies client-side
|
ClearTokens();
|
||||||
window.location.assign(LOGIN_PATH);
|
window.location.assign(LOGIN_PATH);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
@@ -130,35 +135,37 @@ apiPublic.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- TOKEN HELPERS (NO-OPS) ---
|
|
||||||
// Django sets/rotates cookies server-side. Keep API surface to avoid breaking imports.
|
|
||||||
function setTokens(_access?: string, _refresh?: string) {
|
|
||||||
// no-op: cookies are managed by Django
|
function Logout() {
|
||||||
}
|
|
||||||
function clearTokens() {
|
|
||||||
// optional: try to clear auth cookies client-side; server should also clear on logout
|
|
||||||
try {
|
try {
|
||||||
document.cookie = "access_token=; Max-Age=0; path=/";
|
const LogOutResponse = apiAuth.post("/api/logout/");
|
||||||
document.cookie = "refresh_token=; Max-Age=0; path=/";
|
|
||||||
} catch {
|
if (LogOutResponse.body.detail != "Logout successful") {
|
||||||
// ignore
|
throw new Error("Logout failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearTokens();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during logout:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function getAccessToken(): string | null {
|
|
||||||
// no Authorization header is used; rely purely on cookies
|
function ClearTokens(){
|
||||||
return null;
|
document.cookie = "access_token=; Max-Age=0; path=/";
|
||||||
|
document.cookie = "refresh_token=; Max-Age=0; path=/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- EXPORT DEFAULT API WRAPPER ---
|
// --- EXPORT DEFAULT API WRAPPER ---
|
||||||
const Client = {
|
const Client = {
|
||||||
// Axios instances
|
// Axios instances
|
||||||
auth: apiAuth,
|
auth: apiAuth,
|
||||||
public: apiPublic,
|
public: apiPublic,
|
||||||
|
|
||||||
// Token helpers (kept for compatibility; now no-ops)
|
Logout,
|
||||||
setTokens,
|
|
||||||
clearTokens,
|
|
||||||
getAccessToken,
|
|
||||||
|
|
||||||
// Error subscription
|
// Error subscription
|
||||||
onError,
|
onError,
|
||||||
|
|||||||
82
frontend/src/api/models/User.ts
Normal file
82
frontend/src/api/models/User.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// frontend/src/api/model/user.js
|
||||||
|
// User API model for searching users by username
|
||||||
|
// Structure matches other model files (see order.js for reference)
|
||||||
|
|
||||||
|
import Client from '../Client';
|
||||||
|
|
||||||
|
const API_BASE_URL = "/account/users";
|
||||||
|
|
||||||
|
const userAPI = {
|
||||||
|
/**
|
||||||
|
* Get current authenticated user
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
async getCurrentUser() {
|
||||||
|
const response = await Client.auth.get(`${API_BASE_URL}/me/`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all users
|
||||||
|
* @returns {Promise<Array<User>>}
|
||||||
|
*/
|
||||||
|
async getUsers(params: Object) {
|
||||||
|
const response = await Client.auth.get(`${API_BASE_URL}/`, { params });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single user by ID
|
||||||
|
* @param {number|string} id
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
async getUser(id: number) {
|
||||||
|
const response = await Client.auth.get(`${API_BASE_URL}/${id}/`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a user by ID
|
||||||
|
* @param {number|string} id
|
||||||
|
* @param {Object} data
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
async updateUser(id: number, data: Object) {
|
||||||
|
const response = await Client.auth.patch(`${API_BASE_URL}/${id}/`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a user by ID
|
||||||
|
* @param {number|string} id
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async deleteUser(id: number) {
|
||||||
|
const response = await Client.auth.delete(`${API_BASE_URL}/${id}/`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user
|
||||||
|
* @param {Object} data
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
async createUser(data: Object) {
|
||||||
|
const response = await Client.auth.post(`${API_BASE_URL}/`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search users by username (partial match)
|
||||||
|
* @param {Object} params - { username: string }
|
||||||
|
* @returns {Promise<Array<User>>}
|
||||||
|
*/
|
||||||
|
async searchUsers(params: { username: string }) {
|
||||||
|
// Adjust the endpoint as needed for your backend
|
||||||
|
const response = await Client.auth.get(`${API_BASE_URL}/`, { params });
|
||||||
|
console.log("User search response:", response.data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default userAPI;
|
||||||
19
frontend/src/api/websockets/WebSocketClient.ts
Normal file
19
frontend/src/api/websockets/WebSocketClient.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const wsUri = "ws://127.0.0.1/";
|
||||||
|
|
||||||
|
const websocket = new WebSocket(wsUri);
|
||||||
|
|
||||||
|
websocket.onopen = function (event) {
|
||||||
|
console.log("WebSocket is open now.", event);
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.onmessage = function (event) {
|
||||||
|
console.log("WebSocket message received:", event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.onclose = function (event) {
|
||||||
|
console.log("WebSocket is closed now.", event.reason);
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.onerror = function (event) {
|
||||||
|
console.error("WebSocket error observed:", event);
|
||||||
|
};
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import React, { useState } from "react"
|
import React, { useState, useContext } from "react"
|
||||||
import styles from "./HomeNav.module.css"
|
import styles from "./HomeNav.module.css"
|
||||||
import { FaBars, FaChevronDown } from "react-icons/fa";
|
import { FaBars, FaChevronDown } from "react-icons/fa";
|
||||||
|
|
||||||
|
import { UserContext } from "../../context/UserContext";
|
||||||
|
|
||||||
export default function HomeNav() {
|
export default function HomeNav() {
|
||||||
const [navOpen, setNavOpen] = useState(false)
|
const [navOpen, setNavOpen] = useState(false)
|
||||||
|
|
||||||
const toggleNav = () => setNavOpen((prev) => !prev)
|
const toggleNav = () => setNavOpen((prev) => !prev)
|
||||||
|
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={styles.nav}>
|
<nav className={styles.nav}>
|
||||||
<FaBars
|
<FaBars
|
||||||
|
|||||||
30
frontend/src/context/Context.md
Normal file
30
frontend/src/context/Context.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
# EXAMPLE USAGE OF CONTEXT IN A COMPONENT:
|
||||||
|
|
||||||
|
## Wrap your app tree with the provider (e.g., in App.tsx)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { UserContextProvider } from "../context/UserContext";
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<UserContextProvider>
|
||||||
|
<YourRoutes />
|
||||||
|
</UserContextProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Consume in any child component
|
||||||
|
```tsx
|
||||||
|
import React, { useContext } from "react"
|
||||||
|
import { UserContext } from '../context/UserContext';
|
||||||
|
|
||||||
|
export default function ExampleComponent() {
|
||||||
|
const { user, setUser } = useContext(UserContext);
|
||||||
|
|
||||||
|
|
||||||
|
return ...;
|
||||||
|
}
|
||||||
|
```
|
||||||
0
frontend/src/context/SettingsContext.tsx
Normal file
0
frontend/src/context/SettingsContext.tsx
Normal file
74
frontend/src/context/UserContext.tsx
Normal file
74
frontend/src/context/UserContext.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import React, { createContext, useState, useContext, useEffect } from 'react';
|
||||||
|
|
||||||
|
import userAPI from '../api/models/User';
|
||||||
|
|
||||||
|
// definice uživatele
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// určíme typ kontextu
|
||||||
|
interface GlobalContextType {
|
||||||
|
user: User | null;
|
||||||
|
setUser: React.Dispatch<React.SetStateAction<User | null>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// vytvoříme a exportneme kontext
|
||||||
|
export const UserContext = createContext<GlobalContextType | null>(null);
|
||||||
|
|
||||||
|
|
||||||
|
// hook pro použití kontextu
|
||||||
|
export const UserContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
const fetchUser = async () => {
|
||||||
|
try {
|
||||||
|
const currentUser = await userAPI.getCurrentUser();
|
||||||
|
setUser(currentUser);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load user:', error);
|
||||||
|
setUser(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUser();
|
||||||
|
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserContext.Provider value={{ user, setUser }}>
|
||||||
|
{children}
|
||||||
|
</UserContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
EXAMPLE USAGE OF CONTEXT IN A COMPONENT:
|
||||||
|
|
||||||
|
// Wrap your app tree with the provider (e.g., in App.tsx)
|
||||||
|
// import { UserContextProvider } from "../context/UserContext";
|
||||||
|
// function App() {
|
||||||
|
// return (
|
||||||
|
// <UserContextProvider>
|
||||||
|
// <YourRoutes />
|
||||||
|
// </UserContextProvider>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Consume in any child component
|
||||||
|
import React, { useContext } from "react"
|
||||||
|
import { UserContext } from '../context/UserContext';
|
||||||
|
|
||||||
|
export default function ExampleComponent() {
|
||||||
|
const { user, setUser } = useContext(UserContext);
|
||||||
|
|
||||||
|
|
||||||
|
return ...;
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
Reference in New Issue
Block a user