diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ed9ca45..a8832f4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,7 +3,7 @@ import Home from "./pages/home/home"; import HomeLayout from "./layouts/HomeLayout"; import Downloader from "./pages/downloader/Downloader"; import PrivateRoute from "./routes/PrivateRoute"; -import { UserContextProvider } from "./context/UserContext"; +//import { UserContextProvider } from "./context/UserContext"; // Pages import PortfolioPage from "./pages/portfolio/PortfolioPage"; @@ -14,7 +14,7 @@ import ScrollToTop from "./components/common/ScrollToTop"; export default function App() { return ( - + {/* */} {/* Public routes */} @@ -35,7 +35,7 @@ export default function App() { - + {/* */} ); } \ No newline at end of file diff --git a/frontend/src/api/legacy/Client.ts b/frontend/src/api/legacy/Client.ts deleted file mode 100644 index 94ecbd5..0000000 --- a/frontend/src/api/legacy/Client.ts +++ /dev/null @@ -1,275 +0,0 @@ -import axios from "axios"; - -// --- ENV CONFIG --- -const API_BASE_URL = - import.meta.env.VITE_API_BASE_URL || "http://localhost:8000"; - -const LOGIN_PATH = import.meta.env.VITE_LOGIN_PATH || "/login"; - - -// --- ERROR EVENT BUS --- -const ERROR_EVENT = "api:error"; -type ApiErrorDetail = { - message: string; - status?: number; - url?: string; - data?: unknown; -}; - -// Use interface instead of arrow function types for readability -interface ApiErrorHandler { - (e: CustomEvent): void; -} - -function notifyError(detail: ApiErrorDetail) { - window.dispatchEvent(new CustomEvent(ERROR_EVENT, { detail })); - // eslint-disable-next-line no-console - console.error("[API ERROR]", detail); -} -function onError(handler: ApiErrorHandler) { - const wrapped = handler as EventListener; - window.addEventListener(ERROR_EVENT, wrapped as EventListener); - - return () => window.removeEventListener(ERROR_EVENT, wrapped); -} - -// --- AXIOS INSTANCES --- -// Always send cookies. Django will set auth cookies; browser will include them automatically. -function createAxios(baseURL: string): any { - const instance = axios.create({ - baseURL, - withCredentials: true, // cookies - headers: { - "Content-Type": "application/json", - }, - timeout: 20000, - }); - return instance; -} - -// Use a single behavior for both: cookies are always sent -const apiPublic = createAxios(API_BASE_URL); -const apiAuth = createAxios(API_BASE_URL); - - -// --- REQUEST INTERCEPTOR (PUBLIC) --- -// Ensure no Authorization header is ever sent by the public client -apiPublic.interceptors.request.use(function (config: any) { - if (config?.headers && (config.headers as any).Authorization) { - delete (config.headers as any).Authorization; - } - - return config; -}); - -// --- REQUEST INTERCEPTOR (AUTH) --- -// Do not attach Authorization header; rely on cookies set by Django. -apiAuth.interceptors.request.use(function (config: any) { - (config as any)._retryCount = (config as any)._retryCount || 0; - - return config; -}); - -// --- RESPONSE INTERCEPTOR (AUTH) --- -// Simplified: on 401, redirect to login. Server manages refresh via cookies. -apiAuth.interceptors.response.use( - function (response: any) { - return response; - }, - async function (error: any) { - if (!error.response) { - alert("Backend connection is unavailable. Please check your network."); - - notifyError({ - message: "Network error or backend unavailable", - url: error.config?.url, - }); - - return Promise.reject(error); - } - - const status = error.response.status; - if (status === 401) { - ClearTokens(); - window.location.assign(LOGIN_PATH); - return Promise.reject(error); - } - - notifyError({ - message: - (error.response.data as any)?.detail || - (error.response.data as any)?.message || - `Request failed with status ${status}`, - status, - url: error.config?.url, - data: error.response.data, - }); - return Promise.reject(error); - } -); - -// --- PUBLIC CLIENT: still emits errors and alerts on network failure --- -apiPublic.interceptors.response.use( - function (response: any) { - return response; - }, - async function (error: any) { - if (!error.response) { - alert("Backend connection is unavailable. Please check your network."); - notifyError({ - message: "Network error or backend unavailable", - url: error.config?.url, - }); - return Promise.reject(error); - } - notifyError({ - message: - (error.response.data as any)?.detail || - (error.response.data as any)?.message || - `Request failed with status ${error.response.status}`, - status: error.response.status, - url: error.config?.url, - data: error.response.data, - }); - return Promise.reject(error); - } -); - - - - -function Logout() { - try { - const LogOutResponse = apiAuth.post("/api/logout/"); - - if (LogOutResponse.body.detail != "Logout successful") { - throw new Error("Logout failed"); - } - - ClearTokens(); - - } catch (error) { - console.error("Error during logout:", error); - } -} - -function ClearTokens(){ - document.cookie = "access_token=; Max-Age=0; path=/"; - document.cookie = "refresh_token=; Max-Age=0; path=/"; -} - - -// --- EXPORT DEFAULT API WRAPPER --- -const Client = { - // Axios instances - auth: apiAuth, - public: apiPublic, - - Logout, - - // Error subscription - onError, -}; - -export default Client; - -/** -USAGE EXAMPLES (TypeScript/React) - -Import the client --------------------------------------------------- -import Client from "@/api/Client"; - - -Login: obtain tokens and persist to cookies --------------------------------------------------- -async function login(username: string, password: string) { - // SimpleJWT default login endpoint (adjust if your backend differs) - // Example backend endpoint: POST /api/token/ -> { access, refresh } - const res = await Client.public.post("/api/token/", { username, password }); - const { access, refresh } = res.data; - Client.setTokens(access, refresh); - // After this, Client.auth will automatically attach Authorization header - // and refresh when receiving a 401 (up to 2 retries). -} - - -Public request (no cookies, no Authorization) --------------------------------------------------- -// The public client does NOT send cookies or Authorization. -async function listPublicItems() { - const res = await Client.public.get("/api/public/items/"); - return res.data; -} - - -Authenticated requests (auto Bearer header + refresh on 401) --------------------------------------------------- -async function fetchProfile() { - const res = await Client.auth.get("/api/users/me/"); - return res.data; -} - -async function updateProfile(payload: { first_name?: string; last_name?: string }) { - const res = await Client.auth.patch("/api/users/me/", payload); - return res.data; -} - - -Global error handling (UI notifications) --------------------------------------------------- -import { useEffect } from "react"; - -function useApiErrors(showToast: (msg: string) => void) { - useEffect(function () { - const unsubscribe = Client.onError(function (e) { - const { message, status } = e.detail; - showToast(status ? String(status) + ": " + message : message); - }); - return unsubscribe; - }, [showToast]); -} - -// Note: Network connectivity issues trigger an alert and also dispatch api:error. -// All errors are logged to console for developers. - - -Logout --------------------------------------------------- -function logout() { - Client.clearTokens(); - window.location.assign("/login"); -} - - -Route protection (PrivateRoute) --------------------------------------------------- -// If you created src/routes/PrivateRoute.tsx, wrap your protected routes with it. -// PrivateRoute checks for "access_token" cookie presence and redirects to /login if missing. - -// Example: -// -// } > -// }> -// } /> -// } /> -// -// -// } /> -// - - -Refresh and retry flow (what happens on 401) --------------------------------------------------- -// 1) Client.auth request receives 401 from backend -// 2) Client tries to refresh access token using refresh_token cookie -// 3) If refresh succeeds, original request is retried (max 2 times) -// 4) If still 401 (or no refresh token), tokens are cleared and user is redirected to /login - - -Environment variables (optional overrides) --------------------------------------------------- -// VITE_API_BASE_URL default: "http://localhost:8000" -// VITE_API_REFRESH_URL default: "/api/token/refresh/" -// VITE_LOGIN_PATH default: "/login" -*/ diff --git a/frontend/src/api/legacy/Downloader.ts b/frontend/src/api/legacy/Downloader.ts deleted file mode 100644 index 80dde6c..0000000 --- a/frontend/src/api/legacy/Downloader.ts +++ /dev/null @@ -1,114 +0,0 @@ -import Client from "./Client"; - -// Available output containers (must match backend) -export const FORMAT_EXTS = ["mp4", "mkv", "webm", "flv", "mov", "avi", "ogg"] as const; -export type FormatExt = (typeof FORMAT_EXTS)[number]; - -export type InfoResponse = { - title: string | null; - duration: number | null; - thumbnail: string | null; - video_resolutions: string[]; // e.g. ["2160p", "1440p", "1080p", ...] - audio_resolutions: string[]; // e.g. ["320kbps", "160kbps", ...] -}; - -// GET info for a URL -export async function fetchInfo(url: string): Promise { - const res = await Client.public.get("/api/downloader/download/", { - params: { url }, - }); - return res.data as InfoResponse; -} - -// POST to stream binary immediately; returns { blob, filename } -export async function downloadImmediate(args: { - url: string; - ext: FormatExt; - videoResolution?: string | number; // "1080p" or 1080 - audioResolution?: string | number; // "160kbps" or 160 -}): Promise<{ blob: Blob; filename: string }> { - const video_quality = toHeight(args.videoResolution); - const audio_quality = toKbps(args.audioResolution); - - if (video_quality == null || audio_quality == null) { - throw new Error("Please select both video and audio quality."); - } - - const res = await Client.public.post( - "/api/downloader/download/", - { - url: args.url, - ext: args.ext, - video_quality, - audio_quality, - }, - { responseType: "blob" } - ); - - const cd = res.headers?.["content-disposition"] as string | undefined; - const xfn = res.headers?.["x-filename"] as string | undefined; - const filename = - parseContentDispositionFilename(cd) || - (xfn && xfn.trim()) || - inferFilenameFromUrl(args.url, res.headers?.["content-type"] as string | undefined) || - `download.${args.ext}`; - - return { blob: res.data as Blob, filename }; -} - -// Helpers -export function parseContentDispositionFilename(cd?: string): string | null { - if (!cd) return null; - const utf8Match = cd.match(/filename\*\s*=\s*UTF-8''([^;]+)/i); - if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]); - const plainMatch = cd.match(/filename\s*=\s*"([^"]+)"/i) || cd.match(/filename\s*=\s*([^;]+)/i); - return plainMatch?.[1]?.trim() || null; -} - -function inferFilenameFromUrl(url: string, contentType?: string): string { - try { - const u = new URL(url); - const last = u.pathname.split("/").filter(Boolean).pop(); - if (last) return last; - } catch { - // ignore - } - if (contentType) { - const ext = contentTypeToExt(contentType); - return `download${ext ? `.${ext}` : ""}`; - } - return "download.bin"; -} - -function contentTypeToExt(ct?: string): string | null { - if (!ct) return null; - const map: Record = { - "video/mp4": "mp4", - "video/x-matroska": "mkv", - "video/webm": "webm", - "video/x-flv": "flv", - "video/quicktime": "mov", - "video/x-msvideo": "avi", - "video/ogg": "ogg", - "application/octet-stream": "bin", - }; - return map[ct] || null; -} - -function toHeight(v?: string | number): number | undefined { - if (typeof v === "number") return v || undefined; - if (!v) return undefined; - const m = /^(\d{2,4})p$/i.exec(v.trim()); - if (m) return parseInt(m[1], 10); - const n = Number(v); - return Number.isFinite(n) ? (n as number) : undefined; -} - -function toKbps(v?: string | number): number | undefined { - if (typeof v === "number") return v || undefined; - if (!v) return undefined; - const m = /^(\d{2,4})\s*kbps$/i.exec(v.trim()); - if (m) return parseInt(m[1], 10); - const n = Number(v); - return Number.isFinite(n) ? (n as number) : undefined; -} diff --git a/frontend/src/api/legacy/models/User.ts b/frontend/src/api/legacy/models/User.ts deleted file mode 100644 index 41adbba..0000000 --- a/frontend/src/api/legacy/models/User.ts +++ /dev/null @@ -1,82 +0,0 @@ -// 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} - */ - async getCurrentUser() { - const response = await Client.auth.get(`${API_BASE_URL}/me/`); - return response.data; - }, - - /** - * Get all users - * @returns {Promise>} - */ - 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} - */ - 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} - */ - 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} - */ - 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} - */ - 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>} - */ - 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; diff --git a/frontend/src/components/Footer/footer.module.css b/frontend/src/components/Footer/footer.module.css index 4881c0f..e9f5585 100644 --- a/frontend/src/components/Footer/footer.module.css +++ b/frontend/src/components/Footer/footer.module.css @@ -103,4 +103,4 @@ footer .links a{ padding-top: 1em; gap: 2em; } - } \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/components/hero/HeroCarousel.tsx b/frontend/src/components/hero/HeroCarousel.tsx index ea8a687..b2ec490 100644 --- a/frontend/src/components/hero/HeroCarousel.tsx +++ b/frontend/src/components/hero/HeroCarousel.tsx @@ -1,62 +1,8 @@ import { useEffect, useState } from "react"; -const videos = ["dQw4w9WgXcQ", "M7lc1UVf-VE", "aqz-KE-bpKQ"]; // placeholder IDs - export default function HeroCarousel() { - const [index, setIndex] = useState(0); - - useEffect(() => { - const id = setInterval(() => setIndex(i => (i + 1) % videos.length), 10000); - return () => clearInterval(id); - }, []); - return ( -
- {/* Background Gradient and animated glows */} -
-
-
-
-
-
- -
- {/* Text */} -
-

- Welcome to
- Vontor.cz -

-

Creative Tech & Design by Bruno Vontor

- -
- - {/* Video carousel */} -
-
- {videos.map((v,i) => ( -