Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/LanguageContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createContext } from "react";

export const LanguageContext = createContext(null);
99 changes: 99 additions & 0 deletions src/LanguageProvider.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { useState } from "react";
import { LanguageContext } from "./LanguageContext";
import PropTypes from "prop-types";

export function LanguageProvider({ children }) {
const [language, setLanguage] = useState("EN");

const toggleLanguage = () => {
setLanguage((prev) => (prev === "EN" ? "PT" : "EN"));
};

const translations = {
EN: {
header: { star: "Star on GitHub" },
landing: {
headline: "Distraction Free Youtube Player",
description:
"A curated collection of programming tutorials without the distractions of regular YouTube.",
getStarted: "Get Started",
},
body: {
pageTitle: "What Do You Want To Learn Today?",
searchPlaceholder: "Search for tutorials...",
totalVideos: "Total videos:",
initialTitle: "Search a video or select from the playlist to begin",
initialDescription: "Select a video",
skillPrompt: "Select a skill to load tutorial",
warningSearch: "Please enter a search term!",
searchButton: "Search",
errorApi: "YouTube API key is not configured. Cannot perform searches.",
errorDefault: "An unexpected error occurred.",
showDescription: "Show Description",
hideDescription: "Hide Description",
},
footer: {
platformFeatures: "Platform Features",
keyboardShortcuts: "Keyboard Shortcuts",
features: {
adFree: "Ad-Free Experience",
curated: "Curated Content",
openSource: "Open Source",
distractionFree: "Distraction-Free",
multiplePlaylists: "Multiple Playlists",
savedProgress: "Saved Progress",
},
developedBy: "Developed by",
redesignedBy: "Redesigned by",
},
},
PT: {
header: { star: "Dar Star no GitHub" },
landing: {
headline: "YouTube Player Sem Distrações",
description:
"Uma coleção selecionada de tutoriais de programação sem as distrações do YouTube tradicional.",
getStarted: "Começar",
},
body: {
pageTitle: "O que você deseja aprender hoje?",
searchPlaceholder: "Buscar tutoriais...",
totalVideos: "Total de vídeos:",
initialTitle: "Procure um vídeo ou selecione da playlist para começar",
initialDescription: "Selecione um vídeo",
skillPrompt: "Selecione uma habilidade para carregar o tutorial",
warningSearch: "Por favor, insira um termo de busca!",
searchButton: "Buscar",
errorApi:
"A chave da API do YouTube não está configurada. Não é possível realizar buscas.",
errorDefault: "Ocorreu um erro inesperado.",
showDescription: "Mostrar Descrição",
hideDescription: "Ocultar Descrição",
},
footer: {
platformFeatures: "Recursos da Plataforma",
keyboardShortcuts: "Atalhos de Teclado",
features: {
adFree: "Experiência sem anúncios",
curated: "Conteúdo Selecionado",
openSource: "Código Aberto",
distractionFree: "Sem Distrações",
multiplePlaylists: "Múltiplas Playlists",
savedProgress: "Progresso Salvo",
},
developedBy: "Desenvolvido por",
redesignedBy: "Redesenhado por",
},
},
};

return (
<LanguageContext.Provider value={{ language, toggleLanguage, translations }}>
{children}
</LanguageContext.Provider>
);
}

LanguageProvider.propTypes = {
children: PropTypes.node.isRequired,
};
12 changes: 11 additions & 1 deletion src/api/youtube.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import axios from 'axios';
const YOUTUBE_API_KEY = import.meta.env.YOUTUBE_API_KEY;
const BASE_URL = 'https://www.googleapis.com/youtube/v3';

const checkApiKey = () => {
if (!YOUTUBE_API_KEY) {
throw new Error('The YouTube API key has not been configured. Please configure your API');
}
};

export async function searchVideos(query) {
checkApiKey();

return axios.get(`${BASE_URL}/search`, {
params: {
part: 'snippet',
Expand All @@ -16,6 +24,8 @@ export async function searchVideos(query) {
}

export async function getPlaylistItems(playlistId) {
checkApiKey();

return axios.get(`${BASE_URL}/playlistItems`, {
params: {
part: 'snippet',
Expand All @@ -24,4 +34,4 @@ export async function getPlaylistItems(playlistId) {
playlistId
}
});
}
}
126 changes: 45 additions & 81 deletions src/components/LandingPage.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import { motion, useMotionTemplate, useMotionValue } from "framer-motion";
import { Link } from "react-router-dom";
import { useContext } from "react";
import { LanguageContext } from "../LanguageContext";

// Utils function included directly
function cn(...inputs) {
return inputs.filter(Boolean).join(' ');
return inputs.filter(Boolean).join(" ");
}

export const BackgroundLines = ({
children,
className,
svgOptions
}) => {
export const BackgroundLines = ({ children, className, svgOptions }) => {
return (
(<div
className={cn("h-[20rem] md:h-screen w-full bg-white dark:bg-black", className)}>
<div className={cn("h-[20rem] md:h-screen w-full bg-white dark:bg-black", className)}>
<SVG svgOptions={svgOptions} />
{children}
</div>)
</div>
);
};

Expand All @@ -29,9 +25,7 @@ const pathVariants = {
},
};

const SVG = ({
svgOptions
}) => {
const SVG = ({ svgOptions }) => {
const paths = [
"M720 450C720 450 742.459 440.315 755.249 425.626C768.039 410.937 778.88 418.741 789.478 401.499C800.076 384.258 817.06 389.269 826.741 380.436C836.423 371.603 851.957 364.826 863.182 356.242C874.408 347.657 877.993 342.678 898.867 333.214C919.741 323.75 923.618 319.88 934.875 310.177C946.133 300.474 960.784 300.837 970.584 287.701C980.384 274.564 993.538 273.334 1004.85 263.087C1016.15 252.84 1026.42 250.801 1038.22 242.1C1050.02 233.399 1065.19 230.418 1074.63 215.721C1084.07 201.024 1085.49 209.128 1112.65 194.884C1139.8 180.64 1132.49 178.205 1146.43 170.636C1160.37 163.066 1168.97 158.613 1181.46 147.982C1193.95 137.35 1191.16 131.382 1217.55 125.645C1243.93 119.907 1234.19 118.899 1254.53 100.846C1274.86 82.7922 1275.12 92.8914 1290.37 76.09C1305.62 59.2886 1313.91 62.1868 1323.19 56.7536C1332.48 51.3204 1347.93 42.8082 1361.95 32.1468C1375.96 21.4855 1374.06 25.168 1397.08 10.1863C1420.09 -4.79534 1421.41 -3.16992 1431.52 -15.0078",
"M720 450C720 450 741.044 435.759 753.062 410.636C765.079 385.514 770.541 386.148 782.73 370.489C794.918 354.83 799.378 353.188 811.338 332.597C823.298 312.005 825.578 306.419 843.707 295.493C861.837 284.568 856.194 273.248 877.376 256.48C898.558 239.713 887.536 227.843 909.648 214.958C931.759 202.073 925.133 188.092 941.063 177.621C956.994 167.151 952.171 154.663 971.197 135.041C990.222 115.418 990.785 109.375 999.488 96.1291C1008.19 82.8827 1011.4 82.2181 1032.65 61.8861C1053.9 41.5541 1045.74 48.0281 1064.01 19.5798C1082.29 -8.86844 1077.21 -3.89415 1093.7 -19.66C1110.18 -35.4258 1105.91 -46.1146 1127.68 -60.2834C1149.46 -74.4523 1144.37 -72.1024 1154.18 -97.6802C1163.99 -123.258 1165.6 -111.332 1186.21 -135.809C1206.81 -160.285 1203.29 -160.861 1220.31 -177.633C1237.33 -194.406 1236.97 -204.408 1250.42 -214.196",
Expand All @@ -51,7 +45,6 @@ const SVG = ({
"M720 450C720 450 711.596 475.85 701.025 516.114C690.455 556.378 697.124 559.466 689.441 579.079C681.758 598.693 679.099 597.524 675.382 642.732C671.665 687.94 663.4 677.024 657.844 700.179C652.288 723.333 651.086 724.914 636.904 764.536C622.723 804.158 631.218 802.853 625.414 827.056C619.611 851.259 613.734 856.28 605.94 892.262C598.146 928.244 595.403 924.314 588.884 957.785C582.364 991.255 583.079 991.176 575.561 1022.63C568.044 1054.08 566.807 1058.45 558.142 1084.32C549.476 1110.2 553.961 1129.13 542.367 1149.25C530.772 1169.37 538.268 1180.37 530.338 1207.27C522.407 1234.17 520.826 1245.53 512.156 1274.2",
"M720 450C720 450 730.571 424.312 761.424 411.44C792.277 398.569 772.385 393.283 804.069 377.232C835.752 361.182 829.975 361.373 848.987 342.782C867.999 324.192 877.583 330.096 890.892 303.897C904.201 277.698 910.277 282.253 937.396 264.293C964.514 246.333 949.357 246.834 978.7 230.438C1008.04 214.042 990.424 217.952 1021.51 193.853C1052.6 169.753 1054.28 184.725 1065.97 158.075C1077.65 131.425 1087.76 139.068 1111.12 120.345C1134.49 101.622 1124.9 104.858 1151.67 86.3162C1178.43 67.7741 1167.09 66.2676 1197.53 47.2606C1227.96 28.2536 1225.78 23.2186 1239.27 12.9649C1252.76 2.7112 1269.32 -9.47929 1282.88 -28.5587C1296.44 -47.6381 1305.81 -41.3853 1323.82 -62.7027C1341.83 -84.0202 1340.32 -82.3794 1368.98 -98.9326",
];

const colors = [
"#46A5CA",
"#8C2F2F",
Expand All @@ -76,16 +69,18 @@ const SVG = ({
"#604483",
];
return (
(<motion.svg
<motion.svg
viewBox="0 0 1440 900"
fill="none"
xmlns="http://www.w3.org/2000/svg"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1 }}
className="absolute inset-0 w-full h-full">
className="absolute inset-0 w-full h-full"
>
{paths.map((path, idx) => (
<motion.path
key={`path-first-${idx}`}
d={path}
stroke={colors[idx]}
strokeWidth="2.3"
Expand All @@ -101,11 +96,11 @@ const SVG = ({
delay: Math.floor(Math.random() * 10),
repeatDelay: Math.floor(Math.random() * 10 + 2),
}}
key={`path-first-${idx}`} />
/>
))}
{/* duplicate for more paths */}
{paths.map((path, idx) => (
<motion.path
key={`path-second-${idx}`}
d={path}
stroke={colors[idx]}
strokeWidth="2.3"
Expand All @@ -121,42 +116,30 @@ const SVG = ({
delay: Math.floor(Math.random() * 10),
repeatDelay: Math.floor(Math.random() * 10 + 2),
}}
key={`path-second-${idx}`} />
/>
))}
</motion.svg>)
</motion.svg>
);
};

export const SpotlightCard = ({
children,
className,
containerClassName
}) => {
export const SpotlightCard = ({ children, className, containerClassName }) => {
let mouseX = useMotionValue(0);
let mouseY = useMotionValue(0);

function handleMouseMove({
currentTarget,
clientX,
clientY
}) {
function handleMouseMove({ currentTarget, clientX, clientY }) {
if (!currentTarget) return;
let { left, top } = currentTarget.getBoundingClientRect();

mouseX.set(clientX - left);
mouseY.set(clientY - top);
}
return (
(<div
className={cn(
"relative h-[40rem] flex items-center bg-white dark:bg-black justify-center w-full group",
containerClassName
)}
onMouseMove={handleMouseMove}>
<div
className="absolute inset-0 bg-dot-thick-neutral-300 dark:bg-dot-thick-neutral-800 pointer-events-none" />
<div
className={cn("relative h-[40rem] flex items-center bg-white dark:bg-black justify-center w-full group", containerClassName)}
onMouseMove={handleMouseMove}
>
<div className="absolute inset-0 bg-dot-thick-neutral-300 dark:bg-dot-thick-neutral-800 pointer-events-none" />
<motion.div
className="pointer-events-none bg-dot-thick-indigo-500 dark:bg-dot-thick-indigo-500 absolute inset-0 opacity-0 transition duration-300 group-hover:opacity-100"
className="pointer-events-none bg-dot-thick-indigo-500 dark:bg-dot-thick-indigo-500 absolute inset-0 opacity-0 transition duration-300 group-hover:opacity-100"
style={{
WebkitMaskImage: useMotionTemplate`
radial-gradient(
Expand All @@ -172,29 +155,19 @@ export const SpotlightCard = ({
transparent 100%
)
`,
}} />
}}
/>
<div className={cn("relative z-20", className)}>{children}</div>
</div>)
</div>
);
};

export const Highlight = ({
children,
className
}) => {
export const Highlight = ({ children, className }) => {
return (
(<motion.span
initial={{
backgroundSize: "0% 100%",
}}
animate={{
backgroundSize: "100% 100%",
}}
transition={{
duration: 0.7,
ease: "linear",
delay: 0.5,
}}
<motion.span
initial={{ backgroundSize: "0% 100%" }}
animate={{ backgroundSize: "100% 100%" }}
transition={{ duration: 0.7, ease: "linear", delay: 0.5 }}
style={{
backgroundRepeat: "no-repeat",
backgroundPosition: "left center",
Expand All @@ -203,13 +176,16 @@ export const Highlight = ({
className={cn(
`relative inline-block pb-1 px-1 rounded-lg bg-gradient-to-r from-indigo-300 to-purple-300 dark:from-indigo-500 dark:to-purple-500`,
className
)}>
)}
>
{children}
</motion.span>)
</motion.span>
);
};

export default function LandingPage() {
const { language, translations } = useContext(LanguageContext);

return (
<BackgroundLines>
<div className="relative z-10 min-h-screen flex flex-col items-center justify-center px-4">
Expand All @@ -221,7 +197,7 @@ export default function LandingPage() {
>
<h1 className="text-6xl font-bold mb-6">
<Highlight>Distraction Free</Highlight>{" "}
<motion.span
<motion.span
className="text-[#a1a1a1] inline-block"
initial={{ y: -100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
Expand All @@ -230,41 +206,29 @@ export default function LandingPage() {
damping: 8,
stiffness: 100,
duration: 15,
delay: 1.5
delay: 1.5,
}}
>
Youtube Player
{translations[language].landing.headline}
</motion.span>
</h1>
<motion.p
<motion.p
className="text-xl mb-12 text-gray-600 dark:text-gray-300"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.8,
delay: 2.0
}}
transition={{ duration: 0.8, delay: 2.0 }}
>
A curated collection of programming tutorials without the distractions of regular YouTube.
{translations[language].landing.description}
</motion.p>

<motion.div
initial={{ opacity: 0, y: 100 }}
animate={{ opacity: 1, y: 0 }}
transition={{
type: "spring",
damping: 12,
stiffness: 100,
delay: 2.3
}}
transition={{ type: "spring", damping: 12, stiffness: 100, delay: 2.3 }}
>
<Link
to="/learn"
className="p-[3px] relative inline-block"
>
<Link to="/learn" className="p-[3px] relative inline-block">
<div className="absolute inset-0 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-lg" />
<div className="px-8 py-4 bg-black rounded-[6px] relative group transition duration-200 text-white hover:bg-transparent">
Get Started
{translations[language].landing.getStarted}
</div>
</Link>
</motion.div>
Expand Down
Loading