complete basic functionalities #1
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"Header": {
|
||||||
|
"title": "Proxy Raye",
|
||||||
|
"description": "A proxy for XVideos",
|
||||||
|
"disclaimer_0": "Genital sexuality is only one of the many possible conceptions of sexuality",
|
||||||
|
"disclaimer_1": "Platform capitalism makes money on desire flow. Proxies avoid this to happen.",
|
||||||
|
"disclaimer_2": "Platform capitalism is narcissism-driven",
|
||||||
|
"disclaimer_3": "Pornhub annoying elements (like ads) are put there intentionally to make you upgrade to premium",
|
||||||
|
"disclaimer_4": "You're going to masturbate on someone else's imaginary",
|
||||||
|
"disclaimer_5": "No banners or annoying popups. You can jerk off with no hassle!",
|
||||||
|
"disclaimer_6": "You're choosing image over imagination. What if they're not in antithesis?"
|
||||||
|
},
|
||||||
|
"Search": {
|
||||||
|
"placeholder": "categories, pornostars, etc...",
|
||||||
|
"submit": "Search"
|
||||||
|
},
|
||||||
|
"Results": {
|
||||||
|
"query": "Search results for: {{ query }}",
|
||||||
|
"toggle": "Show preview",
|
||||||
|
"noData": "No videos found :("
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"Header": {
|
||||||
|
"title": "Proxy Raye",
|
||||||
|
"description": "Un proxy per XVideos",
|
||||||
|
"disclaimer_0": "Quella genitale è solo una delle possibili concezioni della sessualità.",
|
||||||
|
"disclaimer_1": "Le piattaforme monetizzano i flussi di desiderio. I proxy impediscono che questo accada.",
|
||||||
|
"disclaimer_2": "Le piattaforme si alimentano del narcisisismo degli utenti.",
|
||||||
|
"disclaimer_3": "Gli elementi di disturbo su PornHub sono messi lì a posta per farti passare alla versione Premium.",
|
||||||
|
"disclaimer_4": "Stai per masturbarti sull'immaginario di qualcun altro.",
|
||||||
|
"disclaimer_5": "Niente banner o popup fastidiosi. Puoi masturbarti in santa pace.",
|
||||||
|
"disclaimer_6": "Stai preferendo l'immagine all'immaginazione. E se immagine e immaginazione non fossero in antitesi?"
|
||||||
|
},
|
||||||
|
"Search": {
|
||||||
|
"placeholder": "categorie, pornostar, ecc...",
|
||||||
|
"submit": "Cerca"
|
||||||
|
},
|
||||||
|
"Results": {
|
||||||
|
"query": "Risultati della ricerca per: {{ query }}",
|
||||||
|
"toggle": "Mostra anteprime risultati",
|
||||||
|
"noData": "Nessun video trovato :("
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
const createNextIntlPlugin = require('next-intl/plugin');
|
||||||
|
|
||||||
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
module.exports = withNextIntl({
|
||||||
|
sassOptions: {
|
||||||
|
includePaths: [path.join(__dirname, 'src/styles')],
|
||||||
|
},
|
||||||
|
})
|
|
@ -1,4 +0,0 @@
|
||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
|
@ -9,16 +9,24 @@
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@picocss/pico": "^2.0.6",
|
||||||
|
"axios": "^1.6.8",
|
||||||
|
"cheerio": "^1.0.0-rc.12",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"next": "14.2.2",
|
||||||
|
"next-intl": "^3.11.3",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"next": "14.2.2"
|
"video.js": "^8.10.0",
|
||||||
|
"videojs-hls-quality-selector": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.2"
|
"eslint-config-next": "14.2.2",
|
||||||
|
"sass": "^1.75.0",
|
||||||
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import "@/styles/globals.scss"
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
params: {locale}
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
params: {locale: string};
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang={locale}>
|
||||||
|
<head>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Proxy Raye: un proxy per XVideos basato su PornInvidious</title>
|
||||||
|
</head>
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
|
||||||
|
import Layout from "@/components/Layout";
|
||||||
|
|
||||||
|
import Home from "@/components/Pages/Home";
|
||||||
|
|
||||||
|
import { fetchGalleryData } from '@/utils/scrape/gallery';
|
||||||
|
|
||||||
|
export default async function HomePage() {
|
||||||
|
|
||||||
|
const data = await fetchGalleryData()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Home data={data} />
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import Layout from "@/components/Layout";
|
||||||
|
|
||||||
|
import Search from "@/components/Pages/Search";
|
||||||
|
|
||||||
|
import { fetchGalleryData } from "@/utils/scrape/gallery";
|
||||||
|
|
||||||
|
export default async function SearchPage({ params }: { params: { query: string } }) {
|
||||||
|
|
||||||
|
const data = await fetchGalleryData({ query: params.query })
|
||||||
|
|
||||||
|
return <Layout>
|
||||||
|
<Search data={data} query={params.query} />
|
||||||
|
</Layout>
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Layout from "@/components/Layout";
|
||||||
|
|
||||||
|
import Search from "@/components/Pages/Search";
|
||||||
|
|
||||||
|
export default async function SearchNoQueryPage() {
|
||||||
|
|
||||||
|
return <Layout>
|
||||||
|
<Search data={[]} query={''} />
|
||||||
|
</Layout>
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
import Layout from "@/components/Layout";
|
||||||
|
|
||||||
|
import Video from "@/components/Pages/Video";
|
||||||
|
|
||||||
|
import { fetchVideoData } from "@/utils/scrape/video";
|
||||||
|
|
||||||
|
import { useLocale } from 'next-intl';
|
||||||
|
|
||||||
|
export default async function VideoPage({ params }: { params: { id: string } }) {
|
||||||
|
|
||||||
|
const locale = useLocale()
|
||||||
|
|
||||||
|
const decodedId = decodeURIComponent(params.id)
|
||||||
|
|
||||||
|
const [data, related] = await fetchVideoData(decodedId)
|
||||||
|
|
||||||
|
if (!data.lowResUrl) {
|
||||||
|
redirect(`/${locale}/404`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Layout>
|
||||||
|
<Video id={params.id} data={data} related={related}/>
|
||||||
|
</Layout>
|
||||||
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
|
@ -1,107 +0,0 @@
|
||||||
:root {
|
|
||||||
--max-width: 1100px;
|
|
||||||
--border-radius: 12px;
|
|
||||||
--font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
|
|
||||||
"Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
|
|
||||||
"Fira Mono", "Droid Sans Mono", "Courier New", monospace;
|
|
||||||
|
|
||||||
--foreground-rgb: 0, 0, 0;
|
|
||||||
--background-start-rgb: 214, 219, 220;
|
|
||||||
--background-end-rgb: 255, 255, 255;
|
|
||||||
|
|
||||||
--primary-glow: conic-gradient(
|
|
||||||
from 180deg at 50% 50%,
|
|
||||||
#16abff33 0deg,
|
|
||||||
#0885ff33 55deg,
|
|
||||||
#54d6ff33 120deg,
|
|
||||||
#0071ff33 160deg,
|
|
||||||
transparent 360deg
|
|
||||||
);
|
|
||||||
--secondary-glow: radial-gradient(
|
|
||||||
rgba(255, 255, 255, 1),
|
|
||||||
rgba(255, 255, 255, 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
--tile-start-rgb: 239, 245, 249;
|
|
||||||
--tile-end-rgb: 228, 232, 233;
|
|
||||||
--tile-border: conic-gradient(
|
|
||||||
#00000080,
|
|
||||||
#00000040,
|
|
||||||
#00000030,
|
|
||||||
#00000020,
|
|
||||||
#00000010,
|
|
||||||
#00000010,
|
|
||||||
#00000080
|
|
||||||
);
|
|
||||||
|
|
||||||
--callout-rgb: 238, 240, 241;
|
|
||||||
--callout-border-rgb: 172, 175, 176;
|
|
||||||
--card-rgb: 180, 185, 188;
|
|
||||||
--card-border-rgb: 131, 134, 135;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--foreground-rgb: 255, 255, 255;
|
|
||||||
--background-start-rgb: 0, 0, 0;
|
|
||||||
--background-end-rgb: 0, 0, 0;
|
|
||||||
|
|
||||||
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
|
|
||||||
--secondary-glow: linear-gradient(
|
|
||||||
to bottom right,
|
|
||||||
rgba(1, 65, 255, 0),
|
|
||||||
rgba(1, 65, 255, 0),
|
|
||||||
rgba(1, 65, 255, 0.3)
|
|
||||||
);
|
|
||||||
|
|
||||||
--tile-start-rgb: 2, 13, 46;
|
|
||||||
--tile-end-rgb: 2, 5, 19;
|
|
||||||
--tile-border: conic-gradient(
|
|
||||||
#ffffff80,
|
|
||||||
#ffffff40,
|
|
||||||
#ffffff30,
|
|
||||||
#ffffff20,
|
|
||||||
#ffffff10,
|
|
||||||
#ffffff10,
|
|
||||||
#ffffff80
|
|
||||||
);
|
|
||||||
|
|
||||||
--callout-rgb: 20, 20, 20;
|
|
||||||
--callout-border-rgb: 108, 108, 108;
|
|
||||||
--card-rgb: 100, 100, 100;
|
|
||||||
--card-border-rgb: 200, 200, 200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
max-width: 100vw;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
color: rgb(var(--foreground-rgb));
|
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
transparent,
|
|
||||||
rgb(var(--background-end-rgb))
|
|
||||||
)
|
|
||||||
rgb(var(--background-start-rgb));
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
html {
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import type { Metadata } from "next";
|
|
||||||
import { Inter } from "next/font/google";
|
|
||||||
import "./globals.css";
|
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Create Next App",
|
|
||||||
description: "Generated by create next app",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) {
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<body className={inter.className}>{children}</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,230 +0,0 @@
|
||||||
.main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 6rem;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
display: inherit;
|
|
||||||
justify-content: inherit;
|
|
||||||
align-items: inherit;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
max-width: var(--max-width);
|
|
||||||
width: 100%;
|
|
||||||
z-index: 2;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
.description a {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description p {
|
|
||||||
position: relative;
|
|
||||||
margin: 0;
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: rgba(var(--callout-rgb), 0.5);
|
|
||||||
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.code {
|
|
||||||
font-weight: 700;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, minmax(25%, auto));
|
|
||||||
max-width: 100%;
|
|
||||||
width: var(--max-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 1rem 1.2rem;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
background: rgba(var(--card-rgb), 0);
|
|
||||||
border: 1px solid rgba(var(--card-border-rgb), 0);
|
|
||||||
transition: background 200ms, border 200ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card span {
|
|
||||||
display: inline-block;
|
|
||||||
transition: transform 200ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card h2 {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card p {
|
|
||||||
margin: 0;
|
|
||||||
opacity: 0.6;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
max-width: 30ch;
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
padding: 4rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center::before {
|
|
||||||
background: var(--secondary-glow);
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 480px;
|
|
||||||
height: 360px;
|
|
||||||
margin-left: -400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center::after {
|
|
||||||
background: var(--primary-glow);
|
|
||||||
width: 240px;
|
|
||||||
height: 180px;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center::before,
|
|
||||||
.center::after {
|
|
||||||
content: "";
|
|
||||||
left: 50%;
|
|
||||||
position: absolute;
|
|
||||||
filter: blur(45px);
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
/* Enable hover only on non-touch devices */
|
|
||||||
@media (hover: hover) and (pointer: fine) {
|
|
||||||
.card:hover {
|
|
||||||
background: rgba(var(--card-rgb), 0.1);
|
|
||||||
border: 1px solid rgba(var(--card-border-rgb), 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover span {
|
|
||||||
transform: translateX(4px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion) {
|
|
||||||
.card:hover span {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile */
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
.content {
|
|
||||||
padding: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
margin-bottom: 120px;
|
|
||||||
max-width: 320px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 1rem 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card h2 {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
padding: 8rem 0 6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center::before {
|
|
||||||
transform: none;
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description a {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description p,
|
|
||||||
.description div {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description p {
|
|
||||||
align-items: center;
|
|
||||||
inset: 0 0 auto;
|
|
||||||
padding: 2rem 1rem 1.4rem;
|
|
||||||
border-radius: 0;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
|
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgba(var(--background-start-rgb), 1),
|
|
||||||
rgba(var(--callout-rgb), 0.5)
|
|
||||||
);
|
|
||||||
background-clip: padding-box;
|
|
||||||
backdrop-filter: blur(24px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.description div {
|
|
||||||
align-items: flex-end;
|
|
||||||
pointer-events: none;
|
|
||||||
inset: auto 0 0;
|
|
||||||
padding: 2rem;
|
|
||||||
height: 200px;
|
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
transparent 0%,
|
|
||||||
rgb(var(--background-end-rgb)) 40%
|
|
||||||
);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tablet and Smaller Desktop */
|
|
||||||
@media (min-width: 701px) and (max-width: 1120px) {
|
|
||||||
.grid {
|
|
||||||
grid-template-columns: repeat(2, 50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.vercelLogo {
|
|
||||||
filter: invert(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotate {
|
|
||||||
from {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
import Image from "next/image";
|
|
||||||
import styles from "./page.module.css";
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<main className={styles.main}>
|
|
||||||
<div className={styles.description}>
|
|
||||||
<p>
|
|
||||||
Get started by editing
|
|
||||||
<code className={styles.code}>src/app/page.tsx</code>
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
By{" "}
|
|
||||||
<Image
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel Logo"
|
|
||||||
className={styles.vercelLogo}
|
|
||||||
width={100}
|
|
||||||
height={24}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.center}>
|
|
||||||
<Image
|
|
||||||
className={styles.logo}
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js Logo"
|
|
||||||
width={180}
|
|
||||||
height={37}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.grid}>
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className={styles.card}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
Docs <span>-></span>
|
|
||||||
</h2>
|
|
||||||
<p>Find in-depth information about Next.js features and API.</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className={styles.card}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
Learn <span>-></span>
|
|
||||||
</h2>
|
|
||||||
<p>Learn about Next.js in an interactive course with quizzes!</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className={styles.card}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
Templates <span>-></span>
|
|
||||||
</h2>
|
|
||||||
<p>Explore starter templates for Next.js.</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className={styles.card}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
Deploy <span>-></span>
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {useTranslations} from 'next-intl';
|
||||||
|
|
||||||
|
const Description: React.FC = () => {
|
||||||
|
|
||||||
|
const t = useTranslations('Header');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>{t('description')}</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Description;
|
|
@ -0,0 +1,10 @@
|
||||||
|
@import 'colors';
|
||||||
|
@import 'spacing';
|
||||||
|
|
||||||
|
.messageBox {
|
||||||
|
border: 1px solid $colors_yellow;
|
||||||
|
text-align: center;
|
||||||
|
padding: $spacing_8;
|
||||||
|
border-radius: $spacing_4;
|
||||||
|
margin-bottom: $spacing_32;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import style from './Disclaimer.module.scss'
|
||||||
|
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
const Disclaimer: React.FC = () => {
|
||||||
|
|
||||||
|
const MAX_DISCLAIMER_NO = 6
|
||||||
|
|
||||||
|
const t = useTranslations('Header');
|
||||||
|
|
||||||
|
const getRandomArbitrary = (max: number) => {
|
||||||
|
return Math.floor( Math.random() * max);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.messageBox}>{t(`disclaimer_${getRandomArbitrary(MAX_DISCLAIMER_NO)}`)}</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Disclaimer;
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {useTranslations} from 'next-intl';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
const Title: React.FC = () => {
|
||||||
|
|
||||||
|
const t = useTranslations('Header');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={'/'}><h1>{t('title')}</h1></Link>
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Title;
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Title from './Title';
|
||||||
|
import Description from './Description';
|
||||||
|
import Disclaimer from './Disclaimer';
|
||||||
|
|
||||||
|
const Header: React.FC = () => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title />
|
||||||
|
<Description />
|
||||||
|
<Disclaimer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
|
@ -0,0 +1,5 @@
|
||||||
|
@import 'spacing';
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin-bottom: $spacing_32;
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import videojs from 'video.js';
|
||||||
|
|
||||||
|
import 'video.js/dist/video-js.css';
|
||||||
|
|
||||||
|
export const VideoJS = (props: { options: any; onReady: any; }) => {
|
||||||
|
const videoRef = React.useRef(null);
|
||||||
|
const playerRef = React.useRef(null);
|
||||||
|
const {options, onReady} = props;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
|
||||||
|
// Make sure Video.js player is only initialized once
|
||||||
|
if (!playerRef.current) {
|
||||||
|
// The Video.js player needs to be _inside_ the component el for React 18 Strict Mode.
|
||||||
|
const videoElement = document.createElement("video-js");
|
||||||
|
|
||||||
|
videoElement.classList.add('vjs-big-play-centered');
|
||||||
|
//@ts-ignore
|
||||||
|
videoRef.current.appendChild(videoElement);
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
const player = playerRef.current = videojs(videoElement, options, () => {
|
||||||
|
videojs.log('player is ready');
|
||||||
|
onReady && onReady(player);
|
||||||
|
});
|
||||||
|
|
||||||
|
// You could update an existing player in the `else` block here
|
||||||
|
// on prop change, for example:
|
||||||
|
} else {
|
||||||
|
const player = playerRef.current;
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
player.autoplay(options.autoplay);
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
player.src(options.sources);
|
||||||
|
}
|
||||||
|
}, [options, videoRef]);
|
||||||
|
|
||||||
|
// Dispose the Video.js player when the functional component unmounts
|
||||||
|
React.useEffect(() => {
|
||||||
|
const player = playerRef.current;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
//@ts-ignore
|
||||||
|
if (player && !player.isDisposed()) {
|
||||||
|
//@ts-ignore
|
||||||
|
player.dispose();
|
||||||
|
playerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [playerRef]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-vjs-player>
|
||||||
|
<div ref={videoRef} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VideoJS;
|
|
@ -0,0 +1,63 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import style from './Player.module.scss'
|
||||||
|
|
||||||
|
import VideoJS from './VideoJS';
|
||||||
|
import { VideoData } from '@/meta/data';
|
||||||
|
|
||||||
|
import 'videojs-hls-quality-selector';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: VideoData
|
||||||
|
}
|
||||||
|
|
||||||
|
const Player: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const { data } = props;
|
||||||
|
|
||||||
|
const videoSrc = data.hlsUrl ?? data.lowResUrl
|
||||||
|
const videoType = data.hlsUrl ? 'application/x-mpegURL' : 'video/mp4'
|
||||||
|
|
||||||
|
const playerRef = React.useRef(null);
|
||||||
|
|
||||||
|
const videoJsOptions = {
|
||||||
|
autoplay: true,
|
||||||
|
controls: true,
|
||||||
|
responsive: true,
|
||||||
|
fluid: true,
|
||||||
|
sources: [{
|
||||||
|
src: videoSrc,
|
||||||
|
type: videoType
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
const handlePlayerReady = (player) => {
|
||||||
|
playerRef.current = player;
|
||||||
|
|
||||||
|
player.hlsQualitySelector({
|
||||||
|
vjsIconClass: "vjs-icon-hd"
|
||||||
|
})
|
||||||
|
|
||||||
|
// You can handle player events here, for example:
|
||||||
|
player.on('waiting', () => {
|
||||||
|
console.log('player is waiting');
|
||||||
|
});
|
||||||
|
|
||||||
|
player.on('dispose', () => {
|
||||||
|
console.log('player will dispose');
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.container}>
|
||||||
|
<VideoJS options={videoJsOptions} onReady={handlePlayerReady} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Player;
|
|
@ -0,0 +1,31 @@
|
||||||
|
@import 'spacing';
|
||||||
|
@import 'breakpoints';
|
||||||
|
|
||||||
|
.galleryContainer {
|
||||||
|
display: grid;
|
||||||
|
gap: $spacing_16;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
|
||||||
|
@media only screen and (min-width: $portrait) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: $tablet) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: $laptop) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: $desktop) {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.galleryContainer.show {
|
||||||
|
@media only screen and (max-width: $portrait) {
|
||||||
|
grid-template-columns: repeat(1, 1fr);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
@import 'spacing';
|
||||||
|
@import 'colors';
|
||||||
|
@import 'breakpoints';
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $colors_yellow;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media only screen and (min-width: $tablet) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
border-radius: $spacing_4;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnailContainer {
|
||||||
|
opacity: 0.1;
|
||||||
|
transition: 1s;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnailContainer:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnailContainer.show {
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media only screen and (min-width: $tablet) {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
import style from './Thumbnail.module.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
locale: string
|
||||||
|
videoUrl: string
|
||||||
|
imgUrl: string
|
||||||
|
text: string
|
||||||
|
show: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Thumbnail: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const { locale, videoUrl, imgUrl, text, show } = props
|
||||||
|
|
||||||
|
const encodedUri = encodeURIComponent(videoUrl)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(style.thumbnailContainer, { [style.show]: show } )}>
|
||||||
|
<Link href={`/${locale}/video/${encodedUri}`}>
|
||||||
|
<img className={style.image} src={imgUrl} />
|
||||||
|
<div className={style.text}>{text}</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Thumbnail;
|
|
@ -0,0 +1,38 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import style from './Gallery.module.scss'
|
||||||
|
import Thumbnail from './Thumbnail';
|
||||||
|
import { GalleryData } from '@/meta/data';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: GalleryData[]
|
||||||
|
show: boolean
|
||||||
|
locale: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Gallery: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const { show, locale, data } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classNames(style.galleryContainer, { [style.show]: show })}>
|
||||||
|
{data && data.map((elem, key) => {
|
||||||
|
return <Thumbnail
|
||||||
|
show={show}
|
||||||
|
key={key}
|
||||||
|
imgUrl={elem.imgUrl}
|
||||||
|
videoUrl={elem.videoUrl}
|
||||||
|
text={elem.text}
|
||||||
|
locale={locale} />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Gallery;
|
|
@ -0,0 +1,22 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import style from './NoData.module.scss';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
msg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const NoData: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const { msg } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.container}>
|
||||||
|
<span>{msg}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoData;
|
|
@ -0,0 +1,7 @@
|
||||||
|
@import 'breakpoints';
|
||||||
|
|
||||||
|
.toggleContainer {
|
||||||
|
@media only screen and (min-width: $tablet) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import style from './Toggle.module.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string
|
||||||
|
handleClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Toggle: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const { label, handleClick } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.toggleContainer}>
|
||||||
|
<input onChange={handleClick} type="checkbox" role="switch" name='toggle' />
|
||||||
|
<div className={style.label}>{label}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Toggle;
|
|
@ -0,0 +1,38 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import style from './Wrapper.module.scss';
|
||||||
|
|
||||||
|
import Gallery from './Gallery';
|
||||||
|
import Toggle from './Toggle';
|
||||||
|
import { GalleryData } from '@/meta/data';
|
||||||
|
import NoData from './NoData';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: GalleryData[]
|
||||||
|
locale: string
|
||||||
|
labels: {
|
||||||
|
toggle: string
|
||||||
|
noData: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const { labels, locale, data } = props
|
||||||
|
|
||||||
|
const [show, setShow] = useState<boolean>(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.resultsContainer}>
|
||||||
|
{(data && data.length > 0) && <>
|
||||||
|
<Toggle label={labels.toggle} handleClick={() => setShow(!show)} />
|
||||||
|
<Gallery data={data} locale={locale} show={show} />
|
||||||
|
</>}
|
||||||
|
{(!data || data.length == 0) && <NoData msg={labels.noData}/>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Wrapper;
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React, { } from 'react';
|
||||||
|
|
||||||
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
import Wrapper from './Wrapper';
|
||||||
|
import { GalleryData } from '@/meta/data';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: GalleryData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const Results: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const { data } = props
|
||||||
|
|
||||||
|
const t = useTranslations('Results');
|
||||||
|
const locale = useLocale()
|
||||||
|
|
||||||
|
return (<Wrapper data={data} locale={locale} labels={{ toggle: t('toggle'), noData: t('noData') }} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Results;
|
|
@ -0,0 +1,30 @@
|
||||||
|
@import 'spacing';
|
||||||
|
@import 'colors';
|
||||||
|
|
||||||
|
.searchForm {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query{
|
||||||
|
flex: 6;
|
||||||
|
margin-right: $spacing_16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitBtn{
|
||||||
|
flex: 1;
|
||||||
|
background-color: $colors_yellow;
|
||||||
|
border-color: $colors_yellow;
|
||||||
|
color: $colors_yellow_inverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitBtn:hover {
|
||||||
|
background-color: $colors_yellow_hover;
|
||||||
|
border-color: $colors_yellow_hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitBtn:focus {
|
||||||
|
background-color: $colors_yellow_focus;
|
||||||
|
border-color: $colors_yellow_focus;
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import style from './SearchBarForm.module.scss'
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
query?: string
|
||||||
|
locale: string
|
||||||
|
labels: {
|
||||||
|
query: { placeholder: string }
|
||||||
|
submit: { value: string }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_SEARCH_LENGTH = 50;
|
||||||
|
|
||||||
|
const SearchBarForm: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const { query, labels, locale } = props;
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
const query = event.target.query.value
|
||||||
|
|
||||||
|
if (query.length > 0) {
|
||||||
|
router.push(`/${locale}/search/${query}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className={style.searchForm} method="get" action="/">
|
||||||
|
<input className={style.query} type="text" size={MAX_SEARCH_LENGTH} name="query" placeholder={labels.query.placeholder} defaultValue={query ?? undefined} />
|
||||||
|
<input className={style.submitBtn} type="submit" value={labels.submit.value} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchBarForm;
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useTranslations, useLocale } from 'next-intl';
|
||||||
|
import SearchBarForm from './SearchBarForm';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
query?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchBar: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const { query } = props;
|
||||||
|
|
||||||
|
const t = useTranslations('Search');
|
||||||
|
const locale = useLocale()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchBarForm query={query} locale={locale} labels={{
|
||||||
|
query: { placeholder: t('placeholder')},
|
||||||
|
submit: { value: t('submit')}
|
||||||
|
}} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchBar;
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Layout: React.FC<React.PropsWithChildren> = (props) => {
|
||||||
|
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="container">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Header from '@/components/Layout/Header';
|
||||||
|
import SearchBar from '@/components/Layout/SearchBar';
|
||||||
|
import Results from '@/components/Layout/Results';
|
||||||
|
|
||||||
|
import { GalleryData } from '@/meta/data';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: GalleryData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const Home: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const { data } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
<SearchBar />
|
||||||
|
<Results data={data} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Header from '@/components/Layout/Header';
|
||||||
|
import SearchBar from '@/components/Layout/SearchBar';
|
||||||
|
import Results from '@/components/Layout/Results';
|
||||||
|
import { GalleryData } from '@/meta/data';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
query: string
|
||||||
|
data: GalleryData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const Search: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const { query, data } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
<SearchBar query={query} />
|
||||||
|
<Results data={data} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Search;
|
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Header from '@/components/Layout/Header';
|
||||||
|
import Player from '@/components/Layout/Player';
|
||||||
|
import SearchBar from '@/components/Layout/SearchBar';
|
||||||
|
import Results from '@/components/Layout/Results';
|
||||||
|
|
||||||
|
import { GalleryData, VideoData } from '@/meta/data';
|
||||||
|
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string
|
||||||
|
data: VideoData
|
||||||
|
related: GalleryData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const Video: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
|
const { data, related } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
<Player data={data} />
|
||||||
|
<SearchBar />
|
||||||
|
{related && <Results data={related} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Video;
|
|
@ -0,0 +1 @@
|
||||||
|
export const XVIDEOS_BASE_URL: string = "https://www.xvideos.com"
|
|
@ -0,0 +1,14 @@
|
||||||
|
import {notFound} from 'next/navigation';
|
||||||
|
import {getRequestConfig} from 'next-intl/server';
|
||||||
|
|
||||||
|
// Can be imported from a shared config
|
||||||
|
const locales = ['en', 'it'];
|
||||||
|
|
||||||
|
export default getRequestConfig(async ({locale}) => {
|
||||||
|
// Validate that the incoming `locale` parameter is valid
|
||||||
|
if (!locales.includes(locale as any)) notFound();
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages: (await import(`../locale/${locale}.json`)).default
|
||||||
|
};
|
||||||
|
});
|
|
@ -0,0 +1,11 @@
|
||||||
|
export interface GalleryData {
|
||||||
|
videoUrl: string
|
||||||
|
imgUrl: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoData {
|
||||||
|
lowResUrl: string,
|
||||||
|
hiResUrl?: string,
|
||||||
|
hlsUrl?: string
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import createMiddleware from 'next-intl/middleware';
|
||||||
|
|
||||||
|
export default createMiddleware({
|
||||||
|
// A list of all locales that are supported
|
||||||
|
locales: ['en', 'it'],
|
||||||
|
|
||||||
|
// Used when no locale matches
|
||||||
|
defaultLocale: 'it'
|
||||||
|
});
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
// Match only internationalized pathnames
|
||||||
|
matcher: ['/', '/(en|it)/:path*']
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
$portrait: 40em;
|
||||||
|
$tablet: 48em;
|
||||||
|
$laptop: 64em;
|
||||||
|
$desktop: 80em;
|
|
@ -0,0 +1,5 @@
|
||||||
|
// primaries
|
||||||
|
$colors_yellow: #fdd835;
|
||||||
|
$colors_yellow_hover: #fbc02d;
|
||||||
|
$colors_yellow_focus: rgba(253, 216, 53, 0.125);
|
||||||
|
$colors_yellow_inverse: rgba(0, 0, 0, 0.75);
|
|
@ -0,0 +1,4 @@
|
||||||
|
$spacing_4: 4px;
|
||||||
|
$spacing_8: 8px;
|
||||||
|
$spacing_16: 16px;
|
||||||
|
$spacing_32: 32px;
|
|
@ -0,0 +1 @@
|
||||||
|
@use "@picocss/pico";
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { XVIDEOS_BASE_URL } from '@/constants/urls';
|
||||||
|
import { GalleryData, VideoData } from '@/meta/data';
|
||||||
|
import axios, { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
|
||||||
|
interface FetchParams {
|
||||||
|
baseUrl?: string
|
||||||
|
query?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchGalleryData = async (params?: FetchParams): Promise<GalleryData[]> => {
|
||||||
|
|
||||||
|
let data: GalleryData[] = [];
|
||||||
|
|
||||||
|
const reqHeaders = {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5397.215 Safari/537.36'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryUrl = `${(params && params.baseUrl) ?? XVIDEOS_BASE_URL}${params && params.query ? '/?k=' + params.query : ''}`
|
||||||
|
|
||||||
|
await axios.get(queryUrl, reqHeaders)
|
||||||
|
|
||||||
|
.then(response => {
|
||||||
|
|
||||||
|
const html = response.data;
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
const thumbs = $(".thumb-block");
|
||||||
|
|
||||||
|
thumbs.map((key, thumb) => {
|
||||||
|
|
||||||
|
const videoUrl = $(thumb).find(".thumb a").attr("href")
|
||||||
|
const imgUrl = $(thumb).find(".thumb img").attr("data-src")
|
||||||
|
const text = $(thumb).find(".thumb-under a").attr("title")
|
||||||
|
|
||||||
|
videoUrl && imgUrl && text && data.push({
|
||||||
|
videoUrl,
|
||||||
|
imgUrl,
|
||||||
|
text
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}).catch((error: AxiosError) => {
|
||||||
|
// handle errors
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { XVIDEOS_BASE_URL } from '@/constants/urls';
|
||||||
|
import { GalleryData, VideoData } from '@/meta/data';
|
||||||
|
|
||||||
|
import axios, { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
import { findRelatedVideos, findVideoUrlInsideTagStringByFunctionNameAndExtension } from '../string';
|
||||||
|
|
||||||
|
interface FetchParams {
|
||||||
|
baseUrl?: string
|
||||||
|
query?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchVideoData = async (videoId: string, params?: FetchParams): Promise<[VideoData, GalleryData[]]> => {
|
||||||
|
|
||||||
|
let data: VideoData = {
|
||||||
|
lowResUrl: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let related: GalleryData[] = [];
|
||||||
|
|
||||||
|
const reqHeaders = {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5397.215 Safari/537.36'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryUrl = `${(params && params.baseUrl) ?? XVIDEOS_BASE_URL}${videoId}`
|
||||||
|
|
||||||
|
await axios.get(queryUrl, reqHeaders)
|
||||||
|
|
||||||
|
.then(response => {
|
||||||
|
|
||||||
|
const html = response.data;
|
||||||
|
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
const scriptTags = $("script");
|
||||||
|
|
||||||
|
// populate video data object
|
||||||
|
scriptTags.map((idx, elem) => {
|
||||||
|
|
||||||
|
const lowResUrl = findVideoUrlInsideTagStringByFunctionNameAndExtension($(elem).toString(), 'setVideoUrlLow', '.mp4')
|
||||||
|
const hiResUrl = findVideoUrlInsideTagStringByFunctionNameAndExtension($(elem).toString(), 'setVideoUrlHigh', '.mp4')
|
||||||
|
const hlsUrl = findVideoUrlInsideTagStringByFunctionNameAndExtension($(elem).toString(), 'setVideoHLS', '.m3u8')
|
||||||
|
|
||||||
|
if (lowResUrl) {
|
||||||
|
data.lowResUrl = lowResUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hiResUrl) {
|
||||||
|
data.hiResUrl = hiResUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hlsUrl) {
|
||||||
|
data.hlsUrl = hlsUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// populate related gallery
|
||||||
|
scriptTags.map((idx, elem) => {
|
||||||
|
const relatedVideos = findRelatedVideos($(elem).toString())
|
||||||
|
|
||||||
|
if (relatedVideos) {
|
||||||
|
related = relatedVideos
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}).catch((error: AxiosError) => {
|
||||||
|
// handle errors
|
||||||
|
});
|
||||||
|
|
||||||
|
return [data, related];
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { GalleryData } from "@/meta/data";
|
||||||
|
|
||||||
|
export const findVideoUrlInsideTagStringByFunctionNameAndExtension = (
|
||||||
|
tagBlock: string, functionName: string, extension: string): string|null => {
|
||||||
|
const start = tagBlock.indexOf(`html5player.${functionName}('`) + `html5player.${functionName}('`.length;
|
||||||
|
const end = tagBlock.toString().indexOf("'", start);
|
||||||
|
|
||||||
|
const substr = tagBlock.substring(start, end);
|
||||||
|
|
||||||
|
if (substr.includes(extension)) {
|
||||||
|
return substr
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findRelatedVideos = (tagBlock: string): GalleryData[]|null => {
|
||||||
|
if (!(tagBlock.includes('video_related=['))) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trova l'inizio e la fine dell'array di oggetti nell'input
|
||||||
|
const start = tagBlock.indexOf('[{');
|
||||||
|
const end = tagBlock.lastIndexOf('}]') + 2;
|
||||||
|
|
||||||
|
// Estrai la sottostringa contenente l'array di oggetti
|
||||||
|
const jsonString = tagBlock.substring(start, end);
|
||||||
|
|
||||||
|
// Parsea la stringa JSON in un array di oggetti
|
||||||
|
const videoRelatedArray = JSON.parse(jsonString);
|
||||||
|
|
||||||
|
// Mappa ogni oggetto nell'array per rinominare le chiavi
|
||||||
|
//@ts-ignore
|
||||||
|
const parsedArray = videoRelatedArray.map(obj => ({
|
||||||
|
//@ts-ignore
|
||||||
|
videoUrl: obj.u,
|
||||||
|
imgUrl: obj.i,
|
||||||
|
text: obj.tf
|
||||||
|
}));
|
||||||
|
|
||||||
|
return parsedArray;
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue