├── packages ├── api │ ├── .dockerignore │ ├── src │ │ ├── category │ │ │ ├── infra │ │ │ │ ├── database │ │ │ │ │ ├── migrations │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── 0001_initial.py │ │ │ │ │ ├── repository │ │ │ │ │ │ ├── mapper.py │ │ │ │ │ │ └── rdb.py │ │ │ │ │ └── models.py │ │ │ │ └── django │ │ │ │ │ ├── admin.py │ │ │ │ │ └── apps.py │ │ │ ├── presentation │ │ │ │ └── rest │ │ │ │ │ ├── request.py │ │ │ │ │ ├── containers.py │ │ │ │ │ ├── response.py │ │ │ │ │ └── api.py │ │ │ ├── domain │ │ │ │ ├── exceptions.py │ │ │ │ └── entity.py │ │ │ └── application │ │ │ │ └── use_case │ │ │ │ ├── command.py │ │ │ │ └── query.py │ │ ├── flashcard │ │ │ ├── infra │ │ │ │ ├── database │ │ │ │ │ ├── migrations │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── 0001_initial.py │ │ │ │ │ ├── models.py │ │ │ │ │ └── repository │ │ │ │ │ │ ├── mapper.py │ │ │ │ │ │ └── rdb.py │ │ │ │ └── django │ │ │ │ │ ├── admin.py │ │ │ │ │ └── apps.py │ │ │ ├── presentation │ │ │ │ └── rest │ │ │ │ │ ├── request.py │ │ │ │ │ ├── containers.py │ │ │ │ │ ├── response.py │ │ │ │ │ └── api.py │ │ │ ├── domain │ │ │ │ ├── exceptions.py │ │ │ │ └── entity.py │ │ │ └── application │ │ │ │ └── use_case │ │ │ │ ├── query.py │ │ │ │ └── command.py │ │ ├── .DS_Store │ │ ├── shared │ │ │ ├── domain │ │ │ │ ├── exception.py │ │ │ │ └── entity.py │ │ │ ├── infra │ │ │ │ ├── repository │ │ │ │ │ ├── cursor_wrapper.py │ │ │ │ │ ├── rdb.py │ │ │ │ │ └── mapper.py │ │ │ │ └── django │ │ │ │ │ ├── asgi.py │ │ │ │ │ ├── wsgi.py │ │ │ │ │ └── settings.py │ │ │ └── presentation │ │ │ │ └── rest │ │ │ │ ├── response.py │ │ │ │ └── api.py │ │ └── manage.py │ ├── .DS_Store │ ├── docs │ │ └── collection.paw │ ├── pyproject.toml │ ├── .env │ ├── requirements.txt │ ├── .gitignore │ ├── .vscode │ │ └── launch.json │ └── Dockerfile-dev └── web │ ├── .eslintrc.json │ ├── .vscode │ ├── extensions.json │ ├── settings.json │ └── launch.json │ ├── public │ └── favicon.ico │ ├── utils │ ├── wait.ts │ ├── array.ts │ └── color.ts │ ├── postcss.config.mjs │ ├── src │ ├── lib │ │ ├── utils.ts │ │ └── models.ts │ ├── app │ │ ├── api │ │ │ ├── wipe-database │ │ │ │ └── route.ts │ │ │ ├── populate-database │ │ │ │ └── route.ts │ │ │ ├── flashcards │ │ │ │ ├── route.ts │ │ │ │ └── [id] │ │ │ │ │ └── route.ts │ │ │ └── categories │ │ │ │ ├── route.ts │ │ │ │ └── [id] │ │ │ │ ├── flashcards │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ ├── practice │ │ │ ├── page.tsx │ │ │ ├── flashcards.tsx │ │ │ └── practice-session.tsx │ │ ├── database-operations.tsx │ │ ├── providers.tsx │ │ ├── globals.css │ │ ├── manage │ │ │ ├── _categories │ │ │ │ ├── create-category-dialog.tsx │ │ │ │ ├── categories-columns.tsx │ │ │ │ ├── edit-category-dialog.tsx │ │ │ │ ├── categories-data-table.tsx │ │ │ │ └── confirm-category-delete.tsx │ │ │ ├── _flashcards │ │ │ │ ├── flashcards-columns.tsx │ │ │ │ ├── confirm-flashcard-delete.tsx │ │ │ │ ├── flashcards-data-table.tsx │ │ │ │ ├── select-category.tsx │ │ │ │ ├── create-flashcard-dialog.tsx │ │ │ │ └── edit-flashcard-dialog.tsx │ │ │ └── page.tsx │ │ └── layout.tsx │ └── components │ │ └── ui │ │ ├── hidden-input.tsx │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ ├── tooltip.tsx │ │ ├── popover.tsx │ │ ├── toggle.tsx │ │ ├── button.tsx │ │ ├── accordion.tsx │ │ ├── card.tsx │ │ ├── table.tsx │ │ ├── dialog.tsx │ │ ├── sheet.tsx │ │ ├── alert-dialog.tsx │ │ ├── command.tsx │ │ ├── select.tsx │ │ └── dropdown-menu.tsx │ ├── next.config.js │ ├── components.json │ ├── .gitignore │ ├── Dockerfile-dev │ ├── tsconfig.json │ ├── package.json │ ├── Dockerfile-prod │ └── tailwind.config.ts ├── README.md └── compose.yml /packages/api/.dockerignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /packages/api/src/category/infra/database/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/api/src/flashcard/infra/database/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /packages/api/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolovlazar/flashcards/HEAD/packages/api/.DS_Store -------------------------------------------------------------------------------- /packages/web/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["vsls-contrib.codetour"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/api/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolovlazar/flashcards/HEAD/packages/api/src/.DS_Store -------------------------------------------------------------------------------- /packages/api/docs/collection.paw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolovlazar/flashcards/HEAD/packages/api/docs/collection.paw -------------------------------------------------------------------------------- /packages/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikolovlazar/flashcards/HEAD/packages/web/public/favicon.ico -------------------------------------------------------------------------------- /packages/web/utils/wait.ts: -------------------------------------------------------------------------------- 1 | export async function wait(ms: number) { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | } 4 | -------------------------------------------------------------------------------- /packages/web/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } 5 | -------------------------------------------------------------------------------- /packages/api/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | # Same as Black. 3 | line-length = 80 4 | indent-width = 4 5 | 6 | # Enable the isort rule 7 | extend-select = ["I"] -------------------------------------------------------------------------------- /packages/api/.env: -------------------------------------------------------------------------------- 1 | GROQ_API_KEY= # Generate new 2 | DEBUG=1 3 | DB_NAME=flashcards 4 | DB_USER=postgres 5 | DB_PASSWORD=password 6 | DB_HOST=localhost 7 | DB_PORT=5432 8 | -------------------------------------------------------------------------------- /packages/api/src/category/infra/django/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from category.domain.entity import Category 4 | 5 | admin.site.register(Category) 6 | -------------------------------------------------------------------------------- /packages/api/src/flashcard/infra/django/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from flashcard.domain.entity import Flashcard 4 | 5 | admin.site.register(Flashcard) 6 | -------------------------------------------------------------------------------- /packages/web/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /packages/api/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.8.1 2 | Django==5.1.1 3 | django-environ==0.11.2 4 | django-ninja==1.3.0 5 | groq==0.11.0 6 | typing_extensions==4.12.2 7 | pytest==8.3.3 8 | ruff==0.6.9 9 | psycopg2-binary>=2.9.9 -------------------------------------------------------------------------------- /packages/web/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /packages/api/src/category/presentation/rest/request.py: -------------------------------------------------------------------------------- 1 | from ninja import Schema 2 | 3 | 4 | class PostCategoryRequestBody(Schema): 5 | name: str 6 | 7 | 8 | class PatchCategoryRequestBody(Schema): 9 | name: str | None 10 | -------------------------------------------------------------------------------- /packages/api/.gitignore: -------------------------------------------------------------------------------- 1 | # Environment 2 | .venv 3 | 4 | # Byte-compiled / optimized / DLL files 5 | **/__pycache__ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | dist/ 13 | *.egg-info 14 | *.egg 15 | -------------------------------------------------------------------------------- /packages/api/src/shared/domain/exception.py: -------------------------------------------------------------------------------- 1 | class BaseMsgException(Exception): 2 | message: str 3 | 4 | def __str__(self): 5 | return self.message 6 | 7 | class ModelExistsError(BaseMsgException): 8 | message = "Model already exists" 9 | -------------------------------------------------------------------------------- /packages/web/utils/array.ts: -------------------------------------------------------------------------------- 1 | export const shuffleArray = (array: Array) => { 2 | for (let i = array.length - 1; i > 0; i--) { 3 | const j = Math.floor(Math.random() * (i + 1)); 4 | [array[i], array[j]] = [array[j], array[i]]; 5 | } 6 | 7 | return array; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/web/src/lib/models.ts: -------------------------------------------------------------------------------- 1 | export type Category = { 2 | id: number; 3 | name: string; 4 | slug: string; 5 | }; 6 | 7 | export type Flashcard = { 8 | id: number; 9 | question: string; 10 | answer: string; 11 | slug: string; 12 | category_id: number; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/web/src/app/api/wipe-database/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function GET() { 4 | const response = await fetch('http://api:8000/wipe-database', { 5 | cache: 'no-store', 6 | }); 7 | return NextResponse.json(response.json()); 8 | } 9 | -------------------------------------------------------------------------------- /packages/web/src/app/api/populate-database/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function GET() { 4 | const response = await fetch('http://api:8000/populate-database', { 5 | cache: 'no-store', 6 | }); 7 | return NextResponse.json(response.json()); 8 | } 9 | -------------------------------------------------------------------------------- /packages/api/src/category/infra/django/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CategoryConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "category" 7 | 8 | def ready(self): 9 | import shared.infra.repository.cursor_wrapper 10 | -------------------------------------------------------------------------------- /packages/api/src/flashcard/infra/django/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FlashcardConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "flashcard" 7 | 8 | def ready(self): 9 | import shared.infra.repository.cursor_wrapper 10 | -------------------------------------------------------------------------------- /packages/api/src/flashcard/presentation/rest/request.py: -------------------------------------------------------------------------------- 1 | from ninja import Schema 2 | 3 | 4 | class PostFlashcardRequestBody(Schema): 5 | question: str 6 | answer: str 7 | category_id: int 8 | 9 | 10 | class PatchFlashcardRequestBody(Schema): 11 | question: str | None 12 | answer: str | None 13 | category_id: int | None 14 | -------------------------------------------------------------------------------- /packages/web/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | output: "standalone", 4 | async redirects() { 5 | return [ 6 | { 7 | source: '/', 8 | destination: '/manage', 9 | permanent: true, 10 | }, 11 | ]; 12 | }, 13 | }; 14 | 15 | module.exports = nextConfig; 16 | -------------------------------------------------------------------------------- /packages/api/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug API", 6 | "type": "debugpy", 7 | "request": "launch", 8 | "stopOnEntry": false, 9 | "program": "${workspaceRoot}/src/manage.py", 10 | "args": ["runserver", "--no-color", "--noreload"] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/web/src/components/ui/hidden-input.tsx: -------------------------------------------------------------------------------- 1 | export function HiddenInput(props: Partial) { 2 | return ( 3 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/web/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /packages/api/src/category/domain/exceptions.py: -------------------------------------------------------------------------------- 1 | from shared.domain.exception import BaseMsgException 2 | 3 | 4 | class CategoryNameTooShort(BaseMsgException): 5 | message = "Category name must be at least 5 characters long" 6 | 7 | class CategoryNotFoundError(BaseMsgException): 8 | message = "Category not found" 9 | 10 | class CategoryExistsError(BaseMsgException): 11 | message = "Category already exists" 12 | -------------------------------------------------------------------------------- /packages/api/src/flashcard/domain/exceptions.py: -------------------------------------------------------------------------------- 1 | from shared.domain.exception import BaseMsgException 2 | 3 | 4 | class FlashcardNotFoundError(BaseMsgException): 5 | message = "Flashcard not found" 6 | 7 | class FlashcardExistsError(BaseMsgException): 8 | message = "Flashcard already exists" 9 | 10 | 11 | class FlashcardQuestionTooShort(BaseMsgException): 12 | message = "Flashcard question must be at least 5 characters long" 13 | -------------------------------------------------------------------------------- /packages/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/web/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js: debug full stack", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "npm run dev", 9 | "serverReadyAction": { 10 | "pattern": "started server on .+, url: (https?://.+)", 11 | "uriFormat": "%s", 12 | "action": "debugWithChrome" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/api/src/shared/infra/repository/cursor_wrapper.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from django.db.backends.utils import CursorWrapper 4 | 5 | 6 | def delayed_execute(func): 7 | @wraps(func) 8 | def wrapper(self, sql, params=None): 9 | sql = f"SELECT pg_sleep(0.05); {sql}" 10 | 11 | return func(self, sql, params) 12 | 13 | return wrapper 14 | 15 | 16 | CursorWrapper.execute = delayed_execute(CursorWrapper.execute) 17 | -------------------------------------------------------------------------------- /packages/api/src/shared/domain/entity.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, TypeVar 3 | 4 | 5 | @dataclass(kw_only=True) 6 | class Entity: 7 | id: int | None = None 8 | 9 | def __eq__(self, other: Any) -> bool: 10 | if isinstance(other, type(self)): 11 | return self.id == other.id 12 | return False 13 | 14 | def __hash__(self): 15 | return hash(self.id) 16 | 17 | EntityType = TypeVar("EntityType", bound=Entity) -------------------------------------------------------------------------------- /packages/api/src/shared/presentation/rest/response.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, TypeVar 2 | 3 | from ninja import Schema 4 | 5 | T = TypeVar("T") 6 | 7 | 8 | def response(results: dict | list) -> dict: 9 | return {"results": results} 10 | 11 | 12 | class ObjectResponse(Schema, Generic[T]): 13 | results: T 14 | 15 | 16 | def error_response(msg: str) -> dict: 17 | return {"results": {"message": msg}} 18 | 19 | 20 | class ErrorMessageResponse(Schema): 21 | message: str -------------------------------------------------------------------------------- /packages/api/src/shared/infra/django/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for api project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shared.infra.django.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /packages/api/src/shared/infra/django/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for api project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shared.infra.django.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /packages/api/src/flashcard/presentation/rest/containers.py: -------------------------------------------------------------------------------- 1 | from flashcard.application.use_case.command import FlashcardCommand 2 | from flashcard.application.use_case.query import FlashcardQuery 3 | from flashcard.infra.database.repository.rdb import FlashcardRepository 4 | 5 | flashcard_repo: FlashcardRepository = FlashcardRepository() 6 | 7 | flashcard_query: FlashcardQuery = FlashcardQuery(flashcard_repository=flashcard_repo) 8 | flashcard_command: FlashcardCommand = FlashcardCommand(flashcard_repository=flashcard_repo) 9 | -------------------------------------------------------------------------------- /packages/api/src/category/domain/entity.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | 5 | from django.utils.text import slugify 6 | 7 | from shared.domain.entity import Entity 8 | 9 | 10 | @dataclass(eq=False) 11 | class Category(Entity): 12 | name: str 13 | slug: str 14 | 15 | @classmethod 16 | def new(cls, name: str) -> Category: 17 | return cls(name=name, slug=slugify(name)) 18 | 19 | def update_name(self, name: str) -> None: 20 | self.name = name 21 | self.slug = slugify(name) -------------------------------------------------------------------------------- /packages/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /packages/web/src/app/practice/page.tsx: -------------------------------------------------------------------------------- 1 | import Practice from './practice-session'; 2 | 3 | export const dynamic = 'force-dynamic'; 4 | 5 | const getCategories = async () => { 6 | const res = await fetch('http://api:8000/categories', { cache: 'no-store' }); 7 | if (!res.ok) { 8 | const error = await res.text(); 9 | throw new Error(error); 10 | } 11 | return (await res.json()).results.categories; 12 | }; 13 | 14 | export default async function Manage() { 15 | const categories = await getCategories(); 16 | 17 | return ; 18 | } 19 | -------------------------------------------------------------------------------- /packages/web/utils/color.ts: -------------------------------------------------------------------------------- 1 | import Color from "color"; 2 | 3 | export const stringToColour = (str: string = "default") => { 4 | var hash = 0; 5 | for (var i = 0; i < str.length; i++) { 6 | hash = str.charCodeAt(i) + ((hash << 5) - hash); 7 | } 8 | var colour = "#"; 9 | for (var i = 0; i < 3; i++) { 10 | var value = (hash >> (i * 8)) & 0xff; 11 | colour += ("00" + value.toString(16)).substr(-2); 12 | } 13 | return colour; 14 | }; 15 | 16 | export const getContrastingTextColor = (hexcolor: string) => { 17 | const color = Color(hexcolor); 18 | return color.isLight() ? "#000" : "#fff"; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/web/src/app/api/flashcards/route.ts: -------------------------------------------------------------------------------- 1 | export async function POST(request: Request) { 2 | const formData = await request.formData(); 3 | 4 | const res = await fetch('http://api:8000/flashcards', { 5 | headers: { 6 | 'Content-Type': 'application/json', 7 | }, 8 | body: JSON.stringify(Object.fromEntries(formData)), 9 | method: 'POST', 10 | }); 11 | 12 | if (!res.ok) { 13 | const error = await res.text(); 14 | return new Response(error, { status: res.status }); 15 | } 16 | 17 | const flashcard = (await res.json()).results.flashcard; 18 | 19 | return Response.json({ flashcard }, { status: 201 }); 20 | } 21 | -------------------------------------------------------------------------------- /packages/web/src/app/api/categories/route.ts: -------------------------------------------------------------------------------- 1 | // Create Category 2 | export async function POST(request: Request) { 3 | const formData = await request.formData(); 4 | 5 | const res = await fetch('http://api:8000/categories', { 6 | body: JSON.stringify(Object.fromEntries(formData)), 7 | method: 'POST', 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | }, 11 | }); 12 | 13 | if (!res.ok) { 14 | const error = await res.text(); 15 | return new Response(error, { status: res.status }); 16 | } 17 | 18 | const category = (await res.json()).results.category; 19 | return Response.json(category, { status: 201 }); 20 | } 21 | -------------------------------------------------------------------------------- /packages/web/Dockerfile-dev: -------------------------------------------------------------------------------- 1 | # Use an official Node.js image as the base image 2 | FROM node:22-alpine 3 | 4 | # Set the working directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy only package.json and package-lock.json (or yarn.lock) to leverage Docker cache 8 | COPY package.json package-lock.json ./ 9 | 10 | # Install dependencies 11 | RUN npm ci 12 | 13 | # Copy the rest of your application code 14 | COPY . . 15 | 16 | # Expose the Next.js development server port 17 | EXPOSE 3000 18 | 19 | # Set environment variables 20 | ENV NODE_ENV development 21 | ENV PORT 3000 22 | 23 | # Start the Next.js application in development mode 24 | CMD ["npm", "run", "dev"] 25 | -------------------------------------------------------------------------------- /packages/api/src/category/presentation/rest/containers.py: -------------------------------------------------------------------------------- 1 | from category.application.use_case.command import CategoryCommand 2 | from category.application.use_case.query import CategoryQuery 3 | from category.infra.database.repository.rdb import CategoryRepository 4 | from flashcard.infra.database.repository.rdb import FlashcardRepository 5 | 6 | flashcard_repo: FlashcardRepository = FlashcardRepository() 7 | 8 | category_repo: CategoryRepository = CategoryRepository() 9 | 10 | category_query: CategoryQuery = CategoryQuery(category_repository=category_repo, flashcard_repository=flashcard_repo) 11 | category_command: CategoryCommand = CategoryCommand(category_repository=category_repo) 12 | -------------------------------------------------------------------------------- /packages/api/src/shared/infra/repository/rdb.py: -------------------------------------------------------------------------------- 1 | from django.db import IntegrityError 2 | 3 | from shared.domain.entity import EntityType 4 | from shared.domain.exception import ModelExistsError 5 | from shared.infra.repository.mapper import DjangoModelType, ModelMapperInterface 6 | 7 | 8 | class RDBRepository: 9 | model_mapper: ModelMapperInterface 10 | 11 | def save(self, entity: EntityType) -> EntityType: 12 | instance: DjangoModelType = self.model_mapper.to_instance(entity=entity) 13 | try: 14 | instance.save() 15 | except IntegrityError as e: 16 | raise ModelExistsError(e) 17 | 18 | return self.model_mapper.to_entity(instance=instance) -------------------------------------------------------------------------------- /packages/api/src/category/infra/database/repository/mapper.py: -------------------------------------------------------------------------------- 1 | from category.domain.entity import Category as CategoryEntity 2 | from category.infra.database.models import Category as CategoryModel 3 | from shared.infra.repository.mapper import ModelMapperInterface 4 | 5 | 6 | class CategoryMapper(ModelMapperInterface): 7 | def to_entity(self, instance: CategoryModel) -> CategoryEntity: 8 | return CategoryEntity( 9 | id=instance.id, 10 | name=instance.name, 11 | slug=instance.slug 12 | ) 13 | 14 | def to_instance(self, entity: CategoryEntity) -> CategoryModel: 15 | return CategoryModel( 16 | id=entity.id, 17 | name=entity.name, 18 | slug=entity.slug 19 | ) -------------------------------------------------------------------------------- /packages/api/Dockerfile-dev: -------------------------------------------------------------------------------- 1 | # Use official Python runtime as base image 2 | FROM python:3.11-slim 3 | 4 | # Set environment variables 5 | ENV PYTHONDONTWRITEBYTECODE=1 6 | ENV PYTHONUNBUFFERED=1 7 | 8 | # Set work directory 9 | WORKDIR /app 10 | 11 | # Install system dependencies 12 | RUN apt-get update && apt-get install -y \ 13 | gcc \ 14 | libpq-dev \ 15 | python3-dev \ 16 | postgresql-client \ 17 | && rm -rf /var/lib/apt/lists/* 18 | 19 | # Install Python dependencies 20 | COPY requirements.txt . 21 | RUN pip install --no-cache-dir -r requirements.txt 22 | 23 | # Copy project files 24 | COPY . . 25 | 26 | # Expose port 27 | EXPOSE 8000 28 | 29 | # Start development server 30 | CMD ["python", "src/manage.py", "runserver", "0.0.0.0:8000"] 31 | -------------------------------------------------------------------------------- /packages/api/src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shared.infra.django.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /packages/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"], 22 | "@/prisma/*": ["./prisma/*"], 23 | "@/prisma": ["./prisma"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /packages/api/src/category/infra/database/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.text import slugify 3 | 4 | from category.domain.exceptions import CategoryNameTooShort 5 | 6 | 7 | class Category(models.Model): 8 | name = models.CharField(max_length=255) 9 | slug = models.SlugField(unique=True, blank=True) 10 | 11 | def clean(self) -> None: 12 | if len(self.name) < 6: 13 | raise CategoryNameTooShort() 14 | self.slug = slugify(self.name) 15 | 16 | return super().clean() 17 | 18 | def save(self, *args, **kwargs): 19 | self.clean() 20 | return super().save(*args, **kwargs) 21 | 22 | def delete(self, *args, **kwargs): 23 | return super().delete(*args, **kwargs) 24 | 25 | def __str__(self): 26 | return self.name 27 | 28 | -------------------------------------------------------------------------------- /packages/api/src/shared/infra/repository/mapper.py: -------------------------------------------------------------------------------- 1 | from typing import List, TypeVar 2 | 3 | from django.db.models import Model 4 | 5 | from shared.domain.entity import EntityType 6 | 7 | DjangoModelType = TypeVar("EntityType", bound=Model) 8 | 9 | 10 | class ModelMapperInterface: 11 | def to_entity(self, model: DjangoModelType) -> EntityType: 12 | raise NotImplementedError 13 | 14 | def to_instance(self, entity: EntityType) -> DjangoModelType: 15 | raise NotImplementedError 16 | 17 | def to_entity_list(self, instances: List[DjangoModelType]) -> List[EntityType]: 18 | return [self.to_entity(instance) for instance in instances] 19 | 20 | def to_instance_list(self, entities: List[EntityType]) -> List[DjangoModelType]: 21 | return [self.to_instance(entity) for entity in entities] 22 | -------------------------------------------------------------------------------- /packages/api/src/flashcard/application/use_case/query.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from flashcard.domain.entity import Flashcard 4 | from flashcard.infra.database.repository.rdb import FlashcardRepository 5 | 6 | 7 | class FlashcardQuery: 8 | flashcard_repository: FlashcardRepository 9 | 10 | def __init__(self, flashcard_repository: FlashcardRepository): 11 | self.flashcard_repository = flashcard_repository 12 | 13 | def get_all_flashcards(self) -> List[Flashcard]: 14 | return self.flashcard_repository.find_all() 15 | 16 | def get_flashcard(self, id: int) -> Flashcard: 17 | return self.flashcard_repository.find_by_id(id) 18 | 19 | def get_flashcards_by_category(self, category_id: int) -> List[Flashcard]: 20 | return self.flashcard_repository.find_by_category(category_id=category_id) 21 | -------------------------------------------------------------------------------- /packages/web/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | import { cva, type VariantProps } from "class-variance-authority"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /packages/web/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |