diff --git a/client/index.html b/client/index.html index 7d50ae35..5074e722 100644 --- a/client/index.html +++ b/client/index.html @@ -10,7 +10,6 @@ /> - diff --git a/client/public/innovator.woff2 b/client/public/innovator.woff2 new file mode 100644 index 00000000..54849d43 Binary files /dev/null and b/client/public/innovator.woff2 differ diff --git a/client/src/components/quiz-card.tsx b/client/src/components/quiz-card.tsx index ff90edb1..f765bc73 100644 --- a/client/src/components/quiz-card.tsx +++ b/client/src/components/quiz-card.tsx @@ -23,6 +23,7 @@ import { getRelativeTime } from "../utils"; type QuizCardProps = { quiz: Quiz; onTagClick: (tag: string) => void; + key: string; }; const difficultyText = cva([ @@ -59,9 +60,10 @@ const scoreClass = cva( const scoreToInteger = (score: number) => Math.floor(score) as 0 | 1 | 2 | 3 | 4; -export const QuizCard = ({ quiz, onTagClick }: QuizCardProps) => { +export const QuizCard = ({ quiz, onTagClick, key }: QuizCardProps) => { return (
@@ -84,14 +86,15 @@ export const QuizCard = ({ quiz, onTagClick }: QuizCardProps) => { {quiz.rating.score.toFixed(1)} - + } > diff --git a/client/src/mocks/mocks.quiz.ts b/client/src/mocks/mocks.quiz.ts index 67d2f760..1ca84764 100644 --- a/client/src/mocks/mocks.quiz.ts +++ b/client/src/mocks/mocks.quiz.ts @@ -25,7 +25,7 @@ export const quizOverviews: QuizOverview[] = [ }, tags: [ { - id: "471", + id: "4571", name: "human", }, { @@ -99,7 +99,7 @@ export const quizOverviews: QuizOverview[] = [ name: "seasons", }, { - id: "9879", + id: "98791", name: "fun", }, { diff --git a/client/src/routes/Home.tsx b/client/src/routes/Home.tsx index b0438cbe..8390b123 100644 --- a/client/src/routes/Home.tsx +++ b/client/src/routes/Home.tsx @@ -1,5 +1,267 @@ +import { Separator } from "@ariakit/react"; +import { + RiArrowDownSLine, + RiArrowUpSLine, + RiBookReadLine, + RiCodeLine, + RiTranslate2, +} from "@remixicon/react"; +import { ReactNode, useState } from "react"; import { PageHead } from "../components/page-head"; +const ExpandableItem = ({ + title, + children, +}: { + title: string; + children: ReactNode; +}) => { + const [isExpanded, setIsExpanded] = useState(false); + return ( +
+ + {isExpanded &&
{children}
} +
+ ); +}; + +const confusedWordsData = [ + { + title: "Affect vs. Effect", + content: [ + { + label: "Affect (verb):", + description: "To influence or make a difference to.", + partOfSpeech: "verb", + }, + { + label: "Affect (noun):", + description: "An emotion or desire as influencing behavior.", + partOfSpeech: "noun", + }, + { + label: "Effect (noun):", + description: "A result or consequence.", + partOfSpeech: "noun", + }, + { + label: "Effect (verb):", + description: "To bring about or cause to happen.", + partOfSpeech: "verb", + }, + { + example: + "The weather affects my mood. / The effect of the weather on my mood is noticeable.", + }, + { + example: + "The medication may affect your coordination. / The side effects of the medication include drowsiness.", + }, + ], + }, + { + title: "Their vs. There vs. They're", + content: [ + { + label: "Their:", + description: "Possessive pronoun", + partOfSpeech: "pronoun", + }, + { + label: "There:", + description: "Indicating location or existence", + partOfSpeech: "adverb", + }, + { + label: "They're:", + description: 'Contraction of "they are"', + partOfSpeech: "contraction", + }, + { example: "They're going to their house over there." }, + { + example: + "Their car is parked over there. They're planning to sell it soon.", + }, + ], + }, + { + title: "Its vs. It's", + content: [ + { + label: "Its:", + description: "Possessive pronoun", + partOfSpeech: "pronoun", + }, + { + label: "It's:", + description: 'Contraction of "it is" or "it has"', + partOfSpeech: "contraction", + }, + { example: "It's important to know its meaning." }, + { + example: + "The dog wagged its tail. It's been a long day for the pup.", + }, + ], + }, + { + title: "Your vs. You're", + content: [ + { + label: "Your:", + description: "Possessive pronoun", + partOfSpeech: "pronoun", + }, + { + label: "You're:", + description: 'Contraction of "you are"', + partOfSpeech: "contraction", + }, + { + example: + "Your book is on the table. You're going to need it for class.", + }, + { example: "You're responsible for your own actions." }, + ], + }, + { + title: "To vs. Too vs. Two", + content: [ + { + label: "To:", + description: "Preposition indicating direction or recipient", + partOfSpeech: "preposition", + }, + { + label: "Too:", + description: "Also or excessively", + partOfSpeech: "adverb", + }, + { + label: "Two:", + description: "The number 2", + partOfSpeech: "noun", + }, + { + example: + "I'm going to the store to buy two apples. Do you want to come too?", + }, + { example: "The coffee is too hot to drink right now." }, + ], + }, +]; + +const dailyWordSuggestionsData = [ + { + title: "Hery", + definition: "To worship or praise", + turkish: "Övmek", + example: "The ancient text heried the deeds of the hero.", + partOfSpeech: "verb", + etymology: + "From Middle English herien, from Old English herian, herġan ('to praise, glorify').", + }, + { + title: "Hallow", + definition: "To make holy or sacred", + turkish: "Kutsallaştırmak", + example: "The ground was hallowed by the ceremony.", + partOfSpeech: "verb", + etymology: + "From Middle English halwen, from Old English hālgian ('to make holy').", + }, + { + title: "Fiend", + definition: "An evil spirit or demon", + turkish: "Şeytan", + example: "The story depicted a fiend terrorizing the village.", + partOfSpeech: "noun", + etymology: + "From Middle English feend, from Old English fēond ('enemy, devil, demon').", + }, + { + title: "Ephemeral", + definition: "Lasting for a very short time", + turkish: "Geçici", + example: + "The beauty of cherry blossoms is ephemeral, lasting only a few days.", + partOfSpeech: "adjective", + etymology: + "From Greek ephēmeros ('lasting only one day, short-lived').", + }, + { + title: "Serendipity", + definition: + "The occurrence of events by chance in a happy or beneficial way", + turkish: "Şans eseri", + example: "Finding his lost wallet was a moment of serendipity.", + partOfSpeech: "noun", + etymology: + "Coined by Horace Walpole in 1754 based on the Persian fairy tale 'The Three Princes of Serendip'.", + }, +]; + +const technicalDefinitionsData = [ + { + title: "API", + fullForm: "Application Programming Interface", + definition: + "A set of protocols and tools for building software applications", + example: + "The weather app uses an API to fetch current temperature data.", + categories: ["Software Development", "Web Development"], + relatedTerms: ["REST", "SOAP", "GraphQL"], + }, + { + title: "ORM", + fullForm: "Object-Relational Mapping", + definition: + "A technique for converting data between incompatible type systems in databases and object-oriented programming languages", + example: + "Using an ORM can simplify database operations in your application.", + categories: ["Database", "Software Development"], + relatedTerms: ["SQL", "Database Schema", "Hibernate"], + }, + { + title: "JWT", + fullForm: "JSON Web Token", + definition: + "A compact, URL-safe means of representing claims to be transferred between two parties", + example: + "JWTs are commonly used for authentication in web applications.", + categories: ["Web Security", "Authentication"], + relatedTerms: ["OAuth", "Bearer Token", "JSON"], + }, + { + title: "DNS", + fullForm: "Domain Name System", + definition: + "A hierarchical and decentralized naming system for computers, services, or other resources connected to the Internet or a private network", + example: "DNS translates human-readable domain names to IP addresses.", + categories: ["Networking", "Internet Infrastructure"], + relatedTerms: ["IP Address", "ICANN", "Domain Registrar"], + }, + { + title: "CORS", + fullForm: "Cross-Origin Resource Sharing", + definition: + "A mechanism that allows restricted resources on a web page to be requested from another domain outside the domain from which the first resource was served", + example: + "CORS policies help prevent unauthorized access to APIs from different domains.", + categories: ["Web Security", "Web Development"], + relatedTerms: [ + "Same-Origin Policy", + "Preflight Request", + "XMLHttpRequest", + ], + }, +]; export const Home = () => { return (
@@ -9,6 +271,77 @@ export const Home = () => { English. You can take quizzes and use forums to improve your English." /> +
+
+

+ + Frequently Confused Words +

+ + {confusedWordsData.map((item, index) => ( + + {item.content.map((contentItem, contentIndex) => ( +

+ {contentItem.label && ( + {contentItem.label} + )}{" "} + {contentItem.description} + {contentItem.example && ( + <>{contentItem.example} + )} +

+ ))} +
+ ))} +
+
+

+ + Daily Word Suggestions +

+ + {dailyWordSuggestionsData.map((item, index) => ( + +

+ Definition: {item.definition} +

+

+ Turkish: {item.turkish} +

+

+ {item.example} +

+
+ ))} +
+
+

+ + Technical Definitions +

+ + {technicalDefinitionsData.map((item, index) => ( + +

+ Full form: {item.fullForm} +

+

+ Definition: {item.definition} +

+

+ {item.example} +

+
+ ))} +
+
); }; diff --git a/client/src/routes/Leaderboard.tsx b/client/src/routes/Leaderboard.tsx index 9cd41107..bab0278a 100644 --- a/client/src/routes/Leaderboard.tsx +++ b/client/src/routes/Leaderboard.tsx @@ -9,47 +9,92 @@ export const Leaderboard = () => { { rank: 1, player: { - full_name: "Ümit Can Evleksiz", - username: "umitev_07", - avatar: "https://randomuser.me/api/portraits/men/2.jpg", + full_name: "Arnold Jones", + username: "cute_mittens", + avatar: "https://randomuser.me/api/portraits/men/13.jpg", }, - points: 1000, + points: 1052, }, { rank: 2, player: { - full_name: "Ümit Can Evleksiz", - username: "umitev_07", - avatar: "https://randomuser.me/api/portraits/men/2.jpg", + full_name: "Boby Carlsen", + username: "board_walker", + avatar: "https://randomuser.me/api/portraits/men/57.jpg", }, - points: 950, + points: 987, }, { rank: 3, player: { - full_name: "Ümit Can Evleksiz", - username: "umitev_07", - avatar: "https://randomuser.me/api/portraits/men/2.jpg", + full_name: "Winston Jobs", + username: "whether_dweller", + avatar: "https://randomuser.me/api/portraits/women/13.jpg", }, - points: 900, + points: 943, }, { rank: 4, player: { - full_name: "Ümit Can Evleksiz", - username: "umitev_07", - avatar: "https://randomuser.me/api/portraits/men/2.jpg", + full_name: "Tuana Ümraniyeli", + username: "tt_world", + avatar: "https://randomuser.me/api/portraits/women/7.jpg", }, - points: 850, + points: 891, }, { rank: 5, + player: { + full_name: "Heisenberg Cook", + username: "the_cook06", + avatar: "https://randomuser.me/api/portraits/men/44.jpg", + }, + points: 856, + }, + { + rank: 6, + player: { + full_name: "Hasan Kerem Şeker", + username: "kerem_s54", + avatar: "https://randomuser.me/api/portraits/men/46.jpg", + }, + points: 812, + }, + { + rank: 7, player: { full_name: "Ümit Can Evleksiz", username: "umitev_07", avatar: "https://randomuser.me/api/portraits/men/2.jpg", }, - points: 800, + points: 779, + }, + { + rank: 8, + player: { + full_name: "Kemal Kaya", + username: "kemal_k_2023", + avatar: "https://randomuser.me/api/portraits/men/3.jpg", + }, + points: 745, + }, + { + rank: 9, + player: { + full_name: "Ayşe Kabakçı", + username: "kabakci_a24", + avatar: "https://randomuser.me/api/portraits/women/31.jpg", + }, + points: 702, + }, + { + rank: 10, + player: { + full_name: "Emre Kaya", + username: "emrek_lit", + avatar: "https://randomuser.me/api/portraits/men/16.jpg", + }, + points: 678, }, ]; @@ -57,47 +102,92 @@ export const Leaderboard = () => { { rank: 1, player: { - full_name: "Ümit Can Evleksiz", - username: "umitev_07", - avatar: "https://randomuser.me/api/portraits/men/2.jpg", + full_name: "Selin Demir", + username: "demir_sel2021", + avatar: "https://randomuser.me/api/portraits/women/16.jpg", }, - points: 500, + points: 523, }, { rank: 2, player: { - full_name: "Ümit Can Evleksiz", - username: "umitev_07", - avatar: "https://randomuser.me/api/portraits/men/2.jpg", + full_name: "Ali Yıldız", + username: "yildiz_ali15", + avatar: "https://randomuser.me/api/portraits/men/17.jpg", }, - points: 450, + points: 487, }, { rank: 3, player: { - full_name: "Ümit Can Evleksiz", - username: "umitev_07", - avatar: "https://randomuser.me/api/portraits/men/2.jpg", + full_name: "Zeynep Aksoy", + username: "aksoy_z14", + avatar: "https://randomuser.me/api/portraits/women/17.jpg", }, - points: 400, + points: 452, }, { rank: 4, player: { - full_name: "Ümit Can Evleksiz", - username: "umitev_07", - avatar: "https://randomuser.me/api/portraits/men/2.jpg", + full_name: "Mehmet Öztürk", + username: "mehmet_philos27", + avatar: "https://randomuser.me/api/portraits/men/18.jpg", }, - points: 350, + points: 418, }, { rank: 5, player: { - full_name: "Ümit Can Evleksiz", - username: "umitev_07", - avatar: "https://randomuser.me/api/portraits/men/2.jpg", + full_name: "Ayşe Yılmaz", + username: "aysey_90", + avatar: "https://randomuser.me/api/portraits/women/18.jpg", }, - points: 300, + points: 389, + }, + { + rank: 6, + player: { + full_name: "Cem Kaya", + username: "cemk", + avatar: "https://randomuser.me/api/portraits/men/19.jpg", + }, + points: 356, + }, + { + rank: 7, + player: { + full_name: "Elif Demir", + username: "elifd", + avatar: "https://randomuser.me/api/portraits/women/19.jpg", + }, + points: 327, + }, + { + rank: 8, + player: { + full_name: "Ahmet Yıldırım", + username: "ahmety", + avatar: "https://randomuser.me/api/portraits/men/20.jpg", + }, + points: 301, + }, + { + rank: 9, + player: { + full_name: "Zehra Çelik", + username: "zehrac", + avatar: "https://randomuser.me/api/portraits/women/20.jpg", + }, + points: 278, + }, + { + rank: 10, + player: { + full_name: "Mustafa Aydın", + username: "mustafaa", + avatar: "https://randomuser.me/api/portraits/men/21.jpg", + }, + points: 254, }, ]; @@ -125,7 +215,7 @@ export const Leaderboard = () => { className="sr-only" /> {
- - - - + + + {currentLeaderboard.map((player) => ( - - + - - diff --git a/client/src/routes/Quiz.data.tsx b/client/src/routes/Quiz.data.tsx index ba241b67..056dfce0 100644 --- a/client/src/routes/Quiz.data.tsx +++ b/client/src/routes/Quiz.data.tsx @@ -1,7 +1,7 @@ import { LoaderFunction } from "react-router"; import { safeParse } from "valibot"; import { quizDetailsSchema } from "../types/quiz"; -import { BASE_URL } from "../utils"; +import { BASE_URL, logger } from "../utils"; export const quizLoader = (async ({ params }) => { const { quizId } = params; @@ -22,7 +22,7 @@ export const quizLoader = (async ({ params }) => { } const data = await res.json(); - console.log(data); + logger.log(data); const { output, issues, success } = safeParse(quizDetailsSchema, data); if (!success) { throw new Error(`Failed to parse quiz response: ${issues}`); diff --git a/client/src/routes/Quiz.tsx b/client/src/routes/Quiz.tsx index d34fa9a1..64bbea60 100644 --- a/client/src/routes/Quiz.tsx +++ b/client/src/routes/Quiz.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useLoaderData } from "react-router-typesafe"; import { buttonClass, buttonInnerRing } from "../components/button"; import { PageHead } from "../components/page-head"; @@ -49,6 +49,10 @@ export const QuizPage = () => { const [isQuizStarted, setIsQuizStarted] = useState(false); const [timeRemaining, setTimeRemaining] = useState(600); // 10 minutes in seconds + const ref = useRef(null); + + // focus on the question when the current question changes + useEffect(() => { const savedAnswers = localStorage.getItem(`quiz_${quiz.id}_answers`); if (savedAnswers) { @@ -120,7 +124,8 @@ export const QuizPage = () => {
- { className="font-display text-lg font-medium tracking-tight" > {quiz.questions[currentQuestion].text} - +
{quiz.questions[currentQuestion] && (
@@ -148,7 +153,7 @@ export const QuizPage = () => { : "bg-slate-100 text-slate-950 hover:bg-slate-200" }`} aria-describedby="question" - lang="tr" + lang={quiz.type === 2 ? "tr" : "en"} > { const data = useLoaderData(); + const homeData = useRouteLoaderData("/"); + logger.log(homeData); const [searchTerm, setSearchTerm] = useState(""); const [selectedTagId, setSelectedTagId] = useState(null); const [sortBy, setSortBy] = useState("newest"); @@ -132,8 +135,8 @@ export const Quizzes = () => {
{filteredQuizzes.map((quiz) => ( setSelectedTagId(tag)} key={quiz.id} + onTagClick={(tag) => setSelectedTagId(tag)} quiz={quiz} /> ))} diff --git a/client/src/utils.tsx b/client/src/utils.tsx index 18c41f32..8ccd72e4 100644 --- a/client/src/utils.tsx +++ b/client/src/utils.tsx @@ -10,12 +10,6 @@ export async function enableMocking() { return worker.start(); } -/*export const logger: typeof console.log = (...params) => { - if (import.meta.env.VITE_LOGGING === "true") { - console.log(...params); - } -};*/ - type Logger = { log: typeof console.log; error: typeof console.error;
RankName - {leaderboardType === "quiz" ? "points" : "Points"} +
+ Rank + + Name + + Turq Points
+
{player.rank} -
- -
- {player.player.full_name} +
+
+ +
+ + {player.player.full_name} +

- {player.player.username} + @{player.player.username}

+ {player.rank === 1 && ( + + 🥇{" "} + + )} + {player.rank === 2 && ( + + 🥈{" "} + + )} + {player.rank === 3 && ( + + 🥉{" "} + + )}
+ {player.points}