Skip to content

DraymeM/tiomi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

195 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Tiomi Logo

Tiomi โ€“ Interactive Topic Learning App

Tiomi is an installable, offline-ready Progressive Web App (PWA) that helps learners organize and actively recall knowledge through structured notes, flashcards, and gamified quizzes. Designed for everything from academic study to self-improvement, it brings the best parts of Notion, Quizlet, and Anki โ€” all in one app.

๐Ÿ”— Live Demo: danielmarkus.web.elte.hu/tetelekzv


๐Ÿ“Œ Highlights

  • ๐Ÿ’พ Offline-first: Caching and React Query ensure content works even without internet
  • ๐Ÿ“Š Dashboard: Live animated progress stats with CountUp.js
  • ๐Ÿ—ฃ๏ธ Text-to-speech reader with adjustable speed, pitch, and multiple voices; shows reading time per topic
  • ๐Ÿ“ฑ Installable as a native-like app on mobile and desktop
  • ๐ŸŒ“ Light/Dark Mode toggle based on system or user preference
  • โœ๏ธ Markdown editor with syntax highlighting and custom styling
  • ๐Ÿง  Flashcard system with spaced repetition and difficulty-based prioritization
  • ๐Ÿงญ Step-by-step tutorials for key features (modular, handles resize & interaction-required fallback)
  • ๐ŸŽฎ Gamified quiz engine with streaks, progress %, feedback messages with icons, and optional timer mode
  • ๐Ÿ‘ฅ Authentication & role-based access:
    • Guests: read-only
    • Users: create, read, edit
    • Superusers: full CRUD access
    • Collaborative platform, open content like Wikipedia (future group fragmentation & permissions)
  • ๐Ÿ” Backend rate limiting to prevent misuse with toast notifications
  • ๐Ÿงช Unit/component testing via ViteTest
  • โš™๏ธ Minimal PHP backend with custom-built ORM (due to shared hosting)

๐Ÿงฉ Tech Stack

Layer Technologies
Frontend React (Vite), TypeScript, TanStack Router
UI & Styling TailwindCSS, Headless UI, Toastify
Data & Schema React Query, Zod
Markdown react-markdown, rehype-highlight
Backend Vanilla PHP, custom ORM, MySQL
Auth Session-based with role handling
Testing ViteTest
PWA Support vite-plugin-pwa, Service Workers

๐Ÿฅ‡ Why Tiomi?

Unlike Anki, Quizlet, or Notion, Tiomi is:

  • โœ๏ธ Focused on structured, rich-markdown notes for academic/technical use
  • ๐Ÿง  Designed for contextual flashcards linked to content
  • ๐ŸŽฎ Gamified for active recall via quizzes
  • ๐Ÿงฉ Built around nested topic organization
  • ๐ŸŒ Fully offline-ready using IndexedDB
  • ๐Ÿงญ Includes interactive tutorials for onboarding

๐Ÿš€ Getting Started

๐Ÿ”ง Clone & Setup

git clone https://github.com/DraymeM/tetelekzv.git
cd tetelekzv

๐Ÿง‘โ€๐Ÿ’ป Frontend

npm install
npm run dev

๐Ÿ› ๏ธ Backend (PHP)

cd BackEnd
php -S localhost:8000

โš™๏ธ Configure dev.env.php and env.php with your DB credentials.


โœจ Features in Detail

๐Ÿ”ฅ PWA Capabilities

  • Installable on mobile/desktop
  • Works offline via:
    • IndexedDB caching of API responses
    • Manual critical resource caching
  • Built using vite-plugin-pwa and service workers

๐Ÿ’พ Offline Data Enhancements

Tiomi now includes more robust handling of IndexedDB via idb-keyval:

import { get, set } from "idb-keyval";

// Example: Cache user quiz state
const cacheQuizState = async (key: string, state: QuizState) => {
  await set(key, state);
};

const restoreQuizState = async (key: string): Promise<QuizState | undefined> => {
  return await get(key);
};
  • ๐Ÿง  Used for persistent quiz progress, flashcard ratings, and user settings.

  • โš™๏ธ IndexedDB acts as a local cache layer with fallbacks for offline-first experience.

  • ๐Ÿš€ Enables resume-where-you-left-off functionality in offline mode.

  • ๐Ÿ“ค Automatic sync when connection is restored (planned).


๐Ÿ—ฃ๏ธ Text-to-Speech Reader

Tiomiโ€™s TTS leverages the browser-native Web Speech API to read full topics, including main content and nested subsections. This makes it ideal for:

  • ๐Ÿง‘โ€๐Ÿฆฏ Accessibility (e.g., screen reader support)
  • ๐Ÿง  Learners with dyslexia
  • ๐ŸŽง Auditory learners

Key Features

  • Reads long texts (up to ~10,000 characters) by chunking into ~300-character segments, ensuring no words are split.
  • Adjustable speed, pitch, and volume controls.
  • Supports multiple voices (browser-dependent, e.g., Siri on iOS Safari).
  • Displays estimated reading time per topic (~200 words/minute).
  • Full playback controls: play, pause, resume, and stop.
  • Skip forward and previous chunk navigation to move through text segments.
  • Progress bar with draggable seek support, similar to common audio players.
  • Mobile-friendly with animations.
  • Smooth chunk-based playback to handle very long texts efficiently.
  • Improved chunk navigation and enhanced UI options fully implemented.

How it works: Important details

1. Chunking and Playback Logic

The core useSpeech hook splits the text into manageable chunks (~300 characters) at whitespace boundaries to avoid splitting words mid-sentence. Playback is queued chunk-by-chunk for smooth long-text reading:

 // useSpeech.ts snippet
 const speak = (text: string, voiceName?: string, rate = 1, pitch = 1, volume = 1) => {
   if (!text || !isSupported || !synthRef.current || isLoadingVoices) {
     setError("Cannot speak: Voices are still loading or not supported.");
     return;
   }
   stop(); // reset any ongoing speech
   pendingParams.current = { rate, pitch, volume };
   const cleanText = text.trim().replace(/\s+/g, " ");
   const sentences = cleanText.match(/\S.{0,298}\s/g) || [cleanText]; // chunk by whitespace
   console.log(`Total chunks: ${sentences.length}`);
   utteranceQueue.current = [
     createUtterance(sentences[0] || "", voiceName, rate, pitch, volume),
   ];
   remainingText.current = sentences.slice(1).join("");
   currentUtteranceIndex.current = 0;
   synthRef.current.cancel();
   if (utteranceQueue.current.length > 0) {
     synthRef.current.speak(utteranceQueue.current[0]);
   }
 };

2. Preparing Text Content from Markdown

The TTS text is prepared by cleaning Markdown content and concatenating all topic sections and subsections into a single string for speech:

 // TetelDetails.tsx snippet
 const getTextFromMarkdown = (markdown: string) =>
   markdown
     .replace(/[#_*>\-`]/g, "")
     .replace(/$$   .*?   $$$$   .*?   $$/g, "")
     .replace(/!$$   .*?   $$$$   .*?   $$/g, "")
     .replace(/`{1,3}[\s\S]*?`{1,3}/g, "")
     .replace(/\s+/g, " ")
     .trim();

 const textToSpeak = [
   getTextFromMarkdown(tetel.name),
   ...sections.flatMap((section) => [
     getTextFromMarkdown(section.content),
     ...(section.subsections?.flatMap((sub) => [
       getTextFromMarkdown(sub.title || ""),
       getTextFromMarkdown(sub.description || ""),
     ]) ?? []),
   ]),
   osszegzes?.content ? "ร–sszegzรฉs: " + getTextFromMarkdown(osszegzes.content) + " Vรฉge" : "",
 ]
   .filter((text) => text && text.length > 0)
   .join(" ")
   .trim();

 // Usage in JSX
 <SpeechController text={textToSpeak} />;

3. UI Controls & Features

  • Play/Pause/Resume/Stop: Toggle playback with intuitive buttons.
  • Skip Forward/Previous Chunk: Jump between text chunks โ€” useful for revisiting or skipping parts.
  • Timer Display: Shows elapsed and total estimated reading time based on current rate (~200 words/minute).
  • Progress Bar with Drag Seek: Drag or click on the progress bar to jump to any chunk.
  • Mobile Gesture Support: Swipe down anywhere on the player to stop playback, with smooth fade and slide animations.
  • Settings Menu: Lazy-loaded voice selector and sliders for rate, pitch, and volume.

Notes:

Chunks are processed one at a time to maintain performance with very long texts.

Mobile-specific resume logic handles iOS Safariโ€™s quirks.

The estimated reading time dynamically updates with playback rate changes.

The progress bar and chunk navigation provide a familiar audio player experience.

This architecture and UI provide a robust, accessible, and user-friendly text-to-speech experience designed for deep content consumption and accessibility.


๐Ÿ“Š Dashboard & Stats

  • Counts for:
    • ๐Ÿ“š Topics (Tรฉtelek)
    • ๐Ÿง  Flashcards
    • โ“ Quiz questions
  • Animated via CountUp.js
  • Responsive Tailwind layout

๐ŸŒ“ Light/Dark Mode

  • System theme detection + manual toggle
  • Applies across UI and markdown content

๐ŸŽฎ Quiz System

  • Create/attempt multiple-choice quizzes
  • Tracks progress, scores, and streaks
  • Shows progress feedback in % with CountUp.js and encouraging messages + icons
  • Optional timer mode for challenge sessions

โœ๏ธ Markdown Editor

  • Markdown saved in DB
  • Rendered with:
    • HTML passthrough
    • Syntax-highlighted code blocks
    • Custom styles
  • Perfect for structured, academic content

๐Ÿ‘ฅ User Roles & Auth

  • Secure, session-based login
  • Role permissions:
    • Guests โ†’ read-only
    • Users โ†’ can create/edit
    • Superusers โ†’ full admin CRUD
  • ๐Ÿ†• User Registration:
    • Email + password sign-up with validation
    • Error handling for existing usernames/emails
  • ๐Ÿ“ง Email Confirmation:
    • Sends a welcome email on successful registration
    • Includes styled HTML email with a login button

๐Ÿง  Flashcards

  • Rate each card after answer:
    • โœ… Easy โ†’ delay increases
    • โš–๏ธ Medium โ†’ shown moderately
    • โŒ Hard โ†’ shown soon again
  • 6 difficulty levels (custom scale)
  • Linked to topic hierarchy
  • Dynamic spaced repetition order


๐Ÿ“ฑ Responsive Sidebar & Footer Navigation

Tiomi features a sidebar navigation that transforms into a footer on mobile devices to ensure easier reach and better usability for mobile users.

  • On desktop and larger screens, the sidebar provides quick access to navigation links and the "Vissza" (Back) button.
  • On mobile, the sidebar collapses into a footer bar placed at the bottom of the screen, improving thumb accessibility without sacrificing functionality. This design choice enhances navigation ergonomics, making the app more user-friendly across devices.

๐Ÿงญ Universal Tutorial System

  • Modular, reusable steps for user onboarding
    • Handles resize edge cases and fallback steps with "requires interaction" flag
  • Example setup:
const flashcardTutorialSteps = [
  {
    title: "Kรกrtyapakli",
    content: "Kattints egy kรกrtyรกra a fรณkuszhoz.",
    selector: "#card-deck",
  },
  ...
];

// Inside parent:
<CardTutorial
  open={showTutorial}
  onClose={() => setShowTutorial(false)}
  steps={flashcardTutorialSteps}
/>

โœ… Form Validation

  • Built using Zod schemas
  • Realtime inline error feedback

๐Ÿ”’ Backend & Rate Limiting

  • Lightweight vanilla PHP (due to hosting limits)
  • Custom mini ORM for DB operations
  • Supports .env configs
  • Rate limiter with toast feedback for end users

๐Ÿงฉ Custom ORM

The application includes a lightweight custom ORM for structured and reusable database access. It provides a base Model class with common SQL helpers, and each table/model extends this base with custom logic.

๐Ÿ—๏ธ Base Model Class:

<?php
namespace Models;

use \PDO;
use \PDOException;

abstract class Model
{
  protected PDO $db;
  protected string $table;

  public function __construct(PDO $db)
  {
      $this->db    = $db;
      if (empty($this->table)) {
          throw new \Exception('Model ' . static::class . ' must set protected $table');
      }
  }

  protected function selectOne(string $sql, array $params = []): ?array
  {
      $stmt = $this->db->prepare($sql);
      $stmt->execute($params);
      $row = $stmt->fetch(PDO::FETCH_ASSOC);
      return $row !== false ? $row : null;
  }

  protected function selectAll(string $sql, array $params = []): array
  {
      $stmt = $this->db->prepare($sql);
      $stmt->execute($params);
      return $stmt->fetchAll(PDO::FETCH_ASSOC);
  }
  protected function execute(string $sql, array $params = []): int
  {
      $stmt = $this->db->prepare($sql);
      $stmt->execute($params);
      if (stripos(trim($sql), 'insert') === 0) {
          return (int)$this->db->lastInsertId();
      }
      return $stmt->rowCount();
  }

  public function delete(string $whereSql, array $params = []): int
  {
      $sql = "DELETE FROM {$this->table} WHERE $whereSql";
      return $this->execute($sql, $params);
  }
}

๐Ÿง  Example: Question Model

This child model extends the base Model class to provide specific logic for handling questions and their related answers. It shows off more advanced usage including:

  • Transaction-safe inserts & deletes
  • Embedded answer model delegation
  • Data transformation
  • Pagination, shuffling, and filtering
<?php
namespace Models;

use \PDO;
use \Exception;

class Question extends Model
{
    protected string $table = 'questions';
    private Answer $answerModel;

    public function __construct(PDO $db)
    {
        parent::__construct($db);
        $this->answerModel = new Answer($db);
    }

    public function findById(int $id): ?array
    {
        $q = $this->selectOne("SELECT id, question FROM {$this->table} WHERE id = :id", [':id' => $id]);
        if (! $q) return null;

        $answers = $this->answerModel->findByQuestionId($id);
        return [
            'id' => (int)$q['id'],
            'question' => $q['question'],
            'answers' => $answers
        ];
    }

    public function findAll(): array
    {
        $rows = $this->selectAll("SELECT id, question FROM {$this->table}");
        return array_map(fn($r) => [
            'id' => (int)$r['id'],
            'question' => $r['question']
        ], $rows);
    }

    public function findRandom(): ?array
    {
        $q = $this->selectOne("SELECT id, question FROM {$this->table} ORDER BY RAND() LIMIT 1");
        if (! $q) return null;

        $detail = $this->findById((int)$q['id']);
        if ($detail) {
            shuffle($detail['answers']);
        }
        return $detail;
    }

    public function createWithAnswers(string $text, array $answers, int $tetel_id): int
    {
        if (count($answers) < 2) {
            throw new Exception("At least two answers required");
        }
        $this->db->beginTransaction();
        try {
            $stmt = $this->db->prepare("INSERT INTO {$this->table} (question, tetel_id) VALUES (:q, :tid)");
            $stmt->execute([':q' => $text, ':tid' => $tetel_id]);
            $qid = (int)$this->db->lastInsertId();
            $this->answerModel->createBulk($qid, $answers);
            $this->db->commit();
            return $qid;
        } catch (Exception $e) {
            $this->db->rollBack();
            throw $e;
        }
    }

    public function deleteById(int $id): void
    {
        $this->db->beginTransaction();
        $this->answerModel->deleteByQuestionId($id);
        $this->delete("id = :id", [':id' => $id]);
        $this->db->commit();
    }

    public function updateWithAnswers(int $id, string $text, array $answers): void
    {
        if (count($answers) < 2) {
            throw new Exception("At least two answers required");
        }
        $this->db->beginTransaction();
        $this->execute("UPDATE {$this->table} SET question = :q WHERE id = :id", [':q' => $text, ':id' => $id]);
        $this->answerModel->updateBulk($id, $answers);
        $this->db->commit();
    }
}

๐ŸŒ Example Endpoint: List Questions with Pagination

This API endpoint uses the Question model to return a paginated list of multiple-choice questions. It uses the ORMโ€™s selectAll() helper internally via raw SQL, and maps the results for frontend use.

<?php
header("Content-Type: application/json");
header("Access-Control-Allow-Methods: GET");

$pdo = require __DIR__ . '/../../core/init.php';
require_once __DIR__ . '/../../models/Model.php';
require_once __DIR__ . '/../../models/Answer.php';
require_once __DIR__ . '/../../models/Question.php';

use Models\Question;

// Parse pagination parameters
$page  = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = isset($_GET['limit']) ? max(1, (int)$_GET['limit']) : 35;
$offset = ($page - 1) * $limit;

// Instantiate the model
$questionModel = new Question($pdo);

// Total count for pagination UI
$total = $pdo->query("SELECT COUNT(*) FROM questions")->fetchColumn();

// Fetch paginated questions
$stmt = $pdo->prepare("SELECT id, question FROM questions LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$questions = $stmt->fetchAll(PDO::FETCH_ASSOC);

// Return formatted response
echo json_encode([
  "data" => array_map(fn($q) => [
      'id' => (int)$q['id'],
      'question' => $q['question']
  ], $questions),
  "total" => (int)$total,
]);

๐Ÿงช Testing

  • Unit/component testing via ViteTest
  • Covers:
    • Forms
    • Zod schemas
    • UI components and logic

๐Ÿงพ Sample Component

<Disclosure>
  <Disclosure.Button>
    <FaChevronDown />
  </Disclosure.Button>
  <Disclosure.Panel>
    {section.subsections.map(...)}
  </Disclosure.Panel>
</Disclosure>

๐Ÿง  Use Cases

  • ๐Ÿ“– Organize study materials into nested topics
  • ๐Ÿงช Self-test with flashcards & quizzes
  • โœ๏ธ Markdown-powered note-taking
  • Ideal for structured learning with:
    • Linked flashcards
    • Active recall testing

๐Ÿ“ˆ Performance & Lighthouse

Lighthouse Scores Summary

๐Ÿ“ฑ Mobile Lighthouse Scores

Page Performance โšก Accessibility โ™ฟ Best Practices โœ… SEO ๐Ÿ”
Home Page 95 98 100 100
Topic Detail 95 100 100 100
Quiz Game 98 100 100 100
FlashCardGame 98 100 100 100
Topic List 98 100 100 100
Topic LandingPage 97 100 100 100
Random Flashcards 98 100 100 100

๐Ÿ’ป Desktop Lighthouse Scores

Page Performance โšก Accessibility โ™ฟ Best Practices โœ… SEO ๐Ÿ”
Home Page 100 98 100 100
Topic Detail 100 100 100 100
Quiz Game 100 100 100 100
FlashCardGame 100 100 100 100
Topic List 100 100 100 100
Topic LandingPage 100 100 100 100
Random Flashcards 100 100 100 100

๐Ÿ”ฎ Future Plans

  • Migrate backend to a more scalable stack:
    • โœณ๏ธ Slim PHP / Lumen (Laravel) / Express.js / Actix
  • Group feature with their own admins own permissions
  • Offline sync actions when connectivity is back

Additional Notes

  • Quiz questions and Flashcards are tied to topics (tรฉtelek) mainly help learning specific topics
  • Open collaboration model currently; planned group fragmentation for better access control
  • Tutorial engine now handles resize edge cases and supports fallback steps requiring user interaction before continuing
  • Card game enhancements improve engagement and learning effectiveness
  • Text-to-speech scaffolds all available voices with speed and pitch adjustment, making it ideal for diverse learners
  • Reading time estimates help learners manage study sessions effectively

Releases

No releases published

Packages

 
 
 

Contributors

Languages