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
Binary file added frontend/bun.lockb
Binary file not shown.
4 changes: 3 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"lucide-react": "^0.474.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^4.0.3"
"tailwind-merge": "^3.0.1",
"tailwindcss": "^4.0.3",
"use-debounce": "^10.0.4"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
Expand Down
14 changes: 14 additions & 0 deletions frontend/public/wikipedia.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 53 additions & 41 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,74 @@
import { useEffect, useRef, useCallback, useState } from 'react'
import { WikiCard } from './components/WikiCard'
import { useWikiArticles } from './hooks/useWikiArticles'
import { Loader2 } from 'lucide-react'
import { Analytics } from "@vercel/analytics/react"
import { LanguageSelector } from './components/LanguageSelector'
import { useEffect, useRef, useCallback, useState } from "react";
import { WikiCard } from "./components/WikiCard";
import { useWikiArticles } from "./hooks/useWikiArticles";
import { Loader2 } from "lucide-react";
import { Analytics } from "@vercel/analytics/react";
import { LanguageSelector } from "./components/LanguageSelector";
import Search from "./components/search";

function App() {
const [showAbout, setShowAbout] = useState(false)
const { articles, loading, fetchArticles } = useWikiArticles()
const observerTarget = useRef(null)
const [showAbout, setShowAbout] = useState(false);
const { articles, loading, fetchArticles } = useWikiArticles();
const observerTarget = useRef(null);

const handleObserver = useCallback(
(entries: IntersectionObserverEntry[]) => {
const [target] = entries
const [target] = entries;
if (target.isIntersecting && !loading) {
fetchArticles()
fetchArticles();
}
},
[loading, fetchArticles]
)
);

useEffect(() => {
const observer = new IntersectionObserver(handleObserver, {
threshold: 0.1,
rootMargin: '100px',
})
rootMargin: "100px",
});

if (observerTarget.current) {
observer.observe(observerTarget.current)
observer.observe(observerTarget.current);
}

return () => observer.disconnect()
}, [handleObserver])
return () => observer.disconnect();
}, [handleObserver]);

useEffect(() => {
fetchArticles()
}, [])
fetchArticles();
}, []);

return (
<div className="h-screen w-full bg-black text-white overflow-y-scroll snap-y snap-mandatory">
<div className="fixed top-4 left-4 z-50">
<button
onClick={() => window.location.reload()}
className="text-2xl font-bold text-white drop-shadow-lg hover:opacity-80 transition-opacity"
>
WikiTok
</button>
</div>

<div className="fixed top-4 right-4 z-50 flex flex-col items-end gap-2">
<button
onClick={() => setShowAbout(!showAbout)}
className="text-sm text-white/70 hover:text-white transition-colors"
>
About
</button>
<LanguageSelector />
</div>
<header className="fixed top-0 w-full z-50 py-6 p-2 ">
<div className="flex flex-col gap-2">
<div className="container mx-auto flex items-center justify-between">
<div className="logo">
<button
onClick={() => window.location.reload()}
className="text-2xl font-bold text-white drop-shadow-lg hover:opacity-80 transition-opacity"
>
WikiTok
</button>
</div>
<div className="hidden md:block">
<Search />
</div>
<div className="flex flex-col items-end gap-2">
<button
onClick={() => setShowAbout(!showAbout)}
className="text-sm text-white/70 hover:text-white transition-colors"
>
About
</button>
<LanguageSelector />
</div>
</div>
<div className="mt-6 md:hidden">
<Search />
</div>
</div>
</header>

{showAbout && (
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
Expand All @@ -72,7 +84,7 @@ function App() {
A TikTok-style interface for exploring random Wikipedia articles.
</p>
<p className="text-white/70">
Made with ❤️ by{' '}
Made with ❤️ by{" "}
<a
href="https://x.com/Aizkmusic"
target="_blank"
Expand All @@ -83,7 +95,7 @@ function App() {
</a>
</p>
<p className="text-white/70 mt-2">
Check out the code on{' '}
Check out the code on{" "}
<a
href="https://github.com/IsaacGemal/wikitok"
target="_blank"
Expand All @@ -109,7 +121,7 @@ function App() {
)}
<Analytics />
</div>
)
);
}

export default App
export default App;
2 changes: 1 addition & 1 deletion frontend/src/components/WikiCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,4 @@ export function WikiCard({ article }: WikiCardProps) {
</div>
</div>
);
}
}
165 changes: 165 additions & 0 deletions frontend/src/components/search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { Link2Icon, SearchIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { twMerge } from "tailwind-merge";
import { useDebounce } from "use-debounce";
import { useLocalization } from "../hooks/useLocalization";

type WikiData = {
title: string;
description: string;
link: string;
image: string;
};

export default function Search() {
const { currentLanguage } = useLocalization();
const [query, setQuery] = useState("");
const [suggestions, setSuggestions] = useState<WikiData[]>([]);
const [debounceQuery] = useDebounce(query, 800);
const [placeholder, setPlaceHolder] = useState("");
const intervalId = useRef(0);
const [isFocused, setIsFocused] = useState(false);

const handleChange = (searchText: string) => {
setQuery(searchText);
};

useEffect(() => {
if (debounceQuery.length == 0) return;
let ignore = false;
setSuggestions([]);

const url = `${currentLanguage.api}${new URLSearchParams({
action: "query",
format: "json",
generator: "prefixsearch",
gpssearch: debounceQuery,
prop: "pageimages|description|info",
piprop: "thumbnail",
pithumbsize: "100",
inprop: "url",
origin: "*",
})}`;

fetch(url)
.then((res) => res.json())
.then((wikiData) => {
if (!wikiData.query?.pages) return [];
if (!ignore) {
const formattedData = Object.values(wikiData.query.pages).map(
(page: any) => ({
title: page.title,
description: page.description,
image: page.thumbnail?.source || null,
link:
page.fullurl ||
`https://en.wikipedia.org/wiki/${encodeURIComponent(
page.title
)}`,
})
);
setSuggestions(formattedData);
}
})
.catch((error) => {
console.error("Error fetching Wikipedia suggestions:", error);
return [];
});

return () => {
ignore = true;
};
}, [debounceQuery, currentLanguage.api]);

useEffect(() => {
const placeholders = [
"Famous People...",
"Famous Places...",
"Popular Dishes...",
"History...",
];
intervalId.current = setInterval(() => {
setPlaceHolder(
placeholders[Math.floor(Math.random() * placeholders.length)]
);
console.log(placeholder);
}, 10000);
return () => {
clearInterval(intervalId.current);
};
}, [placeholder]);
return (
<div className="">
<form className="relative" id="search-form" action="">
<div className="flex gap-2 justify-between items-center">
<div
className={`searchbar_container flex gap-2 items-center justify-between border rounded-4xl p-2 w-xl ${
isFocused && ""
}`}
>
<input
type="text"
autoComplete="off"
name="query"
value={query}
id="query"
className={twMerge(
`grow focus:border-0 focus:outline-0 h-full ml-2 transition-all duration-300 placeholder:mb-2 ${
placeholder
? "placeholder:opacity-90"
: "placeholder:opacity-0"
}`
)}
placeholder={placeholder}
onChange={(e) => handleChange(e.target.value)}
onFocus={() => setIsFocused(true)}
/>
<SearchIcon />
</div>
<button
type="submit"
className={twMerge(
"opacity-0 duration-300 transition-opacity hidden md:inline-block",
query && "opacity-100"
)}
>
Search
</button>
</div>
{query && !!suggestions && (
<ul
className={twMerge(
`bg-black/30 backdrop-blur-sm max-w-xl rounded-4xl absolute top-12 left-0 w-full transition-all duration-300 overflow-y-scroll`,
query ? "max-h-[500px] opacity-100" : "max-h-0 opacity-0"
)}
>
{suggestions.map((suggestion) => (
<li key={suggestion.link} className="border-y border-white/10">
<a href={suggestion.link} target="_blank">
<div className="flex items-center gap-3">
<div className="image size-24 flex items-center justify-center overflow-hidden">
<img
src={suggestion.image || "wikipedia.svg"}
alt={`Picture of ${suggestion.title}`}
className="object-cover size-[5.5rem]"
/>
</div>
<div className="desc flex justify-between items-center grow pr-6">
<div className="">
<p className="title text-xl font-semibold">
{suggestion.title}
</p>
<p className="description">{suggestion.description}</p>
</div>
<Link2Icon className="opacity-80" />
</div>
</div>
</a>
</li>
))}
</ul>
)}
</form>
</div>
);
}
2 changes: 1 addition & 1 deletion frontend/src/hooks/useWikiArticles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function useWikiArticles() {
const [articles, setArticles] = useState<WikiArticle[]>([]);
const [loading, setLoading] = useState(false);
const [buffer, setBuffer] = useState<WikiArticle[]>([]);
const {currentLanguage} = useLocalization()
const { currentLanguage } = useLocalization();

const fetchArticles = async (forBuffer = false) => {
if (loading) return;
Expand Down
Empty file added frontend/src/test.ts
Empty file.
10 changes: 10 additions & 0 deletions frontend/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.ts", "./**/*.html"],
theme: {
container: {
center: true,
},
},
plugins: [],
};