diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..08926877662fbbcb7caeab67484f03394099831c
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,3 @@
+POSTGRES_USER=
+POSTGRES_PASSWORD=
+POSTGRES_DB=
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..1cd5fbcf8c2fe948a71c69d64fad98ebda7ad183
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+db/
+.env
+.DS_Store
+src/server/html/
+.idea/
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 2e3ddd6d8b2132e3c4b53de7ba0e9ce61e60670a..08018e27836989ec35c8bf0ac2f0b1b491b5556f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1 +1,6 @@
-FROM php:8.0-apache
\ No newline at end of file
+FROM php:8.0-apache
+
+RUN apt-get update && apt-get install -y libpq-dev
+RUN docker-php-ext-install pdo pdo_pgsql
+COPY ./php.ini /usr/local/etc/php/php.ini
+RUN a2enmod rewrite
\ No newline at end of file
diff --git a/README.md b/README.md
index 1132afae3862a26173197e27c2d754967e6a953d..74e59f30e0b01fc99b8b4c9a6b4a894012aa8141 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,134 @@
 # Drawl
+
+Drawl adalah sebuah aplikasi berbasis web yang memungkinkan pengguna terdaftar untuk membuat daftar "watchlist". Di
+dalam watchlist ini, pengguna dapat menyimpan daftar anime dan drama yang ingin ditonton atau direkomendasikan ke orang
+lain. Terdapat dua visibility, yaitu public dan private watchlist. Aplikasi ini juga memberikan kesempatan bagi pengguna
+yang belum terdaftar untuk menjelajahi dan melihat watchlist serta katalog anime dan drama yang ada tanpa harus
+mendaftar. Selain itu, pengguna terdaftar juga
+dapat menyimpan dan menyukai suatu watchlist.
+
+Terdapat tiga role dalam aplikasi ini, yaitu unregistered user, registered user, dan admin. Admin bertugas untuk
+menambahkan catalog anime dan drama.
+
+## Daftar Isi
+
+- [Daftar Requirements](#daftar-requirements)
+- [Cara Instalasi](#cara-instalasi)
+- [Cara Menjalankan Server](#cara-menjalankan-server)
+- [Tangkapan Layar](#tangkapan-layar)
+- [Pembagian Tugas](#pembagian-tugas)
+
+## Daftar Requirements
+
+- Docker
+
+## Cara Instalasi
+
+Silakan kunjungi halaman official docker pada [link](https://www.docker.com/products/docker-desktop/) berikut. Lalu
+install sesuai dengan sistem operasi Anda.
+
+## Cara Menjalankan Server
+
+1. Pastikan docker telah terinstall
+2. Build docker image dengan menajalankan command pada folder `scripts/build-image.sh`
+3. Pastikan port `5432`, `8008`, dan `8080` tidak sedang digunakan
+4. Jalankan server dengan command `docker compose up`
+5. Tunggu beberapa detik hingga server berhasil berjalan dan database siap menerima koneksi.
+6. Aplikasi siap untuk digunakan
+7. Jika ingin melakukan seed catalog, dapat menggunakan query yang ada pada folder `/src/seed/seed.sql`
+
+## Tangkapan Layar
+
+### Home
+
+![Home](/assets/lighthouse/home.png)
+
+### Sign In
+
+![Sign In](/assets/lighthouse/signin.png)
+
+### Sign Up
+
+![Sign Up](/assets/lighthouse/signup.png)
+
+### Profile
+
+![Profile](/assets/lighthouse/profile.png)
+
+### My Bookmark
+
+![My Bookmark](/assets/lighthouse/my-bookmark.png)
+
+### My Watchlist
+
+![My Watchlist](/assets/lighthouse/my-watchlist.png)
+
+### Catalog
+
+![Catalog](/assets/lighthouse/catalog.png)
+
+### Catalog Create
+
+![Catalog Create](/assets/lighthouse/catalog-create.png)
+
+### Catalog Delete
+
+![Catalog Delete](/assets/lighthouse/catalog-delete.png)
+
+### Catalog Detail
+
+![Catalog Detail](/assets/lighthouse/catalog-detail.png)
+
+### Catalog Edit
+
+![Catalog Edit](/assets/lighthouse/catalog-edit.png)
+
+### Watchlist Detail
+
+![Watchlist Detail](/assets/lighthouse/watchlist-detail.png)
+
+### Watchlist Detail
+
+![Watchlist Delete](/assets/lighthouse/watchlist-delete.png)
+
+## Pembagian Tugas
+
+### Server Side
+
+| Tugas              | NIM      |
+|--------------------|----------|
+| Setup Awal Project | 13521150 |
+| Sign In            | 13521048 |
+| Sign Up            | 13521048 |
+| Profile            | 13521048 |
+| My Bookmark        | 13521153 |
+| My Watchlist       | 13521153 |
+| Catalog            | 13521153 |
+| Catalog Create     | 13521153 |
+| Catalog Delete     | 13521153 |
+| Catalog Detail     | 13521153 |
+| Catalog Edit       | 13521153 |
+| Home               | 13521150 |
+| Watchlist Create   | 13521150 |
+| Watchlist Delete   | 13521150 |
+| Watchlist Edit     | 13521150 |
+
+### Client Side
+
+| Tugas              | NIM      |
+|--------------------|----------|
+| Setup Awal Project | 13521150 |
+| Sign In            | 13521048 |
+| Sign Up            | 13521048 |
+| Profile            | 13521048 |
+| My Bookmark        | 13521153 |
+| My Watchlist       | 13521153 |
+| Catalog            | 13521153 |
+| Catalog Create     | 13521153 |
+| Catalog Delete     | 13521153 |
+| Catalog Detail     | 13521153 |
+| Catalog Edit       | 13521153 |
+| Home               | 13521150 |
+| Watchlist Create   | 13521150 |
+| Watchlist Delete   | 13521150 |
+| Watchlist Edit     | 13521150 |
\ No newline at end of file
diff --git a/assets/lighthouse/catalog-create.png b/assets/lighthouse/catalog-create.png
new file mode 100644
index 0000000000000000000000000000000000000000..ffc30456db7e401133bdf59a24e36251fbf31b95
Binary files /dev/null and b/assets/lighthouse/catalog-create.png differ
diff --git a/assets/lighthouse/catalog-delete.png b/assets/lighthouse/catalog-delete.png
new file mode 100644
index 0000000000000000000000000000000000000000..4bdc783a43865d0d0be5170b4836bda917c7d0a0
Binary files /dev/null and b/assets/lighthouse/catalog-delete.png differ
diff --git a/assets/lighthouse/catalog-detail.png b/assets/lighthouse/catalog-detail.png
new file mode 100644
index 0000000000000000000000000000000000000000..0a702365f27b4f031d2b7b17281e31c0f2f5d871
Binary files /dev/null and b/assets/lighthouse/catalog-detail.png differ
diff --git a/assets/lighthouse/catalog-edit.png b/assets/lighthouse/catalog-edit.png
new file mode 100644
index 0000000000000000000000000000000000000000..0c2d6d35630db390ec2eb8d9f7853fb425d3e5ce
Binary files /dev/null and b/assets/lighthouse/catalog-edit.png differ
diff --git a/assets/lighthouse/catalog.png b/assets/lighthouse/catalog.png
new file mode 100644
index 0000000000000000000000000000000000000000..1addcbe4772e0e4110fb54e511e61a0b0c2bd7a3
Binary files /dev/null and b/assets/lighthouse/catalog.png differ
diff --git a/assets/lighthouse/home.png b/assets/lighthouse/home.png
new file mode 100644
index 0000000000000000000000000000000000000000..937c92a34d1977b311d96439ac66c1c2b262cbd9
Binary files /dev/null and b/assets/lighthouse/home.png differ
diff --git a/assets/lighthouse/my-bookmark.png b/assets/lighthouse/my-bookmark.png
new file mode 100644
index 0000000000000000000000000000000000000000..5f990c91341c3b0edf49e8b3fa2a7a6499ffdabb
Binary files /dev/null and b/assets/lighthouse/my-bookmark.png differ
diff --git a/assets/lighthouse/my-watchlist.png b/assets/lighthouse/my-watchlist.png
new file mode 100644
index 0000000000000000000000000000000000000000..3d2614733002e02414841cbb61d28cf094cd36ea
Binary files /dev/null and b/assets/lighthouse/my-watchlist.png differ
diff --git a/assets/lighthouse/profile.png b/assets/lighthouse/profile.png
new file mode 100644
index 0000000000000000000000000000000000000000..49213d89e27aef3a2d37fc8a04debdc33893abf4
Binary files /dev/null and b/assets/lighthouse/profile.png differ
diff --git a/assets/lighthouse/signin.png b/assets/lighthouse/signin.png
new file mode 100644
index 0000000000000000000000000000000000000000..80bfebfd18bdccdb606689053a585f9456f9863b
Binary files /dev/null and b/assets/lighthouse/signin.png differ
diff --git a/assets/lighthouse/signup.png b/assets/lighthouse/signup.png
new file mode 100644
index 0000000000000000000000000000000000000000..2941c4aff19d751f609226009997657a847d13c8
Binary files /dev/null and b/assets/lighthouse/signup.png differ
diff --git a/assets/lighthouse/watchlist-delete.png b/assets/lighthouse/watchlist-delete.png
new file mode 100644
index 0000000000000000000000000000000000000000..d66eddf49b9fbc1b64091ddbdd3158b6e0824b38
Binary files /dev/null and b/assets/lighthouse/watchlist-delete.png differ
diff --git a/assets/lighthouse/watchlist-detail.png b/assets/lighthouse/watchlist-detail.png
new file mode 100644
index 0000000000000000000000000000000000000000..ed087cee6f6e31644d3c64f3c16105eccc99a20f
Binary files /dev/null and b/assets/lighthouse/watchlist-detail.png differ
diff --git a/assets/lighthouse/watchlist-edit.png b/assets/lighthouse/watchlist-edit.png
new file mode 100644
index 0000000000000000000000000000000000000000..ad85ffa30b77eb6ae8bbbe9cb5f40761f39d6510
Binary files /dev/null and b/assets/lighthouse/watchlist-edit.png differ
diff --git a/docker-compose.yml b/docker-compose.yml
index 7c5e0aeeb93e6ef1a07efefaf9b94d87a8878aad..e29b6893d953a795cce2214bcbf9237e5ba1faef 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,5 +2,25 @@ version: '3.3'
 services:
   web:
     image: tubes-1:latest
+    volumes:
+      - ./src/server:/var/www
+      - ./src/public:/var/www/html
     ports:
       - 8008:80
+  db:
+    image: postgres:latest
+    restart: always
+    volumes:
+      - "./db:/var/lib/postgresql/data"
+      - "./src/migration:/docker-entrypoint-initdb.d/"
+    ports:
+      - "5432:5432"
+    environment:
+      POSTGRES_USER: ${POSTGRES_USER}
+      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
+      POSTGRES_DB: ${POSTGRES_DB}
+  adminer:
+    image: adminer:latest
+    restart: always
+    ports:
+      - "8080:8080"
diff --git a/php.ini b/php.ini
new file mode 100644
index 0000000000000000000000000000000000000000..4e75d5fcb6dcc57e1506a0cb77065d1f06886980
--- /dev/null
+++ b/php.ini
@@ -0,0 +1,3 @@
+file_uploads = On
+upload_max_filesize = 100M
+post_max_size = 100M
\ No newline at end of file
diff --git a/src/migration/db.sql b/src/migration/db.sql
new file mode 100644
index 0000000000000000000000000000000000000000..14fc6159cb1cb3697b2f5810931d6c8acbf82a7c
--- /dev/null
+++ b/src/migration/db.sql
@@ -0,0 +1,231 @@
+DO $$
+BEGIN
+    IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'role') THEN
+        CREATE TYPE role AS ENUM ('BASIC', 'ADMIN');
+    END IF;
+END
+$$;
+
+CREATE TABLE IF NOT EXISTS users (
+    id SERIAL PRIMARY KEY,
+    uuid VARCHAR(36) NOT NULL UNIQUE,
+    name VARCHAR(40) NOT NULL,
+    password VARCHAR(255) NOT NULL,
+    email VARCHAR(255) UNIQUE NOT NULL,
+    role role DEFAULT 'BASIC' NOT NULL,
+
+    created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
+    updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS sessions (
+    id VARCHAR(16) PRIMARY KEY,
+    user_id INT NOT NULL,
+    expired TIMESTAMPTZ NOT NULL,
+
+    created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
+    updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
+    FOREIGN KEY (user_id) REFERENCES users(id)
+);
+
+DO $$
+BEGIN
+    IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'category') THEN
+        CREATE TYPE category AS ENUM ('ANIME', 'DRAMA', 'MIXED');
+    END IF;
+END
+$$;
+
+CREATE TABLE IF NOT EXISTS catalogs (
+    id SERIAL PRIMARY KEY,
+    uuid VARCHAR(36) NOT NULL UNIQUE,
+    title VARCHAR(100) NOT NULL,
+    description VARCHAR(255),
+    poster VARCHAR(255) NOT NULL,
+    trailer VARCHAR(255),
+    category category NOT NULL,
+
+    created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
+    updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
+
+    CHECK (category IN ('ANIME', 'DRAMA'))
+);
+
+CREATE OR REPLACE FUNCTION updated_at()
+RETURNS TRIGGER
+LANGUAGE PLPGSQL
+AS $$ 
+BEGIN
+    NEW.updated_at = NOW();
+    return NEW;
+END;
+$$;
+
+CREATE OR REPLACE TRIGGER t_user_updated_at BEFORE UPDATE 
+ON users FOR EACH ROW EXECUTE PROCEDURE updated_at();
+
+CREATE OR REPLACE TRIGGER t_catalog_updated_at BEFORE UPDATE 
+ON catalogs FOR EACH ROW EXECUTE PROCEDURE updated_at();
+
+DO $$
+BEGIN
+    IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'visibility') THEN
+        CREATE TYPE visibility AS ENUM ('PRIVATE', 'PUBLIC');
+    END IF;
+END
+$$;
+
+CREATE TABLE IF NOT EXISTS watchlists (
+    id SERIAL NOT NULL PRIMARY KEY,
+    uuid VARCHAR(36) NOT NULL UNIQUE,
+    title VARCHAR(40) NOT NULL,
+    description VARCHAR(255),
+    category category NOT NULL DEFAULT 'MIXED',
+    user_id integer NOT NULL,
+    item_count integer DEFAULT 0 NOT NULL,
+    like_count integer DEFAULT 0 NOT NULL,
+    visibility visibility DEFAULT 'PRIVATE' NOT NULL,
+
+    created_at timestamp DEFAULT now() NOT NULL,
+    updated_at timestamp DEFAULT now() NOT NULL,
+
+    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+);
+
+CREATE OR REPLACE TRIGGER t_watchlist_updated_at BEFORE UPDATE 
+ON watchlists FOR EACH ROW EXECUTE PROCEDURE updated_at();
+
+CREATE TABLE IF NOT EXISTS watchlist_items(
+    id SERIAL NOT NULL PRIMARY KEY,
+    uuid VARCHAR(36) NOT NULL UNIQUE,
+
+    rank INT NOT NULL,
+    description VARCHAR(255),
+    watchlist_id integer NOT NULL,
+    catalog_id integer NOT NULL,
+
+    created_at timestamp DEFAULT now() NOT NULL,
+    updated_at timestamp DEFAULT now() NOT NULL,
+
+    FOREIGN KEY (watchlist_id) REFERENCES watchlists(id) ON DELETE CASCADE,
+    FOREIGN KEY (catalog_id) REFERENCES catalogs(id) ON DELETE CASCADE
+);
+
+CREATE OR REPLACE TRIGGER t_watchlist_items_updated_at BEFORE UPDATE 
+ON watchlists FOR EACH ROW EXECUTE PROCEDURE updated_at();
+
+CREATE OR REPLACE FUNCTION watchlist_item_count() 
+RETURNS TRIGGER
+LANGUAGE PLPGSQL
+AS $$
+BEGIN
+    IF (TG_OP = 'INSERT') THEN
+        UPDATE watchlists SET item_count = item_count + 1 WHERE id = NEW.watchlist_id;
+    ELSIF (TG_OP = 'DELETE') THEN
+        UPDATE watchlists SET item_count = item_count - 1 WHERE id = OLD.watchlist_id;
+    END IF;
+    RETURN NEW;
+END;
+$$;
+
+CREATE OR REPLACE TRIGGER t_watchlist_item_count AFTER INSERT OR DELETE
+ON watchlist_items FOR EACH ROW EXECUTE PROCEDURE watchlist_item_count();
+
+CREATE TABLE IF NOT EXISTS watchlist_like (
+    id SERIAL NOT NULL PRIMARY KEY,
+    user_id integer NOT NULL,
+    watchlist_id integer NOT NULL,
+
+    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+    FOREIGN KEY (watchlist_id) REFERENCES watchlists(id) ON DELETE CASCADE
+);
+
+
+CREATE OR REPLACE FUNCTION watchlist_like_count() 
+RETURNS TRIGGER
+LANGUAGE PLPGSQL
+AS $$
+BEGIN
+    IF (TG_OP = 'INSERT') THEN
+        UPDATE watchlists SET like_count = like_count + 1 WHERE id = NEW.watchlist_id;
+    ELSIF (TG_OP = 'DELETE') THEN
+        UPDATE watchlists SET like_count = like_count - 1 WHERE id = OLD.watchlist_id;
+    END IF;
+    RETURN NEW;
+END;
+$$;
+
+CREATE OR REPLACE TRIGGER t_watchlist_like_count AFTER INSERT OR DELETE
+ON watchlist_like FOR EACH ROW EXECUTE PROCEDURE watchlist_like_count();
+
+CREATE TABLE IF NOT EXISTS watchlist_save (
+    id SERIAL NOT NULL PRIMARY KEY,
+    user_id integer NOT NULL,
+    watchlist_id integer NOT NULL,
+
+    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, 
+    FOREIGN KEY (watchlist_id) REFERENCES watchlists(id) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS comments (
+    id SERIAL NOT NULL PRIMARY KEY,
+    uuid VARCHAR(36) NOT NULL UNIQUE,
+    content VARCHAR(255) NOT NULL,
+    user_id integer NOT NULL,
+    watchlist_id integer NOT NULL,
+
+    created_at timestamp DEFAULT now() NOT NULL,
+
+    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+    FOREIGN KEY (watchlist_id) REFERENCES watchlists(id) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS tags (
+    id SERIAL PRIMARY KEY,
+    name VARCHAR(40) NOT NULL UNIQUE,
+
+    created_at timestamp DEFAULT now() NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS watchlist_tag (
+    watchlist_id integer NOT NULL,
+    tag_id integer NOT NULL,
+
+    PRIMARY KEY (watchlist_id, tag_id),
+    FOREIGN KEY (watchlist_id) REFERENCES watchlists(id) ON DELETE CASCADE,
+    FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
+);
+
+INSERT INTO users (uuid, name, password, email, role) VALUES ('72c5a265715fa26c', 'admin', '$2y$10$rAfDHA4M4ftn8K7Wx82wf.fFODD7PCE/t9CVnBwdLnTDBYjnq7ZnO', 'admin@drawl.com', 'ADMIN');
+INSERT INTO tags(name) VALUES ('ACTION'),
+                              ('ADVENTURE'),
+                              ('ANIMALS'),
+                              ('BUSINESS'),
+                              ('COMEDY'),
+                              ('CRIME'),
+                              ('DETECTIVE'),
+                              ('DOCUMENTARY'),
+                              ('DRAMA'),
+                              ('FAMILY'),
+                              ('FANTASY'),
+                              ('FOOD'),
+                              ('HISTORICAL'),
+                              ('HORROR'),
+                              ('LAW'),
+                              ('LIFE'),
+                              ('MANGA'),
+                              ('MEDICAL'),
+                              ('MATURE'),
+                              ('MYSTERY'),
+                              ('MUSIC'),
+                              ('MILITARY'),
+                              ('MELODRAMA'),
+                              ('PSYCHOLOGICAL'),
+                              ('ROMANCE'),
+                              ('SCHOOL'),
+                              ('SCI-FI'),
+                              ('SPORTS'),
+                              ('SUPERNATURAL'),
+                              ('THRILLER'),
+                              ('YOUTH')
+;
\ No newline at end of file
diff --git a/src/public/.htaccess b/src/public/.htaccess
new file mode 100644
index 0000000000000000000000000000000000000000..9e25198b5a4aad8a2399f162f7c2a40e77090aea
--- /dev/null
+++ b/src/public/.htaccess
@@ -0,0 +1,4 @@
+RewriteEngine On
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule ^(.*)$ index.php/$1 [L]
\ No newline at end of file
diff --git a/src/public/assets/fonts/karla/karla-latin-200-italic.woff2 b/src/public/assets/fonts/karla/karla-latin-200-italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..54705f8eea00e1ce119e0d6d18a79fd00c58daab
Binary files /dev/null and b/src/public/assets/fonts/karla/karla-latin-200-italic.woff2 differ
diff --git a/src/public/assets/fonts/karla/karla-latin-200-normal.woff2 b/src/public/assets/fonts/karla/karla-latin-200-normal.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..fc394ea4f8c58232ae8765336748fd9a13d2d2ae
Binary files /dev/null and b/src/public/assets/fonts/karla/karla-latin-200-normal.woff2 differ
diff --git a/src/public/assets/fonts/karla/karla-latin-400-italic.woff2 b/src/public/assets/fonts/karla/karla-latin-400-italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..9e9891f80db3dddb227925102b38c87331b26380
Binary files /dev/null and b/src/public/assets/fonts/karla/karla-latin-400-italic.woff2 differ
diff --git a/src/public/assets/fonts/karla/karla-latin-400-normal.woff2 b/src/public/assets/fonts/karla/karla-latin-400-normal.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..24d3513e4917784b44287ba856a979f6d0f0a72d
Binary files /dev/null and b/src/public/assets/fonts/karla/karla-latin-400-normal.woff2 differ
diff --git a/src/public/assets/fonts/karla/karla-latin-600-italic.woff2 b/src/public/assets/fonts/karla/karla-latin-600-italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..3d5d9024923d7a5244dfc0cbd6b8046fe158bcb6
Binary files /dev/null and b/src/public/assets/fonts/karla/karla-latin-600-italic.woff2 differ
diff --git a/src/public/assets/fonts/karla/karla-latin-600-normal.woff2 b/src/public/assets/fonts/karla/karla-latin-600-normal.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..b3be775a15eafbfe5f1d39c8a74464ec0191dfef
Binary files /dev/null and b/src/public/assets/fonts/karla/karla-latin-600-normal.woff2 differ
diff --git a/src/public/assets/fonts/karla/karla-latin-800-italic.woff2 b/src/public/assets/fonts/karla/karla-latin-800-italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..99ffa5f485f372d42158ac2c1b9d3eb8b71ab9b1
Binary files /dev/null and b/src/public/assets/fonts/karla/karla-latin-800-italic.woff2 differ
diff --git a/src/public/assets/fonts/karla/karla-latin-800-normal.woff2 b/src/public/assets/fonts/karla/karla-latin-800-normal.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..51770db63579a8bc11d7ccc593013b36b252a50a
Binary files /dev/null and b/src/public/assets/fonts/karla/karla-latin-800-normal.woff2 differ
diff --git a/src/public/assets/icons/asc.php b/src/public/assets/icons/asc.php
new file mode 100644
index 0000000000000000000000000000000000000000..ed0a098546505309928376c69d508bec676bf9e7
--- /dev/null
+++ b/src/public/assets/icons/asc.php
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-asc">
+    <path d="m3 8 4-4 4 4" />
+    <path d="M7 4v16" />
+    <path d="M11 12h4" />
+    <path d="M11 16h7" />
+    <path d="M11 20h10" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/bookmark.php b/src/public/assets/icons/bookmark.php
new file mode 100644
index 0000000000000000000000000000000000000000..367bf45fe328c8b968006b2de32493dbd3093583
--- /dev/null
+++ b/src/public/assets/icons/bookmark.php
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+    stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="icon icon-bookmark"
+    data-type="<?= $type ?? "unfilled" ?>">
+    <path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/cancel.php b/src/public/assets/icons/cancel.php
new file mode 100644
index 0000000000000000000000000000000000000000..706d750d093c02f125bddf7451798674dae73f74
--- /dev/null
+++ b/src/public/assets/icons/cancel.php
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
+    <path d="M1 13L7.00001 7.00002M7.00001 7.00002L13 1M7.00001 7.00002L1 1M7.00001 7.00002L13 13" stroke="black"
+        stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/chevron-down.php b/src/public/assets/icons/chevron-down.php
new file mode 100644
index 0000000000000000000000000000000000000000..a83082cd1ffeb85909773f843bfabee7cc33358c
--- /dev/null
+++ b/src/public/assets/icons/chevron-down.php
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+    stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down">
+    <path d="m6 9 6 6 6-6" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/clapperboard.php b/src/public/assets/icons/clapperboard.php
new file mode 100644
index 0000000000000000000000000000000000000000..f225387f69df44cac4dc80c984eebeea43ff80f0
--- /dev/null
+++ b/src/public/assets/icons/clapperboard.php
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-clapperboard">
+    <path d="M20.2 6 3 11l-.9-2.4c-.3-1.1.3-2.2 1.3-2.5l13.5-4c1.1-.3 2.2.3 2.5 1.3Z" />
+    <path d="m6.2 5.3 3.1 3.9" />
+    <path d="m12.4 3.4 3.1 4" />
+    <path d="M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/delete.php b/src/public/assets/icons/delete.php
new file mode 100644
index 0000000000000000000000000000000000000000..126721acc98239e068df4ae5d5780d946a3accfd
--- /dev/null
+++ b/src/public/assets/icons/delete.php
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="32px" height="32px" viewBox="0 0 64 64">
+    <path d="M 26.9375 4 C 26.0435 4 25.203813 4.3940781 24.632812 5.0800781 C 24.574813 5.1490781 24.527234 5.2256406 24.490234 5.3066406 L 22.357422 10 L 11 10 C 9.346 10 8 11.346 8 13 L 8 19 C 8 20.654 9.346 22 11 22 L 13 22 L 13 57 C 13 58.654 14.346 60 16 60 L 48 60 C 49.654 60 51 58.654 51 57 L 51 22 L 53 22 C 54.654 22 56 20.654 56 19 L 56 13 C 56 11.346 54.654 10 53 10 L 41.644531 10 L 39.511719 5.3066406 C 39.474719 5.2256406 39.426141 5.1480781 39.369141 5.0800781 C 38.797141 4.3940781 37.957453 4 37.064453 4 L 26.9375 4 z M 26.9375 6 L 37.0625 6 C 37.3225 6 37.569906 6.1003437 37.753906 6.2773438 L 39.447266 10 L 24.552734 10 L 26.246094 6.2773438 C 26.431094 6.1003438 26.6775 6 26.9375 6 z M 11 12 L 53 12 C 53.551 12 54 12.448 54 13 L 54 19 C 54 19.552 53.551 20 53 20 L 11 20 C 10.449 20 10 19.552 10 19 L 10 13 C 10 12.448 10.449 12 11 12 z M 14 14 C 13.448 14 13 14.447 13 15 L 13 17 C 13 17.553 13.448 18 14 18 C 14.552 18 15 17.553 15 17 L 15 15 C 15 14.447 14.552 14 14 14 z M 19 14 C 18.448 14 18 14.447 18 15 L 18 17 C 18 17.553 18.448 18 19 18 C 19.552 18 20 17.553 20 17 L 20 15 C 20 14.447 19.552 14 19 14 z M 24 14 C 23.448 14 23 14.447 23 15 L 23 17 C 23 17.553 23.448 18 24 18 C 24.552 18 25 17.553 25 17 L 25 15 C 25 14.447 24.552 14 24 14 z M 29 14 C 28.448 14 28 14.447 28 15 L 28 17 C 28 17.553 28.448 18 29 18 C 29.552 18 30 17.553 30 17 L 30 15 C 30 14.447 29.552 14 29 14 z M 35 14 C 34.448 14 34 14.447 34 15 L 34 17 C 34 17.553 34.448 18 35 18 C 35.552 18 36 17.553 36 17 L 36 15 C 36 14.447 35.552 14 35 14 z M 40 14 C 39.448 14 39 14.447 39 15 L 39 17 C 39 17.553 39.448 18 40 18 C 40.552 18 41 17.553 41 17 L 41 15 C 41 14.447 40.552 14 40 14 z M 45 14 C 44.448 14 44 14.447 44 15 L 44 17 C 44 17.553 44.448 18 45 18 C 45.552 18 46 17.553 46 17 L 46 15 C 46 14.447 45.552 14 45 14 z M 50 14 C 49.448 14 49 14.447 49 15 L 49 17 C 49 17.553 49.448 18 50 18 C 50.552 18 51 17.553 51 17 L 51 15 C 51 14.447 50.552 14 50 14 z M 15 22 L 49 22 L 49 57 C 49 57.552 48.551 58 48 58 L 16 58 C 15.449 58 15 57.552 15 57 L 15 56 L 38 56 C 38.552 56 39 55.553 39 55 C 39 54.447 38.552 54 38 54 L 15 54 L 15 22 z M 20 28 C 19.448 28 19 28.447 19 29 L 19 41 C 19 41.553 19.448 42 20 42 C 20.552 42 21 41.553 21 41 L 21 29 C 21 28.447 20.552 28 20 28 z M 28 28 C 27.448 28 27 28.447 27 29 L 27 49 C 27 49.553 27.448 50 28 50 C 28.552 50 29 49.553 29 49 L 29 29 C 29 28.447 28.552 28 28 28 z M 36 28 C 35.448 28 35 28.447 35 29 L 35 49 C 35 49.553 35.448 50 36 50 C 36.552 50 37 49.553 37 49 L 37 29 C 37 28.447 36.552 28 36 28 z M 44 28 C 43.448 28 43 28.447 43 29 L 43 33 C 43 33.553 43.448 34 44 34 C 44.552 34 45 33.553 45 33 L 45 29 C 45 28.447 44.552 28 44 28 z M 44 36 C 43.448 36 43 36.447 43 37 L 43 49 C 43 49.553 43.448 50 44 50 C 44.552 50 45 49.553 45 49 L 45 37 C 45 36.447 44.552 36 44 36 z M 20 44 C 19.448 44 19 44.447 19 45 L 19 49 C 19 49.553 19.448 50 20 50 C 20.552 50 21 49.553 21 49 L 21 45 C 21 44.447 20.552 44 20 44 z M 42 54 C 41.448 54 41 54.447 41 55 C 41 55.553 41.448 56 42 56 L 46 56 C 46.552 56 47 55.553 47 55 C 47 54.447 46.552 54 46 54 L 42 54 z"></path>
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/desc.php b/src/public/assets/icons/desc.php
new file mode 100644
index 0000000000000000000000000000000000000000..c19b68124ee5efdc515d9c9b79fe36994ea308fa
--- /dev/null
+++ b/src/public/assets/icons/desc.php
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down-wide-narrow">
+    <path d="m3 16 4 4 4-4" />
+    <path d="M7 20V4" />
+    <path d="M11 4h10" />
+    <path d="M11 8h7" />
+    <path d="M11 12h4" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/dot.php b/src/public/assets/icons/dot.php
new file mode 100644
index 0000000000000000000000000000000000000000..dbbe2607563c72e598a7ed698f22fd3ff94bdfdd
--- /dev/null
+++ b/src/public/assets/icons/dot.php
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-dot">
+    <circle cx="12.1" cy="12.1" r="1" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/edit.php b/src/public/assets/icons/edit.php
new file mode 100644
index 0000000000000000000000000000000000000000..96560c384482c0338e165312c16461e5ca6245d8
--- /dev/null
+++ b/src/public/assets/icons/edit.php
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
+    <path
+        d="M9.96006 3.56001L12.52 1L17 5.48001L14.44 8.03998M9.96006 3.56001L1.26509 12.2549C1.09536 12.4246 1 12.6549 1 12.8949V17H5.1051C5.34515 17 5.57536 16.9047 5.7451 16.7349L14.44 8.03998M9.96006 3.56001L14.44 8.03998"
+        stroke="#0F172A" stroke-width="1.49583" stroke-linecap="round" stroke-linejoin="round" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/error.php b/src/public/assets/icons/error.php
new file mode 100644
index 0000000000000000000000000000000000000000..87c5c9a9422cb6e0512865e4361da84a0cef1748
--- /dev/null
+++ b/src/public/assets/icons/error.php
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
+    <path
+        d="M9 17C13.4182 17 17 13.4182 17 9C17 4.58172 13.4182 1 9 1C4.58172 1 1 4.58172 1 9C1 13.4182 4.58172 17 9 17Z"
+        stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+    <path d="M11.4006 12.2003L6.60059 5.80029" stroke="black" stroke-width="1.5" stroke-linecap="round"
+        stroke-linejoin="round" />
+    <path d="M6.60059 12.2003L11.4006 5.80029" stroke="black" stroke-width="1.5" stroke-linecap="round"
+        stroke-linejoin="round" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/grip-vertical.php b/src/public/assets/icons/grip-vertical.php
new file mode 100644
index 0000000000000000000000000000000000000000..98136a2beb965ba854dd6d42c76881e89a5989ed
--- /dev/null
+++ b/src/public/assets/icons/grip-vertical.php
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-grip-vertical">
+    <circle cx="9" cy="12" r="1" />
+    <circle cx="9" cy="5" r="1" />
+    <circle cx="9" cy="19" r="1" />
+    <circle cx="15" cy="12" r="1" />
+    <circle cx="15" cy="5" r="1" />
+    <circle cx="15" cy="19" r="1" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/info.php b/src/public/assets/icons/info.php
new file mode 100644
index 0000000000000000000000000000000000000000..217ef2466fbf62301feabd4d6cb86bc642cf3aee
--- /dev/null
+++ b/src/public/assets/icons/info.php
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="2" height="3" viewBox="0 0 2 3" fill="none">
+    <path d="M1 1.30598L1.006 1.29932" stroke="black" stroke-width="1.5" stroke-linecap="round"
+        stroke-linejoin="round" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/lock.php b/src/public/assets/icons/lock.php
new file mode 100644
index 0000000000000000000000000000000000000000..1ac6723f62583198e074f416fe3bda2ec0c372fa
--- /dev/null
+++ b/src/public/assets/icons/lock.php
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="12" height="14" viewBox="0 0 12 14" fill="none">
+    <path
+        d="M9 7H10.05C10.2985 7 10.5 7.20145 10.5 7.45V12.55C10.5 12.7985 10.2985 13 10.05 13H1.95C1.70147 13 1.5 12.7985 1.5 12.55V7.45C1.5 7.20145 1.70147 7 1.95 7H3M9 7V4C9 3 8.4 1 6 1C3.6 1 3 3 3 4V7M9 7H3"
+        stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/love.php b/src/public/assets/icons/love.php
new file mode 100644
index 0000000000000000000000000000000000000000..86cee7cd8a9ecbb77bf93b8061146838fb28164b
--- /dev/null
+++ b/src/public/assets/icons/love.php
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+    stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="icon icon-heart"
+    data-type="<?= $type ?? "unfilled" ?>">
+    <path
+        d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/menu.php b/src/public/assets/icons/menu.php
new file mode 100644
index 0000000000000000000000000000000000000000..bc4b8af3a410394c93c11e368c980ec2121c2b02
--- /dev/null
+++ b/src/public/assets/icons/menu.php
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18" height="14" viewBox="0 0 18 14" fill="none">
+    <path d="M1 1H16.4286" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+    <path d="M1 7H16.4286" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+    <path d="M1 13H16.4286" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/plus.php b/src/public/assets/icons/plus.php
new file mode 100644
index 0000000000000000000000000000000000000000..6517d7a94e602745f29b27333ef7a2b1ae6dcd76
--- /dev/null
+++ b/src/public/assets/icons/plus.php
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus">
+    <path d="M5 12h14" />
+    <path d="M12 5v14" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/search.php b/src/public/assets/icons/search.php
new file mode 100644
index 0000000000000000000000000000000000000000..e6aca1214ea9f64b288a2822f755d8e2c8c88ab9
--- /dev/null
+++ b/src/public/assets/icons/search.php
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search">
+    <circle cx="11" cy="11" r="8" />
+    <path d="m21 21-4.3-4.3" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/success.php b/src/public/assets/icons/success.php
new file mode 100644
index 0000000000000000000000000000000000000000..bd6348451040b9a68d5e49576020b114c044485e
--- /dev/null
+++ b/src/public/assets/icons/success.php
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
+    <path
+        d="M7 13C10.3137 13 13 10.3137 13 7C13 3.68629 10.3137 1 7 1C3.68629 1 1 3.68629 1 7C1 10.3137 3.68629 13 7 13Z"
+        stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/trash.php b/src/public/assets/icons/trash.php
new file mode 100644
index 0000000000000000000000000000000000000000..2ee21928e606ff05b65249181a4e81763e331708
--- /dev/null
+++ b/src/public/assets/icons/trash.php
@@ -0,0 +1,12 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
+    <path
+        d="M15.2218 8.11133V16.4669C15.2218 16.7615 14.983 17.0002 14.6885 17.0002H3.31068C3.01613 17.0002 2.77734 16.7615 2.77734 16.4669V8.11133"
+        stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+    <path d="M7.22266 13.4447V8.11133" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round"
+        stroke-linejoin="round" />
+    <path d="M10.7773 13.4447V8.11133" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round"
+        stroke-linejoin="round" />
+    <path
+        d="M17 4.55556H12.5556M12.5556 4.55556V1.53333C12.5556 1.23878 12.3168 1 12.0222 1H5.97778C5.68323 1 5.44444 1.23878 5.44444 1.53333V4.55556M12.5556 4.55556H5.44444M1 4.55556H5.44444"
+        stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/user.php b/src/public/assets/icons/user.php
new file mode 100644
index 0000000000000000000000000000000000000000..0084567817ae83f90d4986c308c750df18a81dbc
--- /dev/null
+++ b/src/public/assets/icons/user.php
@@ -0,0 +1,10 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="21" height="22" viewBox="0 0 21 22" fill="none">
+    <path
+        d="M10.5 1.5C5.25329 1.5 1 5.75329 1 11C1 16.2467 5.25329 20.5 10.5 20.5C15.7467 20.5 20 16.2467 20 11C20 5.75329 15.7467 1.5 10.5 1.5Z"
+        stroke="#1E2124" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
+    <path d="M3.15747 17.0284C3.15747 17.0284 5.27504 14.325 10.5 14.325C15.725 14.325 17.8427 17.0284 17.8427 17.0284"
+        stroke="#1E2124" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
+    <path
+        d="M10.4999 11C12.074 11 13.3499 9.7241 13.3499 8.15005C13.3499 6.57604 12.074 5.30005 10.4999 5.30005C8.92585 5.30005 7.6499 6.57604 7.6499 8.15005C7.6499 9.7241 8.92585 11 10.4999 11Z"
+        stroke="#1E2124" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/icons/x.php b/src/public/assets/icons/x.php
new file mode 100644
index 0000000000000000000000000000000000000000..b5aceff64dd9692669d48b3680d022fc93af4dd1
--- /dev/null
+++ b/src/public/assets/icons/x.php
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-x">
+    <path d="M18 6 6 18" />
+    <path d="m6 6 12 12" />
+</svg>
\ No newline at end of file
diff --git a/src/public/assets/images/Suzume.webp b/src/public/assets/images/Suzume.webp
new file mode 100644
index 0000000000000000000000000000000000000000..b7d441f7ad5de6a1d34fcc12f7ee0fc8e86ce6af
Binary files /dev/null and b/src/public/assets/images/Suzume.webp differ
diff --git a/src/public/assets/images/Tomorrow.webp b/src/public/assets/images/Tomorrow.webp
new file mode 100644
index 0000000000000000000000000000000000000000..07da06b6da6d18be0aea5b513a5b7d735f6c913c
Binary files /dev/null and b/src/public/assets/images/Tomorrow.webp differ
diff --git a/src/public/assets/images/catalogs/posters/5a5ac4ad0c3a5e7c.webp b/src/public/assets/images/catalogs/posters/5a5ac4ad0c3a5e7c.webp
new file mode 100644
index 0000000000000000000000000000000000000000..0b915c9f2363cf4fff97c124da1ebd98f3012b50
Binary files /dev/null and b/src/public/assets/images/catalogs/posters/5a5ac4ad0c3a5e7c.webp differ
diff --git a/src/public/assets/images/catalogs/posters/5a6b36907cd0f469.webp b/src/public/assets/images/catalogs/posters/5a6b36907cd0f469.webp
new file mode 100644
index 0000000000000000000000000000000000000000..05cd190eba43ef330ebd42a0909d34fdd45cb3bc
Binary files /dev/null and b/src/public/assets/images/catalogs/posters/5a6b36907cd0f469.webp differ
diff --git a/src/public/assets/images/catalogs/posters/no-poster.webp b/src/public/assets/images/catalogs/posters/no-poster.webp
new file mode 100644
index 0000000000000000000000000000000000000000..f5a0f34071dd9e57d1c1ced894189082f60c6140
Binary files /dev/null and b/src/public/assets/images/catalogs/posters/no-poster.webp differ
diff --git a/src/public/assets/videos/catalogs/trailers/6517bd1c9f2a7_Frieren b/src/public/assets/videos/catalogs/trailers/6517bd1c9f2a7_Frieren
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/public/assets/videos/catalogs/trailers/6517bd1c9f2a7_FrierenBeyond's the Journey Ends.mp4 b/src/public/assets/videos/catalogs/trailers/6517bd1c9f2a7_FrierenBeyond's the Journey Ends.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..99c1c2430523e47ea2c8a065c65278533fc26d0a
Binary files /dev/null and b/src/public/assets/videos/catalogs/trailers/6517bd1c9f2a7_FrierenBeyond's the Journey Ends.mp4 differ
diff --git a/src/public/assets/videos/catalogs/trailers/a3e992b0d939a896.mp4 b/src/public/assets/videos/catalogs/trailers/a3e992b0d939a896.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..99c1c2430523e47ea2c8a065c65278533fc26d0a
Binary files /dev/null and b/src/public/assets/videos/catalogs/trailers/a3e992b0d939a896.mp4 differ
diff --git a/src/public/assets/videos/catalogs/trailers/the-journey-of-elaina.mp4 b/src/public/assets/videos/catalogs/trailers/the-journey-of-elaina.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..99c1c2430523e47ea2c8a065c65278533fc26d0a
Binary files /dev/null and b/src/public/assets/videos/catalogs/trailers/the-journey-of-elaina.mp4 differ
diff --git a/src/public/css/catalog-detail.css b/src/public/css/catalog-detail.css
new file mode 100644
index 0000000000000000000000000000000000000000..9c1716ce00c6d8012fdd077ceb00c34f9ecebc1b
--- /dev/null
+++ b/src/public/css/catalog-detail.css
@@ -0,0 +1,77 @@
+.catalog-detail-header {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    align-self: stretch;
+}
+
+.catalog-detail-header-poster {
+    background-color: var(--accent-400);
+    width: 100%;
+    height: 240px;
+}
+
+.catalog-detail-content {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 10px;
+    flex: 1 0 0;
+    margin: 0 12px;
+}
+
+.catalog-detail-content h2 {
+    width: 100%;
+    overflow-wrap: break-word;
+}
+
+.poster {
+    margin-top: -100px;
+    margin-left: 12px;
+}
+
+.button-container {
+    position: fixed;
+    bottom: 20px;
+    right: 20px;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+}
+
+.btn-icon {
+    background: #FFF;
+    border: 1px solid rgba(0, 0, 0, .1);
+    cursor: pointer;
+    padding: 10px 10px;
+    border-radius: 6px;
+}
+
+.btn-icon:hover {
+    background-color: var(--accent-300);
+    transition: all;
+    transition-duration: 400ms;
+    transition-timing-function: ease-in-out;
+}
+
+@media screen and (min-width: 640px) {
+    .poster {
+        margin-left: 32px;
+        margin-top: -40px;
+    }
+
+    .catalog-detail-content {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 10px;
+        flex: 1 0 0;
+        margin: 0 32px;
+    }
+
+    .button-container {
+        bottom: 40px;
+        right: 3rem;
+        gap: 10px;
+    }
+}
diff --git a/src/public/css/catalog-form.css b/src/public/css/catalog-form.css
new file mode 100644
index 0000000000000000000000000000000000000000..943817704fa2c4c29d09255a37f968e2de8698fe
--- /dev/null
+++ b/src/public/css/catalog-form.css
@@ -0,0 +1,16 @@
+form {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    gap: 1rem;
+}
+
+form > button {
+    margin-top: 40px;
+}
+
+@media screen and (min-width: 640px) {
+    .container {
+        width: 800px;
+    }
+  }
\ No newline at end of file
diff --git a/src/public/css/catalog.css b/src/public/css/catalog.css
new file mode 100644
index 0000000000000000000000000000000000000000..e0a38b10e46d7b653deb5185d836ea94dfe22638
--- /dev/null
+++ b/src/public/css/catalog.css
@@ -0,0 +1,29 @@
+form {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 1rem;
+}
+
+.input {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+
+.search-filter {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    align-items: center;
+    justify-content: space-between;
+    gap: 2rem;
+    align-self: stretch;
+}
+
+@media screen and (min-width: 640px) {
+    form {
+        display: flex;
+        align-items: center;
+        gap: 1.5rem;
+    }
+}
\ No newline at end of file
diff --git a/src/public/css/components/modal/watchlistAddItem.css b/src/public/css/components/modal/watchlistAddItem.css
new file mode 100644
index 0000000000000000000000000000000000000000..ea9037ec5b13185ec3c99b9da80c73aff48b5dfa
--- /dev/null
+++ b/src/public/css/components/modal/watchlistAddItem.css
@@ -0,0 +1,23 @@
+.form-search {
+    display: flex;
+}
+
+.search {
+    display: flex;
+    align-items: center;
+    width: 100%;
+    gap: 0.4rem;
+    margin: 0 0 1rem 0;
+}
+
+.search__input {
+    flex-grow: 1;
+}
+
+.search__items {
+    display: flex;
+    padding: 0 0 1rem 0;
+    flex-direction: column;
+    max-height: 60vh;
+    overflow: auto;
+}
\ No newline at end of file
diff --git a/src/public/css/components/modal/watchlistAddSearchItem.css b/src/public/css/components/modal/watchlistAddSearchItem.css
new file mode 100644
index 0000000000000000000000000000000000000000..dd5dbaa80a022629b8e5122cbd93572da7a842d5
--- /dev/null
+++ b/src/public/css/components/modal/watchlistAddSearchItem.css
@@ -0,0 +1,34 @@
+.search-item {
+    display: flex;
+    gap: 1rem;
+    align-items: center;
+    padding: 1rem 0;
+}
+
+.search-item:not(:last-child) {
+    border-bottom: 1px solid rgba(0, 0, 0, .1);
+}
+
+.search-item__poster {
+    height: 8rem;
+    width: 5.4rem;
+    object-fit: cover;
+    border-radius: 0.6rem;
+}
+
+.search-item__content {
+    display: flex;
+    flex-direction: column;
+    gap: 0.4rem;
+    flex-grow: 1;
+    width: 50%;
+}
+
+.search-item__action {
+    padding: 0.2rem;
+}
+
+.search-item__title {
+    width: 100%;
+    overflow-wrap: break-word;
+}
\ No newline at end of file
diff --git a/src/public/css/components/watchlist/watchlistItem.css b/src/public/css/components/watchlist/watchlistItem.css
new file mode 100644
index 0000000000000000000000000000000000000000..29e47a31e347987421e3b6ae2f9731ac143a8b0a
--- /dev/null
+++ b/src/public/css/components/watchlist/watchlistItem.css
@@ -0,0 +1,64 @@
+.watchlist-item {
+    display: flex;
+    align-items: center;
+    gap: 1rem;
+    width: 100%;
+    padding: 1rem 0;
+    cursor: move;
+}
+
+.watchlist-item:not(:last-child) {
+    border-bottom: 1px solid rgba(0, 0, 0, .1);
+}
+
+.watchlist-item__wrapper {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    gap: 0.4rem;
+    width: 100%;
+}
+
+.watchlist-item__poster {
+    height: 8rem;
+    width: 5.4rem;
+    border-radius: 0.4rem;
+    object-fit: cover;
+}
+
+.watchlist-item__content {
+    display: flex;
+    flex-direction: column;
+    gap: 1rem;
+    width: 100%;
+}
+/* .watchlist-item__description {
+    font-size: 0.8rem;
+} */
+
+.watchlist-item__actions {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: star;
+    gap: 2rem;
+}
+
+.watchlist-item__delete {
+    padding: 0;
+}
+
+.dragging {
+    opacity: .5;
+}
+
+@media screen and (min-width: 640px) {
+    /* .watchlist-item__description {
+        font-size: 1rem;
+    } */
+    .watchlist-item__poster {
+        height: 10rem;
+        width: 7.4rem;
+    }
+}
\ No newline at end of file
diff --git a/src/public/css/editProfile.css b/src/public/css/editProfile.css
new file mode 100644
index 0000000000000000000000000000000000000000..cf3034eb7efe8018c481169791e731dcf3dad427
--- /dev/null
+++ b/src/public/css/editProfile.css
@@ -0,0 +1,100 @@
+
+
+
+.edit-parameters {
+    align-self: stretch;
+    height: 415px;
+    padding-bottom: 24px;
+    flex-direction: column;
+    justify-content: flex-start;
+    align-items: flex-start;
+    gap: 16px;
+    display: flex;
+}
+
+.my-profile-container {
+    align-self: stretch;
+    height: 24px;
+    flex-direction: column;
+    justify-content: flex-start;
+    align-items: flex-start;
+    gap: 32px;
+    display: flex
+}
+
+
+.display-name {
+    align-self: stretch;
+    height: 44px;
+    flex-direction: column;
+    justify-content: flex-start;
+    align-items: flex-start;
+    gap: 8px;
+    display: flex
+}
+
+
+.input-container {
+    width: 100%; 
+    height: 100%; 
+    justify-content: flex-start; 
+    align-items: flex-start; 
+    gap: 12px; 
+    display: inline-flex
+}
+
+.input-box {
+    flex: 1 1 0; 
+    flex-direction: column; 
+    justify-content: flex-start; 
+    align-items: flex-start; 
+    gap: 6px; 
+    display: inline-flex; 
+}
+
+.password.display-name {
+    align-self: stretch; 
+    height: 231px; 
+    flex-direction: column; 
+    justify-content: flex-start; 
+    align-items: flex-start; 
+    gap: 8px; 
+    display: flex
+}
+
+.password-button-container {
+    align-self: stretch;
+    height: 204px;  
+    gap: 24px; 
+}
+
+.password-container {
+    align-self: stretch; 
+    height: 140px;  
+    gap: 8px; 
+}
+
+.password-title {
+    align-self: stretch; 
+    height: 66px; 
+    gap: 10px; 
+}
+
+.password-texts {
+    justify-content: center; 
+    align-items: center; 
+    gap: 10px; 
+    display: inline-flex
+}
+
+.red-star {
+    color: #FC7979; 
+    font-size: 14px;  
+    font-weight: 400; 
+    line-height: 20px;
+}
+
+
+
+
+
diff --git a/src/public/css/error-page.css b/src/public/css/error-page.css
new file mode 100644
index 0000000000000000000000000000000000000000..ccf06a80978525b2ca6684611cd90107c165af66
--- /dev/null
+++ b/src/public/css/error-page.css
@@ -0,0 +1,55 @@
+.error-page-container div:has(h1) {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    flex: 1 0 0;
+    align-self: stretch;
+}
+
+.error-page-container h1 {
+    color: var(--accent-800, #8C48A8);
+    font-size: 10rem;
+    font-weight: bolder;
+    mix-blend-mode: color-dodge;    
+}
+
+.error-page-container h2 {
+    font-size: 1.5rem;
+    font-weight: bolder;
+    text-align: center;
+}
+
+.error-page-container p {
+    font-size: 1rem;
+    text-align: center;
+}
+
+.error-page-container div:has(h2) {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    gap: 16px;
+    align-self: stretch;
+}
+
+.error-page-container a {
+    width: fit-content;
+    margin-top: 24px;
+}
+
+.error-page-container span {
+    filter: blur(2px);    
+}
+
+@media screen  and  (min-width: 640px){
+    .error-page-container h1 {
+        font-size: 12rem;
+    }
+    
+    .error-page-container h2 {
+        font-size: 2rem;
+    }
+}
\ No newline at end of file
diff --git a/src/public/css/global.css b/src/public/css/global.css
new file mode 100644
index 0000000000000000000000000000000000000000..d358c67392517cb9609de03ae4b61e13da149238
--- /dev/null
+++ b/src/public/css/global.css
@@ -0,0 +1,1299 @@
+:root {
+    --primary-100: #424549;
+    --primary-200: #36393E;
+    --primary-300: #282B30;
+    --primary-400: #1E2124;
+
+    --accent-100: #FBF7FD;
+    --accent-200: #F5EDFA;
+    --accent-300: #EDDEF6;
+    --accent-400: #DFC4EE;
+    --accent-500: #CC9EE2;
+    --accent-600: #BD84D7;
+    --accent-700: #A35BC2;
+    --accent-800: #8C48A8;
+    --accent-900: #753F8A;
+    --accent-1000: #5F346F;
+    --accent-1100: #421B50;
+
+    --slate-100: #f1f5f9;
+    --slate-200: #e2e8f0;
+    --slate-300: #cbd5e1;
+    --slate-400: #94a3b8;
+    --slate-500: #64748b;
+    --slate-600: #475569;
+    --slate-700: #334155;
+    --slate-800: #1e293b;
+    --slate-900: #0f172a;
+
+    --error-background: #FEE2E2;
+    --error-foreground: #B91C1C;
+
+    --info-background: #dbeafe;
+    --info-foreground: #1d4ed8;
+
+    --success-background: #dcfce7;
+    --success-foreground: #15803d;
+}
+
+@font-face {
+    font-family: 'Karla';
+    src: url(/assets/fonts/karla/karla-latin-200-normal.woff2);
+    font-weight: 200;
+    font-style: normal;
+    font-display: swap;
+
+}
+
+@font-face {
+    font-family: 'Karla';
+    src: url(/assets/fonts/karla/karla-latin-200-italic.woff2);
+    font-weight: 200;
+    font-style: italic;
+    font-display: swap;
+
+}
+
+@font-face {
+    font-family: 'Karla';
+    src: url(/assets/fonts/karla/karla-latin-400-normal.woff2);
+    font-weight: 400;
+    font-style: normal;
+    font-display: swap;
+}
+
+@font-face {
+    font-family: 'Karla';
+    src: url(/assets/fonts/karla/karla-latin-400-italic.woff2);
+    font-weight: 400;
+    font-style: italic;
+    font-display: swap;
+
+}
+
+@font-face {
+    font-family: 'Karla';
+    src: url(/assets/fonts/karla/karla-latin-600-normal.woff2);
+    font-weight: 600;
+    font-style: normal;
+    font-display: swap;
+}
+
+@font-face {
+    font-family: 'Karla';
+    src: url(/assets/fonts/karla/karla-latin-600-italic.woff2);
+    font-weight: 600;
+    font-style: italic;
+    font-display: swap;
+}
+
+@font-face {
+    font-family: 'Karla';
+    src: url(/assets/fonts/karla/karla-latin-800-normal.woff2);
+    font-weight: 800;
+    font-style: normal;
+    font-display: swap;
+}
+
+@font-face {
+    font-family: 'Karla';
+    src: url(/assets/fonts/karla/karla-latin-800-italic.woff2);
+    font-weight: 800;
+    font-style: italic;
+    font-display: swap;
+}
+
+* {
+    inset: unset;
+    margin: 0;
+    padding: 0;
+    box-sizing: border-box;
+    font-family: 'Karla';
+    font-weight: 400;
+}
+
+body {
+    position: relative;
+    display: flex;
+    min-height: 100vh;
+    flex-direction: column;
+    color: var(--primary-300);
+}
+
+/* Typography */
+
+h2 {
+    color: var(--accent-800);
+    font-size: 1.5rem;
+    font-style: normal;
+    font-weight: 600;
+    line-height: 1.5rem;
+}
+
+h3 {
+    color: var(--accent-800);
+    font-weight: 600;
+    font-size: 1rem;
+}
+
+h4 {
+    color: var(--accent-800, #8C48A8);
+}
+
+p {
+    color: var(--slate-700, #334155);
+    font-size: 0.875rem;
+    font-style: normal;
+    font-weight: 400;
+    line-height: normal;
+    overflow-wrap: break-word;
+    width: 100%;
+}
+
+p.subtitle,
+span.subtitle {
+    color: var(--slate-600, #94A3B8);
+    font-size: 0.875rem;
+    font-style: normal;
+    font-weight: 400;
+    line-height: normal;
+}
+
+a {
+    text-decoration: none;
+    color: var(--primary-300);
+}
+
+.title {
+    color: var(--accent-800);
+}
+
+.brand__title {
+    font-size: 1.5rem;
+    color: var(--accent-800);
+    font-weight: 800;
+}
+
+.span-icon {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+/* Layout */
+
+main,
+.container {
+    display: flex;
+    flex-direction: column;
+    padding: 0 1rem;
+    max-width: 80rem;
+    width: 100%;
+    margin: 3rem auto;
+    position: relative;
+    z-index: 10;
+    gap: 2rem;
+    min-height: 80vh;
+}
+
+.alert-error {
+    display: flex;
+    background-color: var(--accent-300);
+    width: 100%;
+    padding: 0.4rem;
+    border-radius: 0.4rem;
+}
+
+.hidden {
+    display: none !important;
+}
+
+/* Button */
+button {
+    background: none;
+    border: none;
+    cursor: pointer;
+    border-radius: 6px;
+    padding: 8px 16px;
+    gap: 8px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-weight: 600;
+    white-space: nowrap;
+    text-transform: uppercase;
+}
+
+button:hover {
+    background-color: var(--accent-300);
+    transition: all;
+    transition-duration: 400ms;
+    transition-timing-function: ease-in-out;
+}
+
+.btn {
+    background: none;
+    border: none;
+    font: inherit;
+    color: inherit;
+    cursor: pointer;
+    border-radius: 6px;
+    padding: 8px 16px;
+    gap: 8px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-weight: 600;
+    white-space: nowrap;
+    text-transform: uppercase;
+}
+
+.btn:hover {
+    background-color: var(--accent-300);
+    transition: all;
+    transition-duration: 400ms;
+    transition-timing-function: ease-in-out;
+}
+
+.btn-icon {
+    padding: 10px 10px;
+}
+
+.btn-bold {
+    background: var(--accent-1100, #421B50);
+    color: var(--accent-100, #FBF7FD);
+}
+
+.btn-bold:hover {
+    background-color: var(--accent-1000, #5F346F);
+    transition: all;
+    transition-duration: 400ms;
+    transition-timing-function: ease-in-out;
+}
+
+.btn-primary {
+    display: inline-flex;
+    width: 100%;
+    max-width: fit-content;
+    text-decoration: none;
+    color: var(--primary-400);
+    background-color: var(--accent-300);
+    padding: 8px 12px;
+    border-radius: 5px;
+    border: none;
+    cursor: pointer;
+    align-items: center;
+    justify-content: center;
+    gap: 0.4rem;
+    font-size: 1rem;
+}
+
+.btn-primary:hover {
+    background-color: var(--accent-300);
+    transition: all;
+    transition-duration: 400ms;
+    transition-timing-function: ease-in-out;
+}
+
+.btn-text {
+    color: var(--accent-800);
+}
+
+.btn-ghost:hover {
+    background-color: transparent;
+}
+
+.btn-outline {
+    border: 1px solid rgba(0, 0, 0, .1);
+}
+
+.btn-outline:hover {
+    background-color: var(--accent-100);
+}
+
+/* Navbar */
+
+.navbar {
+    top: 1rem;
+    position: sticky;
+    padding: 0 1rem;
+    z-index: 40;
+}
+
+.navbar-toggle {
+    padding: 10px 10px;
+}
+
+.navbar-content {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: center;
+    max-width: 80rem;
+    width: 100%;
+    background-color: rgba(255, 255, 255, 0.5);
+    backdrop-filter: blur(8px);
+    box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
+    margin: 0 auto;
+    border-radius: 0.4rem;
+    z-index: 40;
+    position: relative;
+}
+
+.navbar-header {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: center;
+    width: 100%;
+    padding: 1rem 1.5rem;
+}
+
+.navbar-menu {
+    width: 100%;
+    position: absolute;
+    top: 72px;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    justify-content: center;
+    gap: 1rem;
+    padding: 1rem 1.5rem;
+    background-color: #FFF;
+    box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
+}
+
+.navbar-menu.collapsed {
+    display: none;
+}
+
+.navbar-menu .profile-menu {
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    justify-content: center;
+    gap: 1rem;
+    background-color: #FFF;
+}
+
+.navbar-menu .profile-menu a,
+.navbar-menu .profile-menu button {
+    width: 100%;
+    align-items: flex-start;
+    justify-content: flex-start;
+}
+
+.navbar-menu .profile-menu.collapsed {
+    display: flex;
+}
+
+.navbar-menu:not(.collapsed) .profile-icon {
+    display: none;
+}
+
+.navbar-menu a {
+    width: 100%;
+    align-items: flex-start;
+    justify-content: flex-start;
+}
+
+.navbar:has(.navbar-toggle:focus) .navbar-menu {
+    display: flex;
+}
+
+@media screen and (min-width: 640px) {
+    .navbar-toggle {
+        display: none;
+    }
+
+    .navbar-menu {
+        display: flex;
+        background-color: transparent;
+        position: static;
+        flex-direction: row;
+        align-items: center;
+        justify-content: flex-end;
+        box-shadow: none;
+        gap: 0;
+    }
+
+    .navbar-menu .profile-menu {
+        width: fit-content;
+        position: absolute;
+        top: 80px;
+        padding: 1rem 1.5rem;
+        border-radius: 6px;
+        box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
+    }
+
+    .navbar-menu.collapsed {
+        display: flex;
+    }
+
+    .navbar-menu .profile-menu.collapsed {
+        display: none;
+    }
+
+    .navbar-menu a {
+        width: auto;
+    }
+
+    .navbar-header {
+        padding: none;
+    }
+}
+
+/* Content Layout */
+
+.content {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 1.5rem;
+    align-self: stretch;
+}
+
+.content .pagination {
+    display: flex;
+    align-items: flex-start;
+    margin-top: 80px;
+}
+
+/* Media */
+
+.poster {
+    transition: top ease 1s;
+    position: relative;
+    height: 8rem;
+    width: 5.4rem;
+    object-fit: cover;
+    border-radius: 0.6rem;
+    box-shadow: rgba(104, 104, 104, 0.45) 5px 2.4px 5px;
+}
+
+@media screen and (min-width: 500px) {
+    .poster {
+        height: 10rem;
+        width: 7.4rem;
+    }
+}
+
+/* Tag */
+
+.tag {
+    display: flex;
+    padding: 0px 10px;
+    justify-content: center;
+    align-items: center;
+    gap: 10px;
+    border-radius: 8px;
+    background: var(--accent-300, #EDDEF6);
+    width: fit-content;
+
+    font-size: 0.8rem;
+    font-style: normal;
+    font-weight: 400;
+    text-transform: lowercase;
+}
+
+/* Avatar */
+
+.avatar {
+    display: flex;
+    width: 2.5rem;
+    height: 2.5rem;
+    justify-content: center;
+    align-items: center;
+    flex-shrink: 0;
+    border-radius: 2.5rem;
+    object-fit: cover;
+    background: url(<path-to-image>), lightgray 50% / cover no-repeat;
+}
+
+/* Alert */
+
+.alert {
+    display: flex;
+    padding: 16px 6px;
+    justify-content: space-between;
+    align-items: flex-start;
+    align-self: stretch;
+    border-radius: 6px;
+}
+
+.alert[data-type="error"] {
+    background-color: var(--error-background);
+}
+
+.alert[data-type="error"] h3 {
+    color: var(--error-foreground);
+}
+
+.alert[data-type="error"] p {
+    color: var(--error-foreground);
+}
+
+.alert[data-type="error"] svg path {
+    stroke: var(--error-foreground);
+}
+
+.alert[data-type="info"] {
+    background-color: var(--info-background);
+}
+
+.alert[data-type="info"] h3 {
+    color: var(--info-foreground);
+}
+
+.alert[data-type="error"] p {
+    color: var(--error-foreground);
+}
+
+.alert[data-type="info"] svg path {
+    stroke: var(--info-foreground);
+}
+
+.alert[data-type="success"] {
+    background-color: var(--success-background);
+}
+
+.alert[data-type="success"] h3 {
+    color: var(--success-foreground);
+}
+
+.alert[data-type="success"] svg path {
+    stroke: var(--success-foreground);
+}
+
+.alert div {
+    display: flex;
+    padding: 0px 12px;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 6px;
+    flex: 1 0 0;
+}
+
+.alert h3 {
+    font-size: 0.875rem;
+}
+
+.alert p {
+    font-size: 0.625rem;
+}
+
+.alert > svg {
+    display: none;
+}
+
+/* LOADING */
+.loading {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 0.6rem;
+}
+
+@media screen and (min-width: 640px) {
+    .alert {
+        padding: 24px 20px;
+    }
+
+    .alert h3 {
+        font-size: 1rem;
+    }
+
+    .alert p {
+        font-size: 0.875rem;
+    }
+
+    .alert > svg {
+        display: block;
+    }
+
+    .alert div {
+        padding: 0px 24px;
+        gap: 10px;
+    }
+}
+
+/* Card */
+.card {
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-end;
+    align-self: stretch;
+    justify-content: space-between;
+    padding-bottom: 24px;
+    gap: 24px;
+    border-bottom: 1px solid var(--slate-100, #CBD5E1);
+}
+
+.card-content {
+    width: 100%;
+    display: flex;
+    align-items: flex-start;
+    gap: 24px;
+}
+
+.card-title {
+    font-size: 1.2rem;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    max-width: 200px;
+}
+
+.card-body {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 10px;
+    width: 100%;
+    overflow: hidden;
+}
+
+.card-body > p {
+    overflow: hidden;
+    word-wrap: break-word;
+    width: 100%;
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: 3;
+}
+
+.card-button-container {
+    display: flex;
+    align-items: flex-start;
+    gap: 24px;
+}
+
+.card-comment .card-content .card-body .header {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+
+@media screen and (min-width: 640px) {
+    .card {
+        flex-direction: row;
+        align-items: flex-start;
+    }
+
+    .card-title {
+        max-width: 100%;
+    }
+
+    .card-content {
+        width: 90%;
+    }
+
+}
+
+/* Select */
+.c-select-menu {
+    min-width: 200px;
+    width: 100%;
+    position: relative;
+}
+
+.c-select-menu .c-select-btn {
+    display: flex;
+    padding: 0.35rem;
+    border: 1px solid rgba(0, 0, 0, .1);
+    border-radius: 0.4rem;
+    align-items: center;
+    justify-content: space-between;
+    cursor: pointer;
+    position: relative;
+}
+
+.c-select-menu .c-select-options {
+    position: absolute;
+    padding: 0.4rem;
+    top: 3rem;
+    background-color: #fff;
+    border: 1px solid rgba(0, 0, 0, 0.1);
+    border-radius: 0.4rem;
+    min-width: 200px;
+    width: 100%;
+    z-index: 50;
+    max-height: 20rem;
+    overflow: auto;
+}
+
+.c-select-options .c-select-option {
+    display: flex;
+    padding: 0.6rem;
+    cursor: pointer;
+    align-items: center;
+    border-radius: 0.4rem;
+}
+
+.c-select-options .c-select-option:hover {
+    background-color: var(--accent-100);
+}
+
+.c-select-hide {
+    display: none;
+}
+
+/* Pagination */
+.pagination {
+    width: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-top: 80px;
+}
+
+.pagination-elips {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    gap: 10px;
+    border-radius: 8px;
+    width: 36px;
+    height: 36px;
+}
+
+.pagination-item {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    gap: 10px;
+    border-radius: 8px;
+    width: 36px;
+    height: 36px;
+}
+
+.pagination-item:hover {
+    background-color: var(--accent-300);
+    transition: all;
+    transition-duration: 400ms;
+    transition-timing-function: ease-in-out;
+}
+
+.pagination-item[data-type="active"] {
+    background: var(--accent-300, #EDDEF6);
+}
+
+.pagination-item.prev {
+    transform: rotate(90deg);
+}
+
+.pagination-item.next {
+    transform: rotate(-90deg);
+}
+
+/* Dialog */
+
+.dialog {
+    position: fixed;
+    z-index: 100;
+    top: 0;
+    left: 0;
+    display: flex;
+    width: 100%;
+    height: 100vh;
+    justify-content: center;
+    align-items: center;
+    background-color: rgba(255, 255, 255, .5);
+}
+
+.dialog__content {
+    display: flex;
+    padding: 20px;
+    max-width: 90vw;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 20px;
+    flex-shrink: 0;
+    border-radius: 10px;
+    background: #FFF;
+    margin: 6px;
+    border: 1px solid rgba(0, 0, 0, .1);
+}
+
+.dialog__content p,
+.dialog__content h2 {
+    align-self: stretch;
+    overflow-wrap: break-word;
+    text-align: center;
+}
+
+.dialog__button-container {
+    display: flex;
+    justify-content: center;
+    align-items: flex-start;
+    gap: 20px;
+    margin-top: 20px;
+}
+
+body:has(.dialog:not(.hidden)) {
+    overflow: hidden;
+}
+
+main:has(.dialog:not(.hidden)) {
+    overflow: hidden;
+}
+
+@media screen and (min-width: 640px) {
+    .dialog__content {
+        margin: 0;
+        max-width: 500px;
+    }
+}
+
+/* Input */
+
+.input-group {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 10px;
+}
+
+.input-required::after {
+    content: '*';
+    color: #FC7979;
+    margin-left: 8px;
+}
+
+input {
+    width: 100%;
+    display: flex;
+    padding: 0.75rem;
+    border-radius: 0.4rem;
+    align-items: flex-start;
+    align-self: stretch;
+    border: 1px solid var(--slate-200, #CBD5E1);
+    background: #FFF;
+    font-style: normal;
+    font-size: 0.875rem
+}
+
+label {
+    color: var(--slate-900, #0F172A);
+    font-size: 0.875rem;
+    font-style: normal;
+    font-weight: 400;
+}
+
+textarea {
+    width: 100%;
+    display: flex;
+    height: 160px;
+    padding: 0.75rem;
+    border-radius: 0.4rem;
+    align-items: flex-start;
+    align-self: stretch;
+    border: 1px solid var(--slate-200, #CBD5E1);
+    background: #FFF;
+    font-style: normal;
+    font-size: 0.875rem;
+}
+
+input:focus {
+    outline: none;
+    outline: 1px solid var(--accent-800);
+}
+
+textarea:focus {
+    outline: none;
+    outline: 1px solid var(--accent-800);
+}
+
+
+/* Form */
+
+.form-default {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    gap: 1rem;
+}
+
+.form-input-default {
+    display: flex;
+    flex-direction: column;
+    gap: 0.4rem;
+}
+
+.form-input-radio-default {
+    display: flex;
+    align-items: center;
+    gap: 0.8rem;
+}
+
+.input-default {
+    padding: 0.75rem;
+    border-radius: 0.4rem;
+    border: 1px solid rgba(0, 0, 0, .1);
+    width: 100%;
+}
+
+.input-default:focus {
+    outline: none;
+    outline: 1px solid var(--accent-800);
+}
+
+/* Icon */
+
+.icon-heart:hover {
+    fill: var(--accent-600);
+    transition: all;
+    transition-duration: 400ms;
+    transition-timing-function: ease-in-out;
+}
+
+.icon-heart[data-type="filled"] {
+    fill: var(--accent-600);
+}
+
+.icon-heart[data-type="unfilled"] {
+    fill: transparent;
+}
+
+.icon-bookmark:hover {
+    fill: var(--accent-600);
+    transition: all;
+    transition-duration: 400ms;
+    transition-timing-function: ease-in-out;
+}
+
+.icon-bookmark[data-type="filled"] {
+    fill: var(--accent-600);
+}
+
+.icon-bookmark[data-type="unfilled"] {
+    fill: transparent;
+}
+
+/* Modal */
+
+.modal {
+    width: 100%;
+    display: flex;
+    align-items: center;
+    flex-direction: column;
+}
+
+.modal__trigger {
+    width: 100%;
+}
+
+
+.modal__content {
+    display: flex;
+    gap: 1rem;
+    position: relative;
+    width: 100%;
+    max-width: 40rem;
+    margin: 1rem;
+    flex-direction: column;
+    padding: 1rem;
+    background-color: white;
+    border-radius: 0.4rem;
+    max-height: 80vh;
+    overflow: auto;
+    box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
+}
+
+.modal__backdrop {
+    position: fixed;
+    display: none;
+    align-items: center;
+    justify-content: center;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    z-index: 1000;
+    width: 100%;
+    height: 100vh;
+    background-color: rgba(255, 255, 255, .5);
+}
+
+.modal__close {
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    max-width: fit-content;
+    padding: 0;
+}
+
+.icon-x {
+    color: rgba(0, 0, 0, .4);
+}
+
+/* Watchlist Card */
+
+.watchlist__card {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 1.6rem;
+    padding: 1.6rem 0;
+}
+
+.watchlist__card .card-content {
+    flex-direction: column;
+    align-items: center;
+}
+
+.list__poster {
+    display: flex;
+    flex-direction: row-reverse;
+    align-items: center;
+    position: relative;
+}
+
+.list__poster a:not(:first-child),
+.list__poster div:not(:first-child) {
+    margin-right: -2rem;
+}
+
+.list__poster a:hover,
+.list__poster div:hover {
+    position: relative;
+    z-index: 20 !important;
+    top: -10px;
+}
+
+.watchlist__wrapper-type-author {
+    display: flex;
+    flex-direction: row;
+    gap: 0.6rem;
+    flex-wrap: wrap;
+}
+
+.watchlist__meta {
+    display: flex;
+    flex-direction: column;
+    gap: 0.6rem;
+}
+
+.watchlist__type {
+    background-color: var(--accent-300);
+    padding: 0.2rem 0.4rem;
+    border-radius: 0.6rem;
+    max-width: fit-content;
+}
+
+.watchlist__description {
+    margin: 0.4rem 0;
+}
+
+.watchlist__card .author-name {
+    color: var(--accent-800);
+}
+
+.watchlist__item-count {
+    display: flex;
+    align-items: center;
+    gap: 0.4rem;
+    font-size: 0.875rem;
+    color: var(--accent-800);
+}
+
+.watchlist__card .icon-clapperboard {
+    width: 1.2rem;
+}
+
+.watchlist__actions {
+    display: flex;
+    flex-direction: row-reverse;
+    align-items: center;
+    justify-content: space-between;
+    gap: 1rem;
+    width: 100%;
+}
+
+.watchlist__action-save {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.watchlist__action-love {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 0.4rem;
+}
+
+.watchlist__card .btn-ghost {
+    padding: 0;
+}
+
+.watchlist__visibility-title {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+}
+
+@media screen and (min-width: 872px) {
+    .watchlist__card {
+        flex-direction: row;
+    }
+
+    .watchlist__actions {
+        flex-direction: column;
+        max-width: fit-content;
+    }
+
+    .watchlist__action-love {
+        flex-direction: column;
+    }
+
+    .watchlist__card .card-content {
+        flex-direction: row;
+    }
+}
+
+/* No Item */
+
+.no-item__container {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    flex: 1 0 0;
+    align-self: stretch;
+    gap: 10px;
+}
+
+.no-item__container h1 {
+    color: var(--accent-800, #8C48A8);
+    font-weight: bolder;
+    mix-blend-mode: color-dodge;
+    text-align: center;
+    font-size: 4rem;
+}
+
+.no-item__container h2 {
+    text-align: center;
+}
+
+.no-item__container p {
+    font-size: 1rem;
+    text-align: center;
+}
+
+.no-item__container div:has(h2) {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    gap: 16px;
+    align-self: stretch;
+}
+
+.no-item__container a {
+    width: fit-content;
+    margin-top: 24px;
+}
+
+.no-item__container span {
+    filter: blur(2px);
+}
+
+/* Toast */
+
+.toast {
+    min-width: 250px;
+    max-width: 80vw;
+    background-color: var(--error-background);
+    color: var(--error-foreground);
+    text-align: center;
+    border-radius: 6px;
+    display: flex;
+    padding: 16px 6px;
+    justify-content: space-between;
+    align-items: flex-start;
+    align-self: stretch;
+    position: fixed;
+    z-index: 100;
+    margin: 0 auto;
+    left: 0;
+    right: 0;
+    top: 8px;
+}
+
+.toast[data-type="error"] {
+    background-color: var(--error-background);
+}
+
+.toast[data-type="error"] h3 {
+    color: var(--error-foreground);
+}
+
+.toast[data-type="error"] p {
+    color: var(--error-foreground);
+}
+
+.toast[data-type="error"] svg path {
+    stroke: var(--error-foreground);
+}
+
+.toast[data-type="info"] {
+    background-color: var(--info-background);
+}
+
+.toast[data-type="info"] h3 {
+    color: var(--info-foreground);
+}
+
+.toast[data-type="error"] p {
+    color: var(--error-foreground);
+}
+
+.toast[data-type="info"] svg path {
+    stroke: var(--info-foreground);
+}
+
+.toast[data-type="success"] {
+    background-color: var(--success-background);
+}
+
+.toast[data-type="success"] h3 {
+    color: var(--success-foreground);
+}
+
+.toast[data-type="success"] svg path {
+    stroke: var(--success-foreground);
+}
+
+.toast div {
+    display: flex;
+    padding: 0px 12px;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 6px;
+    flex: 1 0 0;
+}
+
+
+.toast p {
+    text-align: start;
+}
+
+.toast > svg {
+    display: none;
+}
+
+@media screen and (min-width: 640px) {
+    .toast {
+        max-width: 400px;
+    }
+}
+
+.catalog-trailer {
+    width: 80%;
+    max-width: 600px;
+    align-self: stretch;
+}
\ No newline at end of file
diff --git a/src/public/css/home.css b/src/public/css/home.css
new file mode 100644
index 0000000000000000000000000000000000000000..a7fdea48a3996cef03d17eedd5a115cef8cefc62
--- /dev/null
+++ b/src/public/css/home.css
@@ -0,0 +1,185 @@
+.button-auth {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    gap: 4px;
+}
+
+.form-search-filter {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 1rem;
+}
+
+@media screen and (min-width: 1024px) {
+    .form-search-filter {
+        flex-direction: row;
+    }
+}
+
+.search {
+    display: flex;
+    align-items: center;
+    flex-grow: 1;
+    width: 100%;
+    gap: 1rem;
+}
+
+.input-search {
+    flex-grow: 1;
+}
+
+.filter {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    justify-content: left;
+    align-items: center;
+    gap: 1rem;
+    z-index: 50;
+}
+
+.filter__sort {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    gap: 0.6rem;
+    width: 100%;
+}
+
+
+.btn-sort {
+    padding: 0.5rem;
+    background-color: #fff;
+    border: 1px solid rgba(0, 0, 0, .1);
+    border-radius: 0.4rem;
+    cursor: pointer;
+
+}
+
+.btn--apply {
+    width: 100%;
+    max-width: none;
+}
+
+
+.icon-new {
+    font-size: 12px;
+}
+
+.list__watchlist {
+    display: flex;
+    flex-direction: column;
+}
+
+.watchlist {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 1.6rem;
+    padding: 1.6rem 0;
+}
+
+
+.watchlist:not(:last-child) {
+    border-bottom: 1px solid rgba(0, 0, 0, .1);
+}
+
+.list__poster {
+    display: flex;
+    align-items: center;
+    position: relative;
+}
+
+.poster:not(:first-child) {
+    margin-left: -2rem;
+}
+
+.poster:hover {
+    z-index: 20 !important;
+    top: -10px;
+}
+
+
+.watchlist__content {
+    display: flex;
+    flex-direction: column;
+    gap: 0.8rem;
+    flex-grow: 1;
+    width: 100%;
+}
+
+.watchlist__title {
+    color: var(--accent-800);
+    font-weight: 600;
+    font-size: 1.3rem;
+}
+
+.watchlist__meta {
+    display: flex;
+    flex-direction: column;
+    gap: 0.6rem;
+}
+
+.watchlist__type {
+    background-color: var(--accent-300);
+    padding: 0.2rem 0.4rem;
+    border-radius: 0.6rem;
+    max-width: fit-content;
+}
+
+.catalog-list-content-author {
+    color: var(--primary-100);
+}
+
+.author-name {
+    color: var(--accent-800);
+}
+
+.watchlist__dot {
+    display: none;
+}
+
+.watchlist__date {
+    font-size: 0.9rem;
+    color: var(--slate-400);
+}
+
+.catalog-list-btn {
+    background-color: transparent;
+    border: none;
+    padding: 0;
+}
+
+.watchlist__action-love {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 0.4rem;
+}
+
+@media screen and (min-width: 872px) {
+    .watchlist {
+        flex-direction: row;
+    }
+
+    .watchlist__actions {
+        flex-direction: column;
+        max-width: fit-content;
+    }
+
+    .watchlist__action-love {
+        flex-direction: column;
+    }
+}
+
+@media screen and (min-width: 1024px) {
+    .filter {
+        flex-direction: row;
+    }
+
+    .btn--apply {
+        max-width: fit-content;
+    }
+}
\ No newline at end of file
diff --git a/src/public/css/signIn.css b/src/public/css/signIn.css
new file mode 100644
index 0000000000000000000000000000000000000000..d71a618dc5842cd69e27dfc4e3939be0dd851b10
--- /dev/null
+++ b/src/public/css/signIn.css
@@ -0,0 +1,108 @@
+.signin-container {
+    width: 100%;
+    height: 100%;
+    background: white;
+    justify-content: flex-start;
+    align-items: center;
+    display: inline-flex;
+    flex-grow: 1;
+}
+
+.signin-poster {
+    display: none;
+    width: 50%;
+    height: 100vh;
+    object-fit: cover;
+    max-width: 50vw;
+}
+
+.right-side {
+    display: flex;
+    flex-direction: column;
+    gap: 1rem;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+}
+
+.main-container {
+    max-width: 500px;
+    width: 90%;
+    padding: 24px;
+    background: white;
+    border-radius: 6px;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    gap: 40px;
+    display: flex;
+}
+
+.welcome-text {
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    gap: 1rem;
+    display: flex;
+}
+
+.welcome-text__h2 {
+    font-weight: bold;
+    font-size: 1.5rem;
+}
+
+.welcome-text__h1 {
+    text-align: center;
+}
+
+.inputs {
+    flex-direction: column;
+    display: flex;
+    gap: 1rem;
+    width: 100%;
+}
+
+.parameter {
+    height: 62px;
+    flex-direction: column;
+    justify-content: flex-start;
+    align-items: flex-start;
+    gap: 6px;
+    display: flex;
+}
+
+.parameter-title {
+    color: #0f172a;
+    font-size: 14px;
+    font-weight: 500;
+    line-height: 20px;
+    word-wrap: break-word;
+}
+
+.inputs > button {
+    margin-bottom: 20px;
+}
+
+form p:has(a) {
+    text-align: center;
+}
+
+
+
+@media screen and (min-width: 640px) {
+    .welcome-text__h2 {
+        font-size: 2rem;
+    }
+}
+
+@media screen and (min-width: 1024px) {
+    .signin-poster {
+        display: block;
+    }
+}
+
+.signup-link {
+    color: var(--accent-800);
+    font-size: 14px;
+    text-decoration: underline;
+}
\ No newline at end of file
diff --git a/src/public/css/signUp.css b/src/public/css/signUp.css
new file mode 100644
index 0000000000000000000000000000000000000000..c13558166cf80593adfb3a075e2a3534b2b682a6
--- /dev/null
+++ b/src/public/css/signUp.css
@@ -0,0 +1,105 @@
+.signup-container {
+    width: 100%;
+    height: 100%;
+    background: white;
+    justify-content: flex-start;
+    align-items: center;
+    display: inline-flex;
+    flex-grow: 1;
+}
+
+.signup-poster {
+    display: none;
+    width: 50%;
+    height: 100vh;
+    object-fit: cover;
+    max-width: 50vw;
+}
+
+.right-side {
+    display: flex;
+    flex-direction: column;
+    gap: 1rem;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+}
+
+.main-container {
+    max-width: 500px;
+    width: 90%;
+    padding: 24px;
+    background: white;
+    border-radius: 6px;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    gap: 40px;
+    display: flex;
+}
+
+.welcome-text {
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    gap: 1rem;
+    display: flex;
+}
+
+.welcome-text__h2 {
+    font-weight: bold;
+    font-size: 1.5rem;
+}
+
+.welcome-text__h1 {
+    text-align: center;
+}
+
+.inputs {
+    flex-direction: column;
+    display: flex;
+    gap: 1rem;
+    width: 100%;
+}
+
+.parameter {
+    height: 62px;
+    flex-direction: column;
+    justify-content: flex-start;
+    align-items: flex-start;
+    gap: 6px;
+    display: flex;
+}
+
+.parameter-title {
+    color: #0f172a;
+    font-size: 14px;
+    font-weight: 500;
+    line-height: 20px;
+    word-wrap: break-word;
+}
+
+
+
+form p:has(a) {
+    text-align: center;
+}
+
+
+@media screen and (min-width: 640px) {
+    .welcome-text__h2 {
+        font-size: 2rem;
+    }
+}
+
+@media screen and (min-width: 1024px) {
+    .signup-poster {
+        display: block;
+    }
+}
+
+.signin-link {
+    color: var(--accent-800);
+    font-size: 14px;
+    text-decoration: underline;
+}
\ No newline at end of file
diff --git a/src/public/css/watchlist-detail.css b/src/public/css/watchlist-detail.css
new file mode 100644
index 0000000000000000000000000000000000000000..99a0a6d9c05c8d8c0d069540fb03f87a8672922f
--- /dev/null
+++ b/src/public/css/watchlist-detail.css
@@ -0,0 +1,96 @@
+article.header {
+    display: flex;
+    justify-content: space-between;
+    flex-direction: column;
+    align-items: flex-end;
+    gap: 32px;
+    align-self: stretch;
+}
+
+article.header .detail {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 10px;
+    width: 100%;
+}
+
+article.header .container-subtitle {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+}
+
+article.header .container-button {
+    width: 100%;
+    display: flex;
+    align-items: flex-start;
+    justify-content: center;
+}
+
+.container-btn-love {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    font-size: small;
+}
+
+button#show-more {
+    width: 100%;
+}
+
+
+form {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-end;
+    gap: 24px;
+    align-self: stretch;
+}
+
+.watchlist-detail__button-container {
+    position: fixed;
+    bottom: 20px;
+    right: 20px;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    width: fit-content;
+}
+
+.watchlist-detail__button-container .btn-icon {
+    background: #FFF;
+    border: 1px solid rgba(0, 0, 0, .1);
+    cursor: pointer;
+    padding: 10px 10px;
+    border-radius: 6px;
+}
+
+.watchlist-detail__button-container .btn-icon:hover {
+    background-color: var(--accent-300);
+    transition: all;
+    transition-duration: 400ms;
+    transition-timing-function: ease-in-out;
+}
+
+@media screen and (min-width: 640px) {
+    article.header {
+        flex-direction: row;
+        align-items: flex-start;
+    }
+
+    .watchlist-detail__button-container {
+        bottom: 40px;
+        right: 3rem;
+        gap: 10px;
+    }
+
+    article.header .container-button {
+        justify-content: flex-end;
+    }
+
+    article.header .detail {
+        width: 80%;
+    }
+}
diff --git a/src/public/css/watchlist-self.css b/src/public/css/watchlist-self.css
new file mode 100644
index 0000000000000000000000000000000000000000..af6f1adb8da7936320d32e62a08d1f15d81782a0
--- /dev/null
+++ b/src/public/css/watchlist-self.css
@@ -0,0 +1,30 @@
+.search-filter {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    align-items: center;
+    justify-content: space-between;
+    gap: 2rem;
+    align-self: stretch;
+}
+
+.search-filter>div {
+    display: flex;
+    flex-direction: column;
+    gap: 1.5rem;
+}
+
+.search-filter .visibility {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+}
+
+.visibility .selected {
+    background-color: var(--accent-300);
+}
+
+.no-item__container a {
+    text-decoration: underline;
+    color: var(--accent-800);
+}
\ No newline at end of file
diff --git a/src/public/css/watchlistCreate.css b/src/public/css/watchlistCreate.css
new file mode 100644
index 0000000000000000000000000000000000000000..054e8ff15aecad03f8f36fbd7617f166a781f242
--- /dev/null
+++ b/src/public/css/watchlistCreate.css
@@ -0,0 +1,109 @@
+.container__create-watchlist {
+    display: flex;
+    flex-direction: column;
+    gap: 4rem;
+    align-items: flex-start;
+    margin-bottom: 8rem;
+}
+
+.container__form {
+    width: 100%;
+    flex-grow: 1;
+}
+
+.form__create-watchlist {
+    width: 100%;
+}
+
+.form-watchlist-actions {
+    display: flex;
+    flex-direction: column;
+    gap: 0.6rem;
+}
+
+.tags {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 0.2rem;
+    margin: 0.2rem 0;
+}
+
+.checkbox {
+    max-width: fit-content;
+}
+
+.input-tag {
+    display: flex;
+    flex-direction: row-reverse;
+    align-items: center;
+    justify-content: start;
+    gap: 1rem;
+}
+
+.watchlist-items__title {
+    margin: 1rem 0 0 0;
+}
+
+.watchlist-items {
+    display: flex;
+    flex-direction: column;
+}
+
+.actions {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 1rem;
+    width: 100%;
+    background-color: white;
+    padding: 2rem 3rem;
+}
+
+.btn__add-item {
+    min-width: 18rem;
+}
+
+.btn__save {
+    min-width: 18rem;
+    width: 100%;
+}
+
+.form-search {
+    display: flex;
+}
+
+.search {
+    display: flex;
+    align-items: center;
+    width: 100%;
+    gap: 0.4rem;
+}
+
+.search__input {
+    flex-grow: 1;
+}
+
+@media screen and (min-width: 784px) {
+    .tags {
+        grid-template-columns: repeat(5, minmax(0, 1fr));
+    }
+}
+
+
+@media screen and (min-width: 1024px) {
+    .container__create-watchlist {
+        flex-direction: row;
+    }
+
+    .actions {
+        position: sticky;
+        top: 8rem;
+        left: auto;
+        bottom: auto;
+        max-width: 20rem;
+    }
+}
\ No newline at end of file
diff --git a/src/public/index.php b/src/public/index.php
new file mode 100644
index 0000000000000000000000000000000000000000..d0ffb63eca3753f23fd104a03e749b6ab4e861a9
--- /dev/null
+++ b/src/public/index.php
@@ -0,0 +1,8 @@
+<?php
+
+// Initial config
+define('ROOT_PATH', '/var/www');
+require_once ROOT_PATH . '/config/bootstrap.php';
+
+// Routing
+require_once ROOT_PATH . '/routes/view.php';
diff --git a/src/public/js/catalog/createUpdate.js b/src/public/js/catalog/createUpdate.js
new file mode 100644
index 0000000000000000000000000000000000000000..a9986031bc7d281aea32489c966bcb7491205dcc
--- /dev/null
+++ b/src/public/js/catalog/createUpdate.js
@@ -0,0 +1,185 @@
+const CATEGORY = ["ANIME", "DRAMA"];
+
+function validateInput(formData, update = false) {
+  if (!formData.get("title") || formData.get("title").trim() === "") {
+    return {
+      valid: false,
+      message: "Title is required.",
+    };
+  }
+  if (formData.get("title").length > 40) {
+    return {
+      valid: false,
+      message: "Title is too long. Maximum 40 chars.",
+    };
+  }
+  if (formData.get("description") && formData.get("description").length > 255) {
+    return {
+      valid: false,
+      message: "Description is too long. Maximum 255 chars.",
+    };
+  }
+  if (
+    !formData.get("category") ||
+    !CATEGORY.includes(formData.get("category").trim())
+  ) {
+    return {
+      valid: false,
+      message: "Category is invalid.",
+    };
+  }
+
+  if (!update && !formData.get("poster").name) {
+    return {
+      valid: false,
+      message: "Poster is required.",
+    };
+  }
+  return {
+    valid: true,
+  };
+}
+
+function createCatalog(form) {
+  const apiUrl = `/api/catalog/create`;
+  const formData = new FormData(form);
+
+  const validate = validateInput(formData);
+
+  if (!validate.valid) {
+    showToast("Error", validate.message, "error");
+    return;
+  }
+
+  const xhttp = new XMLHttpRequest();
+  xhttp.open("POST", apiUrl, true);
+
+  xhttp.onreadystatechange = function () {
+    if (xhttp.readyState === 4) {
+      const response = JSON.parse(xhttp.responseText);
+      if (xhttp.status === 200 && response.status === 200) {
+        showToast(
+          "Success",
+          `Catalog ${response.data.title} created`,
+          "success"
+        );
+        setTimeout(() => {
+          window.location.href = `/catalog/${response.data.uuid}`;
+        }, [1000]);
+      } else {
+        try {
+          showToast("Error", response.message);
+        } catch (e) {
+          showToast("Error", "Something went wrong", "error");
+        }
+      }
+    }
+  };
+
+  xhttp.send(formData);
+}
+
+function updateCatalog(form) {
+  const urlParts = window.location.pathname.split("/");
+  const uuidIndex = urlParts.indexOf("catalog") + 1;
+  const uuid = urlParts[uuidIndex];
+  const apiUrl = `/api/catalog/${uuid}/update`;
+  const formData = new FormData(form);
+
+  const validate = validateInput(formData, true);
+
+  if (!validate.valid) {
+    showToast("Error", validate.message, "error");
+    return;
+  }
+
+  const xhttp = new XMLHttpRequest();
+  xhttp.open("POST", apiUrl, true);
+
+  xhttp.onreadystatechange = function () {
+    if (xhttp.readyState === 4) {
+      const response = JSON.parse(xhttp.responseText);
+      if (xhttp.status === 200 && response.status === 200) {
+        showToast("Success", "Catalog updated", "success");
+        setTimeout(() => {
+          window.location.href = `/catalog/${uuid}`;
+        }, [1000]);
+      } else {
+        try {
+          showToast("Error", response.message);
+        } catch (e) {
+          showToast("Error", "Something went wrong", "error");
+        }
+      }
+    }
+  };
+
+  xhttp.send(formData);
+}
+
+const form = document.querySelector("form#catalog-create-update");
+
+form.addEventListener("submit", function (event) {
+  event.preventDefault();
+
+  const currentUrl = window.location.href;
+  const url = new URL(currentUrl);
+  const urlPathname = url.pathname.split("/");
+  const action = urlPathname[urlPathname.length - 1].toLowerCase();
+
+  if (action === "create") {
+    dialog(
+      "Create Catalog",
+      `Are you sure you want to create this catalog?`,
+      "create",
+      "create",
+      "Confirm",
+      () => {
+        createCatalog(form);
+      }
+    );
+  } else if (action === "edit") {
+    dialog(
+      "Update Catalog",
+      `Are you sure you want to update this catalog?`,
+      "update",
+      "update",
+      "Confirm",
+      () => {
+        updateCatalog(form);
+      }
+    );
+  }
+});
+
+posterImg = document.querySelector("img#poster");
+if (posterImg) {
+  posterInput = document.querySelector("input#posterField");
+  posterInput.addEventListener("change", function (event) {
+    if (this.files[0]) {
+      var reader = new FileReader();
+
+      reader.readAsDataURL(this.files[0]);
+
+      reader.onloadend = function () {
+        posterImg.src = reader.result;
+      };
+    }
+  });
+}
+
+trailerVideo = document.querySelector("video#trailer");
+if (trailerVideo) {
+  trailerInput = document.querySelector("input#trailerField");
+  trailerInput.addEventListener("change", function (event) {
+    if (this.files[0]) {
+      var reader = new FileReader();
+
+      reader.readAsDataURL(this.files[0]);
+
+      reader.onloadend = function () {
+        trailerVideo.src = reader.result;
+      };
+    }
+  });
+}
diff --git a/src/public/js/catalog/delete.js b/src/public/js/catalog/delete.js
new file mode 100644
index 0000000000000000000000000000000000000000..de2c8056536e030b53137e4ef05326d313a0b305
--- /dev/null
+++ b/src/public/js/catalog/delete.js
@@ -0,0 +1,47 @@
+function deleteCatalog(uuid, title) {
+  const xhttp = new XMLHttpRequest();
+  xhttp.open("DELETE", `/api/catalog/${uuid}/delete`, true);
+  xhttp.setRequestHeader("Content-Type", "application/json");
+
+  xhttp.onreadystatechange = function () {
+    if (xhttp.readyState === 4) {
+      if (xhttp.status === 200) {
+        showToast("Success", `Catalog ${title} deleted`, "success");
+        setTimeout(() => {
+          window.location.href = "/catalog";
+        }, 1000);
+      } else {
+        try {
+          const response = JSON.parse(xhttp.responseText);
+          showToast("Error", response.message, "error");
+        } catch (error) {
+          showToast("Error", "Something went wrong", "error");
+        }
+      }
+    }
+  };
+
+  xhttp.send();
+}
+
+const deleteTriggerButtons = document.querySelectorAll(
+  `.catalog-delete-trigger`
+);
+deleteTriggerButtons.forEach((deleteTriggerButton) => {
+  if (deleteTriggerButton) {
+    deleteTriggerButton.addEventListener("click", () => {
+      const uuid = deleteTriggerButton.getAttribute("data-uuid");
+      const title = deleteTriggerButton.getAttribute("data-title");
+      dialog(
+        "Delete Catalog",
+        `Are you sure you want to delete ${title}?`,
+        uuid,
+        "delete",
+        "Delete",
+        () => {
+          deleteCatalog(uuid, title);
+        }
+      );
+    });
+  }
+});
diff --git a/src/public/js/components/alert.js b/src/public/js/components/alert.js
new file mode 100644
index 0000000000000000000000000000000000000000..91d641977e416965c3b159cff93f88548ab9d0ec
--- /dev/null
+++ b/src/public/js/components/alert.js
@@ -0,0 +1,7 @@
+btnClose = document.querySelector(".alert button");
+
+if (btnClose) {
+  btnClose.addEventListener("click", () => {
+    document.querySelector(".alert").remove();
+  });
+}
diff --git a/src/public/js/components/modal.js b/src/public/js/components/modal.js
new file mode 100644
index 0000000000000000000000000000000000000000..f262d3a36c627534f29c253d42a8742b666a3860
--- /dev/null
+++ b/src/public/js/components/modal.js
@@ -0,0 +1,31 @@
+function modal() {
+    const modals = document.querySelectorAll('.modal');
+    const modalTriggers = document.querySelectorAll('.modal__trigger');
+    const containerDefault = document.querySelector('main');
+    const modalClose = document.querySelector('.modal__close');
+
+    modals.forEach(modal => {
+        const modalTrigger = modal.querySelector('.modal__trigger');
+        const modalContentWrapper = modal.querySelector('#modal__content');
+        const modalClose = modal.querySelector('.modal__close');
+
+        modalTrigger.addEventListener('click', function () {
+            if (modalContentWrapper.style.display === 'flex') {
+                modalContentWrapper.classList.style.display = 'none'
+            } else {
+                modalContentWrapper.style.display = 'flex';
+                document.body.style.overflow = 'hidden';
+                containerDefault.style.zIndex = '1000';
+            }
+        })
+
+        modalClose.addEventListener('click', function () {
+            modalContentWrapper.style.display = 'none';
+            document.body.style.overflow = 'unset';
+            containerDefault.style.zIndex = '10';
+        })
+    })
+}
+
+modal();
+
diff --git a/src/public/js/components/modal/watchlistAddItem.js b/src/public/js/components/modal/watchlistAddItem.js
new file mode 100644
index 0000000000000000000000000000000000000000..739714a72cea223c4bd925c53c7b425ed32e48b4
--- /dev/null
+++ b/src/public/js/components/modal/watchlistAddItem.js
@@ -0,0 +1,222 @@
+const PAGE_SIZE = 4;
+const MAX_ITEMS = 50;
+const PLUS_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus"><path d="M5 12h14" /><path d="M12 5v14" /></svg>`
+
+const inputSearch = document.querySelector('.search__input');
+const btnGetData = document.querySelector('.get__data');
+const searchItems = document.querySelector('.search__items');
+
+let watchlistItemContainer = document.querySelector('.watchlist-items');
+let catalogSelected = [];
+let page = 1;
+let isLoading = false;
+let endOfList = true;
+
+function deleteItemAction(id) {
+    const item = document.querySelector(`div[data-id="${id}"]`);
+    if (item) {
+        catalogSelected = catalogSelected.filter((e) => {
+            return e !== id
+        });
+        const btnAddToList = searchItems.querySelector(`button.search-item__action[data-id="${id}"]`);
+        if (btnAddToList) {
+            btnAddToList.innerHTML = PLUS_ICON;
+        }
+        item.remove();
+
+        if (catalogSelected.length === 0) {
+            const itemsPlaceholder = document.createElement("p");
+            itemsPlaceholder.classList.add("items-placeholder");
+            itemsPlaceholder.textContent = "No items selected.";
+            watchlistItemContainer.appendChild(itemsPlaceholder);
+        }
+    }
+}
+
+function deleteItem() {
+    const btnWatchlistItemDeletes = document.querySelectorAll('.watchlist-item__delete');
+    btnWatchlistItemDeletes.forEach(btnWatchlistItemDelete => {
+        btnWatchlistItemDelete.addEventListener('click', () => {
+            deleteItemAction(btnWatchlistItemDelete.dataset.id);
+        })
+    })
+}
+
+function drag() {
+    const watchlistItems = document.querySelectorAll('.watchlist-item')
+
+    watchlistItems.forEach(watchlistItem => {
+        watchlistItem.addEventListener('dragstart', () => {
+            setTimeout(() => watchlistItem.classList.add('dragging'), 0);
+        });
+        watchlistItem.addEventListener('dragend', () => watchlistItem.classList.remove("dragging"));
+    });
+}
+
+function sortableItems(e) {
+    e.preventDefault();
+    const afterElement = getDragAfterElement(watchlistItemContainer, e.clientY);
+    const draggable = document.querySelector('.dragging');
+    if (afterElement === null) {
+        watchlistItemContainer.appendChild(draggable);
+    } else {
+        watchlistItemContainer.insertBefore(draggable, afterElement);
+    }
+}
+
+function getDragAfterElement(container, y) {
+    const draggableElements = [...container.querySelectorAll('.watchlist-item:not(.dragging)')];
+
+    return draggableElements.reduce((closest, child) => {
+        const box = child.getBoundingClientRect();
+        const offset = y - box.top - box.height / 2;
+        if (offset < 0 && offset > closest.offset) {
+            return {offset: offset, element: child}
+        }
+        return closest;
+    }, {offset: Number.NEGATIVE_INFINITY}).element
+}
+
+function createLoading() {
+    const loading = document.createElement("div");
+    loading.classList.add('loading');
+    loading.innerHTML = 'Loading...';
+    searchItems.appendChild(loading);
+}
+
+function fetchSearch(replace = false) {
+    if (replace) page = 1;
+
+    const xhttp = new XMLHttpRequest();
+    xhttp.onreadystatechange = function () {
+        if (this.readyState === 4) {
+            isLoading = false;
+            const loading = document.querySelector('.loading');
+            if (loading) {
+                loading.remove();
+                if (this.response === "") {
+                    endOfList = true;
+                }
+            }
+            replace ? searchItems.innerHTML = this.response : searchItems.innerHTML += this.response;
+
+            if (this.response === "" && searchItems.innerHTML === "") {
+                const notFound = document.createElement("div");
+                notFound.classList.add("loading");
+                notFound.innerHTML = "No Results Found.";
+                searchItems.appendChild(notFound);
+            }
+
+            if (this.response) page++;
+            const btnAddToList = searchItems.querySelectorAll('.search-item__action');
+            btnAddToList.forEach(e => {
+                if (catalogSelected.includes(e.dataset.id)) {
+                    e.innerHTML = '✔️';
+                }
+
+                e.addEventListener('click', () => {
+                    if (!catalogSelected.includes(e.dataset.id)) {
+                        if (catalogSelected.length >= MAX_ITEMS) {
+                            showToast("Failed", "You can add up to 50 items");
+                            setTimeout(() => {
+                                const toast = document.querySelector("#toast");
+                                toast.classList.add("hidden");
+                            }, 2000);
+                        } else {
+                            e.innerHTML = '✔️';
+                            catalogSelected.push(e.dataset.id);
+                            const xhttp = new XMLHttpRequest();
+                            xhttp.onreadystatechange = function () {
+                                if (xhttp.readyState === 4) {
+                                    const itemsPlaceholder = document.querySelector("p.items-placeholder");
+                                    if (itemsPlaceholder) {
+                                        itemsPlaceholder.remove();
+                                    }
+                                    const wrapper = document.createElement('div');
+                                    wrapper.classList.add('watchlist-item');
+                                    wrapper.draggable = "true";
+                                    wrapper.dataset.id = e.dataset.id;
+                                    wrapper.innerHTML = xhttp.response;
+                                    watchlistItemContainer.appendChild(wrapper);
+                                    deleteItem();
+                                    drag();
+                                }
+                            }
+                            xhttp.open("GET", "/api/watchlist/item?id=" + e.dataset.id, true);
+                            xhttp.send();
+                        }
+                    } else {
+                        deleteItemAction(e.dataset.id);
+                    }
+                })
+            });
+        } else if (!endOfList) {
+            const loading = document.querySelector('.loading');
+            if (loading == null) {
+                isLoading = true;
+                createLoading();
+            }
+        }
+    }
+    xhttp.open("GET", `/api/catalog?title=${inputSearch.value.trim()}&page=${page}&pageSize=${PAGE_SIZE}`, true);
+    xhttp.send();
+}
+
+function getCatalogSelected() {
+    catalogSelected = [];
+    const btnDelete = document.querySelectorAll("button.watchlist-item__delete");
+
+    btnDelete.forEach(btn => {
+        catalogSelected.push(btn.dataset.id);
+    })
+}
+
+let search = () => {
+    if (inputSearch.value.trim() !== "") {
+        endOfList = false;
+        searchItems.innerHTML = "";
+        fetchSearch(true);
+    }
+}
+
+const debounce = (fn, delay) => {
+    let timer;
+    return function () {
+        clearTimeout(timer);
+        timer = setTimeout(() => {
+            fn();
+        }, delay);
+    }
+}
+
+search = debounce(search, 1000);
+
+inputSearch.addEventListener('keydown', function (e) {
+    if (e.keyCode === 13) {
+        e.preventDefault();
+    }
+})
+
+inputSearch.addEventListener('input', search);
+
+searchItems.addEventListener('scroll', () => {
+    if (searchItems.offsetHeight + searchItems.scrollTop - searchItems.clientHeight + 100 > 0 && !endOfList && !isLoading) {
+        fetchSearch();
+    }
+})
+searchItems.addEventListener('touchmove', () => {
+    if (searchItems.offsetHeight + searchItems.scrollTop - searchItems.clientHeight + 100 > 0 && !endOfList && !isLoading) {
+        fetchSearch();
+    }
+})
+
+watchlistItemContainer.addEventListener("dragover", sortableItems);
+btnAddItem.addEventListener('click', () => {
+    endOfList = false;
+    getCatalogSelected();
+    fetchSearch(true);
+})
+
+drag();
+getCatalogSelected();
+deleteItem();
\ No newline at end of file
diff --git a/src/public/js/components/navbar.js b/src/public/js/components/navbar.js
new file mode 100644
index 0000000000000000000000000000000000000000..c46ff5597fde538d9f9952ca44a4e3092bca7684
--- /dev/null
+++ b/src/public/js/components/navbar.js
@@ -0,0 +1,60 @@
+navbarToggle = document.getElementById("navbar-toggle");
+if (navbarToggle) {
+  navbarToggle.addEventListener("click", function () {
+    navbarMenu = document.getElementById("navbar-menu");
+    if (navbarMenu.classList.contains("collapsed")) {
+      navbarMenu.classList.remove("collapsed");
+      navbarToggle.focus();
+      this.setAttribute("aria-expanded", "true");
+    } else {
+      navbarMenu.classList.add("collapsed");
+      navbarToggle.blur();
+      this.setAttribute("aria-expanded", "false");
+    }
+  });
+
+  navbarToggle.addEventListener("blur", function (e) {
+    if (
+      e.relatedTarget &&
+      (e.relatedTarget.parentElement.id === "navbar-menu" ||
+        e.relatedTarget.parentElement.id === "profile-menu")
+    ) {
+      return;
+    } else {
+      navbarMenu = document.getElementById("navbar-menu");
+      navbarMenu.classList.add("collapsed");
+      navbarToggle.blur();
+      this.setAttribute("aria-expanded", "false");
+    }
+  });
+}
+
+profileMenuToggle = document.getElementById("profile-menu-toggle");
+if (profileMenuToggle) {
+  profileMenuToggle.addEventListener("click", function () {
+    profileMenu = document.getElementById("profile-menu");
+    if (profileMenu.classList.contains("collapsed")) {
+      profileMenu.classList.remove("collapsed");
+      profileMenuToggle.focus();
+      this.setAttribute("aria-expanded", "true");
+    } else {
+      profileMenu.classList.add("collapsed");
+      profileMenuToggle.blur();
+      this.setAttribute("aria-expanded", "false");
+    }
+  });
+
+  profileMenuToggle.addEventListener("blur", function (e) {
+    if (
+      e.relatedTarget &&
+      e.relatedTarget.parentElement.id === "profile-menu"
+    ) {
+      return;
+    } else {
+      profileMenu = document.getElementById("profile-menu");
+      profileMenu.classList.add("collapsed");
+      profileMenuToggle.blur();
+      this.setAttribute("aria-expanded", "false");
+    }
+  });
+}
diff --git a/src/public/js/components/select.js b/src/public/js/components/select.js
new file mode 100644
index 0000000000000000000000000000000000000000..3c0b54377ac9384d2801598581690415acea832a
--- /dev/null
+++ b/src/public/js/components/select.js
@@ -0,0 +1,17 @@
+const selects = document.querySelectorAll('.c-select-menu');
+
+selects.forEach(select => {
+    const selectBtn = select.querySelector('.c-select-btn-text');
+    select.addEventListener('click', () => {
+        options = select.querySelector('.c-select-options');
+        options.classList.toggle('c-select-hide');
+        option = options.querySelectorAll('.c-select-option');
+        input = select.querySelector('#' + select.dataset.id);
+        option.forEach(optn => {
+            optn.addEventListener('click', (event) => {
+                selectBtn.textContent = event.currentTarget.innerText;
+                input.value = event.currentTarget.innerText.trim();
+            })
+        })
+    })
+});
\ No newline at end of file
diff --git a/src/public/js/components/watchlist/watchlistItem.js b/src/public/js/components/watchlist/watchlistItem.js
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/public/js/global.js b/src/public/js/global.js
new file mode 100644
index 0000000000000000000000000000000000000000..bce2ff47c26a85586d54d343f0d746f8b9a0f71d
--- /dev/null
+++ b/src/public/js/global.js
@@ -0,0 +1,182 @@
+function showToast(title, message, type = "error") {
+  const oldToast = document.querySelector("#toast");
+  if (oldToast) {
+    oldToast.remove();
+  }
+  const toast = document.createElement("div");
+  toast.id = "toast";
+  toast.classList.add("toast");
+  toast.setAttribute("data-type", type);
+  toast.innerHTML = `
+  <div>
+  <h3>
+      ${title}
+  </h3>
+  <p>
+      ${message}
+  </p>
+</div>
+    <button id="close" class="btn-ghost">
+        <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
+        <path d="M1 13L7.00001 7.00002M7.00001 7.00002L13 1M7.00001 7.00002L1 1M7.00001 7.00002L13 13" stroke="black"
+            stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+        </svg>
+    </button>
+    `;
+  const body = document.querySelector("body");
+  body.appendChild(toast);
+  const toastClose = toast.querySelector("button");
+  toastClose.addEventListener("click", () => {
+    toast.remove();
+  });
+}
+
+function dialog(
+  title,
+  message,
+  dialogId,
+  actionId,
+  actionButtonText,
+  onaction
+) {
+  const body = document.querySelector("body");
+
+  const dialog = document.createElement("div");
+  dialog.classList.add("dialog");
+  dialog.id = `dialog-${dialogId}`;
+  dialog.innerHTML = `
+        <div class="dialog__content">
+            <h2>
+                ${title}
+            </h2>
+            <p>
+                ${message}
+            </p>
+            <div class="dialog__button-container">
+                <button id="cancel">
+                    Cancel
+                </button>
+                <button id=${actionId} class="btn-bold">
+                    ${actionButtonText}
+                </button>
+            </div>
+        </div>
+    `;
+
+  body.appendChild(dialog);
+
+  const cancelButton = dialog.querySelector("#cancel");
+  cancelButton.addEventListener("click", () => {
+    dialog.remove();
+  });
+
+  const actionButton = dialog.querySelector(`#${actionId}`);
+  actionButton.addEventListener("click", () => {
+    dialog.remove();
+    onaction();
+  });
+}
+
+// Lik and Save Watchlist
+
+function like() {
+  const btnLikes = document.querySelectorAll(".btn__like");
+  btnLikes.forEach((btn) => {
+    if (btn.dataset.id) {
+      let liked = btn.dataset.liked;
+      btn.addEventListener("click", () => {
+        const iconLike = btn.querySelector(".icon-heart");
+        const likeCountEl = document.querySelector(
+          `span[data-id='${btn.dataset.id}']`
+        );
+        if (!liked) {
+          likeCountEl.innerHTML = `${parseInt(likeCountEl.textContent) + 1}`;
+          iconLike.dataset.type = "filled";
+        } else {
+          likeCountEl.innerHTML = `${parseInt(likeCountEl.textContent) - 1}`;
+          iconLike.dataset.type = "unfilled";
+        }
+        liked = !liked;
+
+        let data = {
+          watchlistUUID: btn.dataset.id,
+        };
+        data = JSON.stringify(data);
+
+        const xhttp = new XMLHttpRequest();
+        xhttp.open("POST", "/api/watchlist/like", true);
+        xhttp.setRequestHeader("Content-Type", "application/json");
+
+        xhttp.onreadystatechange = function () {
+          if (this.readyState === 4) {
+          }
+        };
+
+        xhttp.send(data);
+      });
+    }
+  });
+}
+
+function save() {
+  const btnSaves = document.querySelectorAll(".btn__save");
+  btnSaves.forEach((btn) => {
+    if (btn.dataset.id) {
+      let saved = btn.dataset.saved;
+      btn.addEventListener("click", () => {
+        const iconSaved = btn.querySelector(".icon-bookmark");
+        if (!saved) {
+          iconSaved.dataset.type = "filled";
+        } else {
+          iconSaved.dataset.type = "unfilled";
+        }
+        saved = !saved;
+
+        let data = {
+          watchlistUUID: btn.dataset.id,
+        };
+        data = JSON.stringify(data);
+
+        const xhttp = new XMLHttpRequest();
+        xhttp.open("POST", "/api/watchlist/save", true);
+        xhttp.setRequestHeader("Content-Type", "application/json");
+
+        xhttp.onreadystatechange = function () {
+          if (this.readyState === 4) {
+          }
+        };
+
+        xhttp.send(data);
+      });
+    }
+  });
+}
+
+logoutBtn = document.querySelector("button#logout");
+if (logoutBtn) {
+  logoutBtn.addEventListener("click", () => {
+    const xhttp = new XMLHttpRequest();
+    xhttp.open("POST", "/api/auth/logout", true);
+    xhttp.setRequestHeader("Content-Type", "application/json");
+
+    xhttp.onreadystatechange = function () {
+      if (this.readyState === 4) {
+        if (xhttp.status === 200) {
+          showToast("Success", "Logout success", "success");
+          setTimeout(() => {
+            window.location.href = `/`;
+          }, [1000]);
+        } else {
+          try {
+            const response = JSON.parse(xhttp.responseText);
+            showToast("Error", response.message);
+          } catch (e) {
+            showToast("Error", "Something went wrong", "error");
+          }
+        }
+      }
+    };
+
+    xhttp.send();
+  });
+}
diff --git a/src/public/js/home.js b/src/public/js/home.js
new file mode 100644
index 0000000000000000000000000000000000000000..a0a4c89ee4aa6fa833e21cf6d746b2e3a08b15d2
--- /dev/null
+++ b/src/public/js/home.js
@@ -0,0 +1,75 @@
+const btnSort = document.querySelector(".btn-sort");
+const sortAsc = document.querySelector(".btn-sort-asc");
+const sortDesc = document.querySelector(".btn-sort-desc");
+const images = document.querySelectorAll(".poster");
+const order = document.querySelector("#order");
+const btnApply = document.querySelector("#btn-apply");
+const watchlists = document.querySelector(".list__watchlist");
+const inputSearch = document.querySelector(".input-search");
+
+if (btnSort) {
+    btnSort.addEventListener("click", () => {
+        sortAsc.classList.toggle("hidden");
+        sortDesc.classList.toggle("hidden");
+        order.value = order.value == "asc" ? "desc" : "asc";
+    });
+}
+
+const debounce = (fn, delay) => {
+    let timer;
+    return function () {
+        clearTimeout(timer);
+        timer = setTimeout(() => {
+            fn();
+        }, delay);
+    }
+}
+
+const fetchWatchlist = (url) => {
+    const loading = document.createElement("div");
+    loading.classList.add('loading');
+    loading.innerHTML = 'Loading...';
+    watchlists.innerHTML = ""
+    watchlists.appendChild(loading);
+
+    const xhttp = new XMLHttpRequest();
+    xhttp.onreadystatechange = function () {
+        if (this.readyState === 4) {
+            watchlists.innerHTML = "";
+            if (!this.response) {
+                const noRes = document.createElement("div");
+                noRes.classList.add("loading");
+                noRes.innerHTML = "No Results Found.";
+                watchlists.appendChild(noRes);
+            } else {
+                watchlists.innerHTML = this.response;
+                like();
+                save();
+                modal();
+            }
+        }
+    }
+    xhttp.open("GET", url, true);
+    xhttp.send();
+}
+
+let search = () => {
+    if (inputSearch.value.trim() !== "") {
+        const urlString = window.location.href;
+        const url = new URL(urlString);
+        url.pathname = '/api/watchlists';
+
+        const urlParams = new URLSearchParams(window.location.search);
+        urlParams.set("search", inputSearch.value.trim());
+        urlParams.delete("page");
+
+        fetchWatchlist(`${url.origin}${url.pathname}?${urlParams.toString()}`);
+    }
+}
+
+search = debounce(search, 500);
+
+inputSearch.addEventListener("keyup", search);
+
+like();
+save();
\ No newline at end of file
diff --git a/src/public/js/profile.js b/src/public/js/profile.js
new file mode 100644
index 0000000000000000000000000000000000000000..f1c3ebf7a57729d5e34ffcd01a336e912a7c224f
--- /dev/null
+++ b/src/public/js/profile.js
@@ -0,0 +1,89 @@
+function deleteAccount() {
+  const xhttp = new XMLHttpRequest();
+  xhttp.open("DELETE", `/api/auth/delete`, true);
+  xhttp.setRequestHeader("Content-Type", "application/json");
+
+  xhttp.onreadystatechange = function () {
+    if (xhttp.readyState === 4) {
+      if (xhttp.status === 200) {
+        showToast("Success", `User deleted`, "success");
+        setTimeout(() => {
+          window.location.reload();
+        }, 1000);
+      } else {
+        try {
+          const response = JSON.parse(xhttp.responseText);
+          showToast("Error", response.message, "error");
+        } catch (error) {
+          showToast("Error", "Something went wrong", "error");
+        }
+      }
+    }
+  };
+
+  xhttp.send();
+}
+
+const deleteTriggerButton = document.getElementById("delete-account");
+if (deleteTriggerButton) {
+  deleteTriggerButton.addEventListener("click", () => {
+    dialog(
+      "Delete Account",
+      `Are you sure you want to delete your account?`,
+      "delete-account",
+      "delete",
+      "Delete",
+      () => {
+        deleteAccount();
+      }
+    );
+  });
+}
+
+function updateAccount(form) {
+  const xhttp = new XMLHttpRequest();
+  xhttp.open("PUT", `/api/auth/update`, true);
+  xhttp.setRequestHeader("Content-Type", "application/json");
+
+  xhttp.onreadystatechange = function () {
+    if (xhttp.readyState === 4) {
+      const response = JSON.parse(xhttp.responseText);
+      if (xhttp.status === 200 && response.status === 200) {
+        showToast("Success", `Profile updated`, "success");
+        nameText = document.getElementById("name");
+        nameText.innerText = response.name;
+      } else {
+        try {
+          const response = JSON.parse(xhttp.responseText);
+          showToast("Error", response.message, "error");
+        } catch (error) {
+          showToast("Error", "Something went wrong", "error");
+        }
+      }
+    }
+  };
+
+  xhttp.send(
+    JSON.stringify({
+      name: form.name.value,
+      oldPassword: form.oldPassword.value,
+      newPassword: form.newPassword.value,
+    })
+  );
+}
+
+const form = document.getElementById("profile-edit-form");
+form.addEventListener("submit", function (event) {
+  event.preventDefault();
+
+  dialog(
+    "Update Account",
+    `Are you sure you want to update your account?`,
+    "update",
+    "update",
+    "Confirm",
+    () => {
+      updateAccount(form);
+    }
+  );
+});
diff --git a/src/public/js/profile/bookmark.js b/src/public/js/profile/bookmark.js
new file mode 100644
index 0000000000000000000000000000000000000000..cce092b5bcd048e8dfe1c8bb3be08b2e665f9fdd
--- /dev/null
+++ b/src/public/js/profile/bookmark.js
@@ -0,0 +1,2 @@
+like();
+save();
diff --git a/src/public/js/profile/watchlist.js b/src/public/js/profile/watchlist.js
new file mode 100644
index 0000000000000000000000000000000000000000..d8733fcc8190c1d625dd9c94cd829549498e900f
--- /dev/null
+++ b/src/public/js/profile/watchlist.js
@@ -0,0 +1 @@
+like();
diff --git a/src/public/js/watchlist/createUpdate.js b/src/public/js/watchlist/createUpdate.js
new file mode 100644
index 0000000000000000000000000000000000000000..1f848c0e167a00d31c6ba296f95115f843c74bf8
--- /dev/null
+++ b/src/public/js/watchlist/createUpdate.js
@@ -0,0 +1,171 @@
+const formUpdateWatchlist = document.querySelector("form#update-watchlist");
+const VISIBILITY = ["PUBLIC", "PRIVATE"];
+
+function validateWatchlistCreateUpdateRequest(request) {
+    if (!request.title || request.title.trim() === "") {
+        return {
+            valid: false,
+            message: "Title is required."
+        };
+    }
+    if (request.title.length > 40) {
+        return {
+            valid: false,
+            message: "Title is too long. Maximum 40 chars."
+        }
+    }
+    if (request.description && request.description.length > 255) {
+        return {
+            valid: false,
+            message: "Description is too long. Maximum 255 chars."
+        }
+    }
+    if (!request.visibility || !VISIBILITY.includes(request.visibility.trim())) {
+        return {
+            valid: false,
+            message: "Visibility is invalid."
+        }
+    }
+    if (!request.items || request.items.length === 0) {
+        return {
+            valid: false,
+            message: "Watchlist must contain 1 item."
+        }
+    }
+    if (request.items.length > 50) {
+        return {
+            valid: false,
+            message: "Too many items. Maximum 50 items."
+        }
+    }
+
+    let maxDescExceeded = false;
+    let title;
+    for (let i = 0; i < request.items.length; i++) {
+        if (request.items[i].description.length > 255) {
+            title = request.items[i].title;
+            maxDescExceeded = true;
+            break;
+        }
+    }
+    if (maxDescExceeded) {
+        return {
+            valid: false,
+            message: `Description is too long for item ${title}. Maximum 255 chars.`
+        }
+    }
+    return {
+        valid: true
+    }
+}
+
+function createEditWatchlist() {
+    // Parse url
+    const currentUrl = window.location.href;
+    const url = new URL(currentUrl);
+    const urlPathname = url.pathname.split("/");
+    const action = urlPathname[urlPathname.length - 1];
+
+    const titleEl = document.querySelector("input#title");
+    const descriptionEl = document.querySelector("textarea#description");
+    const visibilityEl = document.querySelector("input#visibility");
+    const itemsEl = document.querySelectorAll("div.watchlist-item");
+    const tagsEl = document.querySelectorAll("input.watchlist-tag");
+
+    const title = titleEl.value;
+    const description = descriptionEl.value;
+    const visibility = visibilityEl.value;
+    const items = [];
+    const tags = [];
+
+    itemsEl.forEach(item => {
+        const itemDescEl = item.querySelector("textarea.watchlist-item__description");
+        const titleEl = item.querySelector(".watchlist-item__title");
+
+        const catalogTitle = titleEl.textContent;
+        const catalogId = itemDescEl.name.split("__")[0].split("[")[1];
+        const catalogUUID = itemDescEl.name.split("__")[1];
+        const catalogCategory = itemDescEl.name.split("__")[2].split("]")[0];
+        const description = itemDescEl.value;
+
+        items.push({
+            "id": catalogId,
+            "uuid": catalogUUID,
+            "category": catalogCategory,
+            "title": catalogTitle,
+            description
+        })
+    })
+
+    tagsEl.forEach(item => {
+        if (item.checked) {
+            tags.push({
+                id: item.value
+            })
+        }
+    })
+
+    // validate request
+    let data = {
+        title,
+        description,
+        visibility,
+        items,
+        tags
+    }
+
+    let validationResult = validateWatchlistCreateUpdateRequest(data);
+    if (!validationResult.valid) {
+        showToast("Invalid Request", validationResult.message);
+        return;
+    }
+
+    // ajax request
+    if (action === "edit") {
+        data["watchlistUUID"] = url.pathname.split("/")[2];
+    }
+
+    data = JSON.stringify(data);
+
+    const xhttp = new XMLHttpRequest();
+
+    xhttp.open(action === "create" ? "POST" : "PUT", "/api/watchlist", true);
+    xhttp.setRequestHeader("Content-Type", "application/json");
+
+    xhttp.onreadystatechange = function () {
+        if (xhttp.readyState === 4) {
+            const response = JSON.parse(xhttp.response);
+            if (xhttp.status !== 200) {
+                showToast("Invalid Request", response.message)
+            } else {
+                showToast("Success", response.message, "success");
+                setTimeout(() => {
+                    if (response.redirectTo !== null && response.redirectTo !== undefined) {
+                        window.location.href = response.redirectTo;
+                        window.history.pushState({}, "", response.redirectTo);
+                        window.location.reload();
+                    }
+                }, 1000)
+            }
+        }
+    }
+
+    xhttp.send(data);
+}
+
+formUpdateWatchlist.addEventListener("submit", e => {
+    e.preventDefault();
+
+    dialog(
+        "Update Watchlist",
+        `Are you sure you want to update this watchlist?`,
+        "update",
+        "update",
+        "Confirm",
+        () => {
+            createEditWatchlist();
+        }
+    );
+
+
+})
\ No newline at end of file
diff --git a/src/public/js/watchlist/delete.js b/src/public/js/watchlist/delete.js
new file mode 100644
index 0000000000000000000000000000000000000000..45483abab2f6d96e42d034b88ad1dda6d10f7f7c
--- /dev/null
+++ b/src/public/js/watchlist/delete.js
@@ -0,0 +1,49 @@
+const btnDelete = document.querySelector("button.btn__delete");
+
+function deleteWatchlist() {
+    let data = {
+        watchlistUUID: btnDelete.dataset.id,
+    }
+    data = JSON.stringify(data);
+
+    const xhttp = new XMLHttpRequest();
+
+    xhttp.open("DELETE", "/api/watchlist", true);
+    xhttp.setRequestHeader("Content-Type", "application/json");
+
+    xhttp.onreadystatechange = function () {
+        if (xhttp.readyState === 4) {
+            const response = JSON.parse(xhttp.response);
+            if (xhttp.status !== 200) {
+                showToast("Failed to Delete Watchlist", response.message);
+            } else {
+                showToast("Success", response.message, "success");
+                setTimeout(() => {
+                    if (response.redirectTo !== null && response.redirectTo !== undefined) {
+                        window.location.href = response.redirectTo;
+                        window.history.pushState({}, "", response.redirectTo);
+                        window.location.reload();
+                    }
+                }, 1000)
+            }
+        }
+    }
+
+    xhttp.send(data);
+}
+
+if (btnDelete) {
+    btnDelete.addEventListener("click", () => {
+        dialog(
+            "Delete Watchlist",
+            `Are you sure you want to delete this watchlist?`,
+            "delete",
+            "delete",
+            "Delete",
+            () => {
+                deleteWatchlist();
+            }
+        );
+    
+    })
+}
diff --git a/src/public/js/watchlist/detail.js b/src/public/js/watchlist/detail.js
new file mode 100644
index 0000000000000000000000000000000000000000..71a4d25cd6fa850104a300ddf893d2938534ec3d
--- /dev/null
+++ b/src/public/js/watchlist/detail.js
@@ -0,0 +1,2 @@
+like();
+save();
\ No newline at end of file
diff --git a/src/public/js/watchlistCreate.js b/src/public/js/watchlistCreate.js
new file mode 100644
index 0000000000000000000000000000000000000000..b60b7e10d24ab3aeb51cad78cf687bf8391ad84c
--- /dev/null
+++ b/src/public/js/watchlistCreate.js
@@ -0,0 +1,16 @@
+const btnAddItem = document.querySelector('.btn__add-item');
+
+// function addItem () {
+//     const xhttp = new XMLHttpRequest();
+//     xhttp.onreadystatechange = function () {
+//         if (this.readyState === 4) {
+//             const div = document.createElement('div');
+//             div.innerHTML = this.response;
+//             watchlistItems.appendChild(div);
+//         }
+//     }
+//     xhttp.open("POST", "/cc/watchlist-item", true);
+//     xhttp.send();
+// }
+
+// btnAddItem.addEventListener('click', addItem);
\ No newline at end of file
diff --git a/src/seed/seed.sql b/src/seed/seed.sql
new file mode 100644
index 0000000000000000000000000000000000000000..8b62583090dd7f402747b70f44c583abb704cafd
--- /dev/null
+++ b/src/seed/seed.sql
@@ -0,0 +1,19 @@
+INSERT INTO catalogs (uuid, title, description, poster, trailer, category)
+SELECT
+    md5(random()::text || clock_timestamp()::text)::uuid,
+    'Anime Title ' || num,
+    'Anime Description ' || num,
+    '5a5ac4ad0c3a5e7c.webp',
+    'a3e992b0d939a896.mp4',
+    'ANIME'
+FROM generate_series(1, 100) AS num;
+
+INSERT INTO catalogs (uuid, title, description, poster, trailer, category)
+SELECT
+    md5(random()::text || clock_timestamp()::text)::uuid,
+    'Drama Title ' || num,
+    'Drama Description ' || num,
+    '5a6b36907cd0f469.webp',
+    'a3e992b0d939a896.mp4',
+    'DRAMA'
+FROM generate_series(1, 100) AS num;
diff --git a/src/server/.env.example b/src/server/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..e22355013b0cb5006a33161d86fb2a915497536c
--- /dev/null
+++ b/src/server/.env.example
@@ -0,0 +1,5 @@
+DB_NAME=
+DB_HOST=db
+DB_PORT=5432
+DB_USER=
+DB_PASSWORD=
\ No newline at end of file
diff --git a/src/server/app/App/Domain.php b/src/server/app/App/Domain.php
new file mode 100644
index 0000000000000000000000000000000000000000..29ef6d0998deb38a8620eef658c1170c8eb6ecc3
--- /dev/null
+++ b/src/server/app/App/Domain.php
@@ -0,0 +1,14 @@
+<?php
+
+abstract class Domain
+{
+    public int|string $id;
+    public string $table;
+    public function __construct()
+    {
+        $this->table = get_class($this) . 's';
+    }
+
+    abstract public function toArray(): array;
+    abstract public function fromArray(array $data);
+}
\ No newline at end of file
diff --git a/src/server/app/App/Middleware.php b/src/server/app/App/Middleware.php
new file mode 100644
index 0000000000000000000000000000000000000000..10de9271143365a07e9f49692132943854cb5675
--- /dev/null
+++ b/src/server/app/App/Middleware.php
@@ -0,0 +1,6 @@
+<?php
+
+interface Middleware
+{
+    function run(): void;
+}
diff --git a/src/server/app/App/Repository.php b/src/server/app/App/Repository.php
new file mode 100644
index 0000000000000000000000000000000000000000..039c7d7383d4da539a43f191aa43e84ab5fdbedd
--- /dev/null
+++ b/src/server/app/App/Repository.php
@@ -0,0 +1,227 @@
+<?php
+require_once __DIR__ . '/../Utils/QueryBuilder.php';
+
+/**
+ * ABC for Repository
+ *
+ * Provides basic CRUD operations
+ */
+abstract class Repository
+{
+    protected \PDO $connection;
+    protected string $table;
+    protected QueryBuilder $queryBuilder;
+    protected string $currentQuery = "";
+
+    public function __construct(\PDO $connection)
+    {
+        $this->connection = $connection;
+        $this->queryBuilder = new QueryBuilder($this);
+    }
+
+    public function query()
+    {
+        $this->queryBuilder->query = "";
+        return $this->queryBuilder;
+    }
+
+    public function getTable()
+    {
+        return $this->table;
+    }
+
+    protected function reset()
+    {
+        $this->queryBuilder->query = "";
+        $this->currentQuery = "";
+    }
+
+    public function save(Domain $domain)
+    {
+        $array = $domain->toArray();
+        $domainKeyLength = count($array);
+        $this->currentQuery = "INSERT INTO {$this->table} (";
+
+        $countKey = 0;
+        foreach ($array as $key => $value) {
+            if ($key != 'id' || $this->table == "sessions") {
+                $this->currentQuery .= "$key";
+            }
+
+            if ($countKey < $domainKeyLength - 1) {
+                $this->currentQuery .= ", ";
+            }
+
+            $countKey += 1;
+        }
+
+        $this->currentQuery .= ") VALUES (";
+        $countKey = 0;
+        foreach ($array as $key => $value) {
+            if ($key != 'id' || $this->table == "sessions") {
+                $this->currentQuery .= ":$key";
+            }
+
+            if ($countKey < $domainKeyLength - 1) {
+                $this->currentQuery .= ", ";
+            }
+
+            $countKey += 1;
+        }
+
+        $this->currentQuery .= ")";
+        $statement = $this->connection->prepare($this->currentQuery);
+        foreach ($array as $key => $value) {
+            if ($key != 'id' || $this->table == "sessions") {
+                $statement->bindValue(":$key", $value);
+            }
+        }
+
+        $statement->execute();
+
+        $this->reset();
+
+        try {
+            if ($this->table != "sessions") {
+                $domain->id = $this->connection->lastInsertId();
+            }
+            return $domain;
+        } finally {
+            $statement->closeCursor();
+        }
+    }
+
+    public function findAll(array $projection = [], int|null $page = null, int|null $pageSize = null): array
+    {
+        $selectQuery = "SELECT ";
+        $pageCountQuery = "SELECT COUNT(*) ";
+
+        if (count($projection) === 0) {
+            $selectQuery .= "*";
+        } else {
+            $countProjection = 0;
+            foreach ($projection as $column) {
+                $selectQuery .= "$column AS " . str_replace(".", "_", $column);
+                if ($countProjection < count($projection) - 1) {
+                    $selectQuery .= ", ";
+                }
+                $countProjection += 1;
+            }
+        }
+
+        $this->currentQuery .= " FROM {$this->table}";
+
+        $this->currentQuery .= $this->queryBuilder->query;
+
+        $pageCountStatement = $this->connection->prepare($pageCountQuery . $this->currentQuery);
+        $pageCountStatement->execute();
+
+        if ($pageSize) {
+            $this->currentQuery .= " LIMIT $pageSize";
+        }
+
+        if ($page) {
+            $offset = ($page - 1) * $pageSize;
+            $this->currentQuery .= " OFFSET $offset";
+        }
+
+        $selectStatement = $this->connection->prepare($selectQuery . $this->currentQuery);
+        $selectStatement->execute();
+
+        $this->reset();
+
+        try {
+            return [
+                'items' => $selectStatement->fetchAll(),
+                'page' => $page ?? 1,
+                'totalPage' => $pageSize ? ceil($pageCountStatement->fetchColumn() / $pageSize) : 1
+            ];
+        } finally {
+            $selectStatement->closeCursor();
+            $pageCountStatement->closeCursor();
+        }
+    }
+
+    public function findOne($key, $value, $projection = [])
+    {
+        $this->currentQuery = "SELECT ";
+
+        if (count($projection) === 0) {
+            $this->currentQuery .= "*";
+        } else {
+            $countProjection = 0;
+            foreach ($projection as $column) {
+                $this->currentQuery .= "$column";
+                if ($countProjection < count($projection) - 1) {
+                    $this->currentQuery .= ", ";
+                }
+                $countProjection += 1;
+            }
+        }
+
+        $this->currentQuery .= " FROM {$this->table} WHERE $key = :$key LIMIT 1";
+
+        $statement = $this->connection->prepare($this->currentQuery);
+        $statement->bindValue(":$key", $value);
+
+        $statement->execute();
+
+        $this->reset();
+
+        try {
+            if ($row = $statement->fetch()) {
+                return $row;
+            }
+            return null;
+        } finally {
+            $statement->closeCursor();
+        }
+    }
+
+    public function update(Domain $domain)
+    {
+        $domainKeyLength = count($domain->toArray());
+        $this->currentQuery = "UPDATE {$this->table} SET ";
+        $countKey = 0;
+        foreach ($domain->toArray() as $key => $value) {
+            $countKey += 1;
+
+            $this->currentQuery .= "$key = :$key";
+            if ($countKey < $domainKeyLength) {
+                $this->currentQuery .= ", ";
+            }
+        }
+
+        $this->currentQuery .= " WHERE id = :id";
+
+        $statement = $this->connection->prepare($this->currentQuery);
+        $array = $domain->toArray();
+        foreach ($array as $key => $value) {
+            $statement->bindValue(":$key", $value);
+        }
+        $statement->bindValue(":id", $array['id'], \PDO::PARAM_INT);
+        $statement->execute();
+        $this->reset();
+        try {
+            return $domain;
+        } finally {
+            $statement->closeCursor();
+        }
+    }
+
+    public function deleteAll(): void
+    {
+        $this->connection->exec("DELETE FROM {$this->table}");
+        $this->reset();
+    }
+
+    public function deleteBy($key, $value): void
+    {
+        $statement = $this->connection->prepare("DELETE FROM {$this->table} WHERE $key = :$key");
+        $statement->bindValue(":$key", $value);
+        $statement->execute();
+        $this->reset();
+
+        $statement->closeCursor();
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/App/Router.php b/src/server/app/App/Router.php
new file mode 100644
index 0000000000000000000000000000000000000000..577789df93664c5eea5e95df9b6dbb7719e57f57
--- /dev/null
+++ b/src/server/app/App/Router.php
@@ -0,0 +1,52 @@
+<?php
+
+require_once __DIR__ . '/View.php';
+
+class Router
+{
+    private static array $routes = [];
+
+    public static function add(string $method, string $path, string $controller, string $function, array $middlewares = []): void
+    {
+        self::$routes[] = [
+            'method' => $method,
+            'path' => $path,
+            'controller' => $controller,
+            'function' => $function,
+            'middlewares' => $middlewares,
+        ];
+    }
+
+    public static function run(): void
+    {
+        $path = '/';
+        if (isset($_SERVER['PATH_INFO'])) {
+            $path = $_SERVER['PATH_INFO'];
+        }
+
+        $method = $_SERVER['REQUEST_METHOD'];
+
+        foreach (self::$routes as $route) {
+            $pattern = "#^" . $route['path'] . "$#";
+            if (preg_match($pattern, $path, $variables) && $method == $route['method']) {
+                // call middlewares
+                foreach ($route['middlewares'] as $middleware) {
+                    $instance = new $middleware;
+                    $instance->run();
+                }
+
+                $function = $route['function'];
+                $controller = new $route['controller'];
+
+                array_shift($variables);
+                call_user_func_array([$controller, $function], $variables);
+
+                return;
+            }
+        }
+
+        http_response_code(404);
+
+        View::redirect("/404");
+    }
+}
diff --git a/src/server/app/App/View.php b/src/server/app/App/View.php
new file mode 100644
index 0000000000000000000000000000000000000000..41c386e2540f66ed0cf3fde5b5f43084d4efc38e
--- /dev/null
+++ b/src/server/app/App/View.php
@@ -0,0 +1,24 @@
+<?php
+
+require_once __DIR__ . '/../Service/SessionService.php';
+
+class View
+{
+    public static function render(string $view, $model = [], SessionService $sessionService = null)
+    {
+        $user = $sessionService ? $sessionService->current() : null;
+
+        require __DIR__ . '/../View/components/header.php';
+        if (!isset($_SERVER['PATH_INFO']) || ($_SERVER['PATH_INFO'] != '/signin' && $_SERVER['PATH_INFO'] != '/signup')) {
+            require __DIR__ . '/../View/components/navbar.php';
+        }
+        require __DIR__ . '/../View/' . $view . '.php';
+        require __DIR__ . '/../View/components/footer.php';
+    }
+
+    public static function redirect(string $url)
+    {
+        header("Location: $url");
+        exit();
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Config/Database.php b/src/server/app/Config/Database.php
new file mode 100644
index 0000000000000000000000000000000000000000..d069393730a3a2e1f346cef091f6b8d2a008773d
--- /dev/null
+++ b/src/server/app/Config/Database.php
@@ -0,0 +1,36 @@
+<?php
+
+class Database
+{
+    private static ?\PDO $pdo = null;
+
+    public static function getConnection(string $env = 'dev'): \PDO
+    {
+        if (self::$pdo == null) {
+            // create new POD
+            require_once __DIR__ . '/../../config/database.php';
+            $config = getDatabaseConfig();
+            self::$pdo = new \PDO(
+                $config['database'][$env]['url'],
+                $config['database'][$env]['username'],
+                $config['database'][$env]['password']
+            );
+        }
+        return self::$pdo;
+    }
+
+    public static function beginTransaction()
+    {
+        self::$pdo->beginTransaction();
+    }
+
+    public static function commitTransaction()
+    {
+        self::$pdo->commit();
+    }
+
+    public static function rollbackTransaction()
+    {
+        self::$pdo->rollback();
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Controller/BookmarkController.php b/src/server/app/Controller/BookmarkController.php
new file mode 100644
index 0000000000000000000000000000000000000000..9b2c93f0bb57ae1b6c92386ef6377bdeb5659787
--- /dev/null
+++ b/src/server/app/Controller/BookmarkController.php
@@ -0,0 +1,75 @@
+<?php
+require_once __DIR__ . '/../Config/Database.php';
+require_once __DIR__ . '/../App/View.php';
+require_once __DIR__ . '/../Exception/ValidationException.php';
+
+require_once __DIR__ . '/../Repository/WatchlistSaveRepository.php';
+
+require_once __DIR__ . '/../Service/BookmarkService.php';
+require_once __DIR__ . '/../Service/SessionService.php';
+
+require_once __DIR__ . '/../Model/bookmark/BookmarkGetRequest.php';
+
+class BookmarkController
+{
+    private BookmarkService $bookmarkService;
+    private SessionService $sessionService;
+
+    public function __construct()
+    {
+        $connection = Database::getConnection();
+        $watchlistSaveRepository = new WatchlistSaveRepository($connection);
+        $this->bookmarkService = new BookmarkService($watchlistSaveRepository);
+
+        $sessionRepository = new SessionRepository($connection);
+        $userRepository = new UserRepository($connection);
+        $this->sessionService = new SessionService($sessionRepository, $userRepository);
+    }
+
+    public function self()
+    {
+        $page = $_GET['page'] ?? 1;
+        $pageSize = $_GET['pageSize'] ?? 10;
+
+        $user = $this->sessionService->current();
+        $request = new BookmarkGetRequest();
+        $request->userId = $user ? $user->id : null;
+        $request->page = $page;
+        $request->pageSize = $pageSize;
+
+        $result = $this->bookmarkService->findByUser($request);
+
+        function posterCompare($element1, $element2)
+        {
+            return $element1["rank"] - $element2["rank"];
+        }
+
+        $bookmarks = [];
+
+        foreach ($result["items"] as $item) {
+            $posters = json_decode($item["posters"], true);
+            $tags = json_decode($item["tags"], true);
+            $tags = array_filter($tags, function ($value) {
+                return $value["id"] !== null;
+            });
+            usort($posters, "posterCompare");
+            $item["posters"] = $posters;
+            $item["tags"] = $tags;
+
+            array_push($bookmarks, $item);
+        }
+
+        $result["items"] = $bookmarks;
+
+        View::render('profile/bookmark', [
+            'title' => 'Bookmark',
+            'js' => [
+                '/js/profile/bookmark.js'
+            ],
+            'data' => [
+                'bookmarks' => $result,
+                'userUUID' => $user ? $user->uuid : null,
+            ],
+        ], $this->sessionService);
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Controller/CatalogController.php b/src/server/app/Controller/CatalogController.php
new file mode 100644
index 0000000000000000000000000000000000000000..fca0b5899ec27f13fd25175a97ecb220540b3bbd
--- /dev/null
+++ b/src/server/app/Controller/CatalogController.php
@@ -0,0 +1,275 @@
+<?php
+
+require_once __DIR__ . '/../App/View.php';
+require_once __DIR__ . '/../Config/Database.php';
+require_once __DIR__ . '/../Exception/ValidationException.php';
+
+require_once __DIR__ . '/../Service/CatalogService.php';
+require_once __DIR__ . '/../Service/SessionService.php';
+
+require_once __DIR__ . '/../Repository/CatalogRepository.php';
+require_once __DIR__ . '/../Repository/UserRepository.php';
+require_once __DIR__ . '/../Repository/SessionRepository.php';
+
+require_once __DIR__ . '/../Model/CatalogCreateRequest.php';
+require_once __DIR__ . '/../Model/catalog/CatalogUpdateRequest.php';
+require_once __DIR__ . '/../Model/CatalogSearchRequest.php';
+
+class CatalogController
+{
+    private CatalogService $catalogService;
+    private SessionService $sessionService;
+
+    public function __construct()
+    {
+        $connection = Database::getConnection();
+
+        $catalogRepository = new CatalogRepository($connection);
+        $this->catalogService = new CatalogService($catalogRepository);
+
+        $sessionRepository = new SessionRepository($connection);
+        $userRepository = new UserRepository($connection);
+        $this->sessionService = new SessionService($sessionRepository, $userRepository);
+    }
+
+    public function index(): void
+    {
+        $page = $_GET['page'] ?? 1;
+        $category = $_GET['category'] ?? "MIXED";
+
+        $user = $this->sessionService->current();
+
+        View::render('catalog/index', [
+            'title' => 'Catalog',
+            'styles' => [
+                '/css/catalog.css',
+            ],
+            'js' => [
+                '/js/catalog/delete.js'
+            ],
+            'data' => [
+                'catalogs' => $this->catalogService->findAll($page, $category),
+                'category' => strtoupper(trim($category)),
+                'userRole' => $user ? $user->role : null
+            ]
+        ], $this->sessionService);
+    }
+
+    public function create(): void
+    {
+        View::render('catalog/form', [
+            'title' => 'Add Catalog',
+            'styles' => [
+                '/css/catalog-form.css',
+            ],
+            'js' => [
+                '/js/catalog/createUpdate.js'
+            ],
+            'type' => 'create'
+        ], $this->sessionService);
+    }
+
+    public function edit($uuid): void
+    {
+        $catalog = $this->catalogService->findByUUID($uuid);
+
+        if (!$catalog) {
+            View::redirect('/404');
+        }
+
+        View::render('catalog/form', [
+            'title' => 'Edit Catalog',
+            'styles' => [
+                '/css/catalog-form.css',
+            ],
+            'js' => [
+                '/js/catalog/createUpdate.js'
+            ],
+            'type' => 'edit',
+            'data' => $catalog->toArray()
+        ], $this->sessionService);
+    }
+
+    public function detail($uuid): void
+    {
+        $catalog = $this->catalogService->findByUUID($uuid);
+
+
+        if (!$catalog) {
+            View::redirect('/404');
+        }
+
+        $user = $this->sessionService->current();
+        View::render('catalog/detail', [
+            'title' => 'Catalog Detail',
+            'styles' => [
+                '/css/catalog-detail.css',
+            ],
+            'js' => [
+                '/js/catalog/delete.js'
+            ],
+            'data' => [
+                'item' => $catalog->toArray(),
+                'userRole' => $user ? $user->role : null
+            ]
+        ], $this->sessionService);
+    }
+
+    public function postCreate(): void
+    {
+        $request = new CatalogCreateRequest();
+        if (isset($_POST['category'])) {
+            $request->category = $_POST['category'];
+        }
+
+        $request->title = $_POST['title'];
+        $request->description = $_POST['description'];
+
+        if (isset($_FILES['poster'])) {
+            $request->poster = $_FILES['poster'];
+        }
+
+        if (isset($_FILES['trailer'])) {
+            $request->trailer = $_FILES['trailer'];
+        }
+
+        try {
+            $response = $this->catalogService->create($request);
+            http_response_code(200);
+            $response = [
+                "status" => 200,
+                "message" => "Successfully created catalog",
+                "data" => [
+                    "uuid" => $response->catalog->uuid,
+                    "title" => $response->catalog->title,
+                ]
+            ];
+
+            echo json_encode($response);
+        } catch (ValidationException $exception) {
+            http_response_code(400);
+            $response = [
+                "status" => 400,
+                "message" => $exception->getMessage(),
+            ];
+
+            echo json_encode($response);
+        } catch (\Exception $exception) {
+            http_response_code(500);
+            $response = [
+                "status" => 500,
+                "message" => "Something went wrong.",
+            ];
+
+            echo json_encode($response);
+        }
+    }
+
+    public function search()
+    {
+        $request = new CatalogSearchRequest();
+        $request->title = $_GET["title"];
+        $request->page = $_GET["page"];
+        $request->pageSize = $_GET["pageSize"];
+
+        $catalogs = $this->catalogService->search($request);
+
+        foreach ($catalogs->catalogs['items'] as $item) {
+            $title = $item->title;
+            $poster = $item->poster;
+            $uuid = $item->uuid;
+            $description = $item->description;
+            $category = $item->category;
+            $page = $catalogs->catalogs['page'];
+            require __DIR__ . '/../View/components/modal/watchlistAddSearchItem.php';
+        }
+    }
+
+    public function update($uuid): void
+    {
+        $user = $this->sessionService->current();
+        try {
+            if (!$user || $user->role !== 'ADMIN') {
+                throw new ValidationException("You are not authorized to update this catalog.");
+            }
+
+            $request = new CatalogUpdateRequest();
+
+            $request->uuid = $uuid;
+            $request->title = $_POST['title'];
+            $request->description = $_POST['description'];
+            $request->category = $_POST['category'];
+
+            if (isset($_FILES['poster'])) {
+                $request->poster = $_FILES['poster'];
+            }
+
+            if (isset($_FILES['trailer'])) {
+                $request->trailer = $_FILES['trailer'];
+            }
+
+            $this->catalogService->update($request);
+            http_response_code(200);
+            $response = [
+                "status" => 200,
+                "message" => "Successfully update catalog",
+            ];
+
+            echo json_encode($response);
+        } catch (ValidationException $exception) {
+            http_response_code(400);
+            $response = [
+                "status" => 400,
+                "message" => $exception->getMessage(),
+            ];
+
+            echo json_encode($response);
+        } catch (\Exception $exception) {
+            http_response_code(500);
+            $response = [
+                "status" => 500,
+                "message" => "Something went wrong.",
+            ];
+
+            echo json_encode($response);
+        }
+    }
+
+    public function delete(string $uuid)
+    {
+        $user = $this->sessionService->current();
+
+        try {
+            if ($user && $user->role === 'ADMIN') {
+                $this->catalogService->deleteByUUID($uuid);
+                http_response_code(200);
+
+                $response = [
+                    "status" => 200,
+                    "message" => "Successfully delete catalog",
+                ];
+
+                echo json_encode($response);
+            } else {
+                throw new ValidationException("You are not authorized to delete this catalog.");
+            }
+        } catch (ValidationException $exception) {
+            http_response_code(400);
+
+            $response = [
+                "status" => 400,
+                "message" => $exception->getMessage(),
+            ];
+
+            echo json_encode($response);
+        } catch (\Exception $exception) {
+            http_response_code(500);
+            $response = [
+                "status" => 500,
+                "message" => "Something went wrong.",
+            ];
+
+            echo json_encode($response);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Controller/ErrorPageController.php b/src/server/app/Controller/ErrorPageController.php
new file mode 100644
index 0000000000000000000000000000000000000000..139ea2f3a1c3d36ea67faf32f0a5c08c1c59d1f1
--- /dev/null
+++ b/src/server/app/Controller/ErrorPageController.php
@@ -0,0 +1,42 @@
+<?php
+require_once __DIR__ . '/../App/View.php';
+require_once __DIR__ . '/../Config/Database.php';
+
+require_once __DIR__ . '/../Service/SessionService.php';
+
+require_once __DIR__ . '/../Repository/UserRepository.php';
+require_once __DIR__ . '/../Repository/SessionRepository.php';
+
+class ErrorPageController
+{
+    private SessionService $sessionService;
+
+    public function __construct()
+    {
+        $connection = Database::getConnection();
+
+        $sessionRepository = new SessionRepository($connection);
+        $userRepository = new UserRepository($connection);
+        $this->sessionService = new SessionService($sessionRepository, $userRepository);
+    }
+
+    public function fourohfour(): void
+    {
+        View::render('404', [
+            'title' => '404',
+            'styles' => [
+                '/css/error-page.css',
+            ],
+        ], $this->sessionService);
+    }
+
+    public function fivehundred(): void
+    {
+        View::render('500', [
+            'title' => '500',
+            'styles' => [
+                '/css/error-page.css',
+            ],
+        ], $this->sessionService);
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Controller/HomeController.php b/src/server/app/Controller/HomeController.php
new file mode 100644
index 0000000000000000000000000000000000000000..3cbc1151903c1805988c8665c318ba0e7a6ba279
--- /dev/null
+++ b/src/server/app/Controller/HomeController.php
@@ -0,0 +1,143 @@
+<?php
+require_once __DIR__ . '/../App/View.php';
+require_once __DIR__ . '/../Config/Database.php';
+
+require_once __DIR__ . '/../Service/SessionService.php';
+require_once __DIR__ . '/../Service/TagService.php';
+
+require_once __DIR__ . '/../Repository/UserRepository.php';
+require_once __DIR__ . '/../Repository/SessionRepository.php';
+require_once __DIR__ . '/../Repository/WatchlistRepository.php';
+require_once __DIR__ . '/../Repository/WatchlistLikeRepository.php';
+require_once __DIR__ . '/../Repository/WatchlistSaveRepository.php';
+require_once __DIR__ . '/../Repository/WatchlistTagRepository.php';
+
+require_once __DIR__ . '/../Model/WatchlistsGetRequest.php';
+
+class HomeController
+{
+    private SessionService $sessionService;
+    private WatchlistService $watchlistService;
+    private TagService $tagService;
+
+    public function __construct()
+    {
+        $connection = Database::getConnection();
+
+        $sessionRepository = new SessionRepository($connection);
+        $userRepository = new UserRepository($connection);
+        $this->sessionService = new SessionService($sessionRepository, $userRepository);
+
+        $watchlistRepository = new WatchlistRepository($connection);
+        $watchlistItemRepository = new WatchlistItemRepository($connection);
+        $watchlistLikeRepository = new WatchlistLikeRepository($connection);
+        $watchlistSaveRepository = new WatchlistSaveRepository($connection);
+        $watchlistTagRepository = new WatchlistTagRepository($connection);
+        $this->watchlistService = new WatchlistService($watchlistRepository, $watchlistItemRepository, $watchlistLikeRepository, $watchlistSaveRepository, $watchlistTagRepository);
+
+        $tagRepository = new TagRepository($connection);
+        $this->tagService = new TagService($tagRepository);
+    }
+
+    public function index(): void
+    {
+        $data = $this->getWatchlist();
+
+        View::render('home/index', [
+            'title' => 'Homepage',
+            'data' => $data,
+            'styles' => [
+                '/css/home.css',
+            ],
+            'js' => [
+                '/js/home.js',
+            ],
+        ], $this->sessionService);
+    }
+
+    public function watchlists(): void
+    {
+        $data = $this->getWatchlist();
+
+        foreach ($data["items"] as $item) {
+            $uuid = $item["watchlist_uuid"];
+            $posters = $item["posters"];
+            $visibility = $item["visibility"];
+            $title = $item["title"];
+            $category = $item["category"];
+            $creator = $item["creator"];
+            $createdAt = $item["created_at"];
+            $description = $item["description"];
+            $itemCount = $item["item_count"];
+            $loveCount = $item["love_count"];
+            $loved = $item["loved"];
+            $saved = $item["saved"];
+            $self = ($data["userUUID"] == $item["creator_uuid"]);
+            $userUUID = $data["userUUID"];
+
+            require __DIR__ . '/../View/components/card/watchlistCard.php';
+        }
+
+        if (count($data["items"]) > 0) {
+            $currentPage = $data["page"];
+            $totalPage = $data["pageTotal"];
+            require __DIR__ . '/../View/components/pagination.php';
+        }
+    }
+
+    private function getWatchlist(): array
+    {
+        // Get current user
+        $user = $this->sessionService->current();
+
+        $tags = $this->tagService->findAll();
+        $tagsInit = [];
+
+        foreach ($tags["items"] as $tag) {
+            array_push($tagsInit, $tag->name);
+        }
+
+        // Get watchlists
+        $request = new WatchlistsGetRequest();
+        $request->category = $_GET["category"] ?? "";
+        $request->tags = $_GET["tags"] ?? "";
+        $request->sortBy = $_GET["sortBy"] ?? "";
+        $request->order = $_GET["order"] ?? "";
+        $request->page = $_GET["page"] ?? 1;
+        $request->tag = $_GET["tag"] ?? "";
+        $request->tagsInit = $tagsInit;
+        $request->search = isset($_GET["search"]) ? strtolower($_GET["search"]) : "";
+        $request->userId = $user->id ?? -1;
+
+
+        $result = $this->watchlistService->findAll($request);
+
+        function posterCompare($element1, $element2)
+        {
+            return $element1["rank"] - $element2["rank"];
+        }
+
+        $data = [
+            "items" => [],
+            "page" => $result["page"],
+            "pageTotal" => $result["pageTotal"],
+            "userUUID" => $user->uuid ?? "",
+            "tags" => $tagsInit
+        ];
+
+        foreach ($result["items"] as $item) {
+            $posters = json_decode($item["posters"], true);
+            $tags = json_decode($item["tags"], true);
+            $tags = array_filter($tags, function ($value) {
+                return $value["id"] !== null;
+            });
+            usort($posters, "posterCompare");
+            $item["posters"] = $posters;
+            $item["tags"] = $tags;
+
+            array_push($data["items"], $item);
+        }
+
+        return $data;
+    }
+}
diff --git a/src/server/app/Controller/UserController.php b/src/server/app/Controller/UserController.php
new file mode 100644
index 0000000000000000000000000000000000000000..54a9f7361f2e000b3a8428d449690ca104ac3609
--- /dev/null
+++ b/src/server/app/Controller/UserController.php
@@ -0,0 +1,285 @@
+<?php
+
+require_once __DIR__ . '/../App/View.php';
+require_once __DIR__ . '/../Config/Database.php';
+require_once __DIR__ . '/../Exception/ValidationException.php';
+
+require_once __DIR__ . '/../Repository/UserRepository.php';
+require_once __DIR__ . '/../Repository/SessionRepository.php';
+
+require_once __DIR__ . '/../Service/UserService.php';
+require_once __DIR__ . '/../Service/SessionService.php';
+
+require_once __DIR__ . '/../Model/UserSignUpRequest.php';
+require_once __DIR__ . '/../Model/UserSignInRequest.php';
+require_once __DIR__ . '/../Model/UserEditRequest.php';
+require_once __DIR__ . '/../Model/session/SessionCreateRequest.php';
+
+class UserController
+{
+    private UserService $userService;
+    private SessionService $sessionService;
+
+    public function __construct()
+    {
+        $connection = Database::getConnection();
+        $userRepository = new UserRepository($connection);
+        $this->userService = new UserService($userRepository);
+
+        $sessionRepository = new SessionRepository($connection);
+        $this->sessionService = new SessionService($sessionRepository, $userRepository);
+    }
+
+    public function signUp(): void
+    {
+        View::render('user/signUp', [
+            'title' => 'Sign Up',
+            'styles' => [
+                '/css/signUp.css',
+            ],
+        ], $this->sessionService);
+    }
+
+    public function signIn(): void
+    {
+        View::render('user/signIn', [
+            "title" => "Sign In",
+            "styles" => [
+                "/css/signIn.css",
+            ],
+        ], $this->sessionService);
+    }
+
+    public function profile(): void
+    {
+        View::render('user/editProfile', [
+            'title' => 'Profile',
+            'styles' => [
+                '/css/editProfile.css',
+            ],
+            'data' => [
+                'name' => 'Breezy',
+                'email' => 'sampleemail@gmail.com'
+            ],
+        ], $this->sessionService);
+    }
+
+    public function postSignUp(): void
+    {
+        $request = new UserSignUpRequest();
+
+        $request->email = $_POST['email'];
+        $request->password = $_POST['password'];
+        $request->confirmPassword = $_POST['passwordConfirm'];
+
+        try {
+            $this->userService->signUp($request);
+
+            View::redirect('/signin');
+        } catch (ValidationException $exception) {
+            View::render('user/signUp', [
+                'title' => 'Sign Up',
+                'error' => $exception->getMessage(),
+                'styles' => [
+                    '/css/signUp.css',
+                ],
+            ], $this->sessionService);
+        }
+    }
+
+
+    public function postSignIn(): void
+    {
+        $request = new UserSignInRequest();
+        $request->email = $_POST['email'];
+        $request->password = $_POST['password'];
+
+        try {
+            $response = $this->userService->signIn($request);
+
+            $sessionCreateRequest = new SessionCreateRequest();
+            $sessionCreateRequest->userId = $response->user->id;
+
+            $this->sessionService->create($sessionCreateRequest);
+
+            View::redirect('/');
+        } catch (ValidationException $exception) {
+            View::render('user/signIn', [
+                "title" => "Sign In",
+                "data" => [
+                    "email" => $request->email,
+                ],
+                "error" => $exception->getMessage(),
+                "styles" => [
+                    "/css/signIn.css",
+                ],
+            ], $this->sessionService);
+        }
+    }
+
+
+    public function showEditProfile(): void
+    {
+        $currentUser = $this->sessionService->current();
+        View::render('user/editProfile', [
+            'title' => 'Drawl | Edit Profile',
+            'styles' => [
+                '/css/editProfile.css',
+            ],
+            'js' => [
+                '/js/profile.js'
+            ],
+            'data' => ['name' => $currentUser->name, 'email' => $currentUser->email]
+        ], $this->sessionService);
+    }
+
+    public function logout(): void
+    {
+        try {
+            $this->sessionService->destroy();
+            http_response_code(200);
+            $response = [
+                "status" => 200,
+                "message" => "Logout success.",
+            ];
+        } catch (ValidationException $exception) {
+            http_response_code(400);
+            $response = [
+                "status" => 400,
+                "message" => $exception->getMessage(),
+            ];
+
+            echo json_encode($response);
+        } catch (\Exception $exception) {
+            http_response_code(500);
+            $response = [
+                "status" => 500,
+                "message" => "Something went wrong.",
+            ];
+
+            echo json_encode($response);
+        }
+    }
+
+    public function postEditProfile(): void
+    {
+        $request = new UserEditRequest();
+        $request->name = $_POST['name'];
+        $request->email = $_POST["email"] ?? null; // or from session
+        $request->oldPassword = $_POST['oldPassword'];
+        $request->newPassword = $_POST['newPassword'];
+
+        $currentUser = $this->sessionService->current();
+
+        try {
+            $this->userService->update($currentUser, $request);
+            View::redirect('/profile');
+        } catch (ValidationException $exception) {
+            View::render('user/editProfile', [
+                'title' => 'Drawl | Edit Profile',
+                'error' => $exception->getMessage(),
+                'styles' => [
+                    '/css/editProfile.css',
+                ],
+                'data' => [
+                    'name' => $currentUser->name,
+                    'email' => $currentUser->email
+                ],
+            ], $this->sessionService);
+        }
+    }
+
+    public function update(): void
+    {
+        $request = new UserEditRequest();
+
+        $json = file_get_contents('php://input');
+        $data = json_decode($json);
+
+        if ($data === null) {
+            http_response_code(400);
+            $response = [
+                "status" => 400,
+                "message" => "Invalid request.",
+            ];
+
+            echo json_encode($response);
+            return;
+        }
+
+
+        $request->name = $data->name;
+        $request->oldPassword = $data->oldPassword;
+        $request->newPassword = $data->newPassword;
+
+        $currentUser = $this->sessionService->current();
+
+        try {
+            $this->userService->update($currentUser, $request);
+            http_response_code(200);
+            $response = [
+                "status" => 200,
+                "message" => "Successfully update user",
+                "name" => $request->name,
+            ];
+
+            echo json_encode($response);
+        } catch (ValidationException $exception) {
+            http_response_code($exception->getCode() ?? 400);
+
+            $response = [
+                "status" => $exception->getCode() ?? 400,
+                "message" => $exception->getMessage(),
+            ];
+
+            echo json_encode($response);
+        } catch (\Exception $exception) {
+            http_response_code(500);
+            $response = [
+                "status" => 500,
+                "message" => "Something went wrong.",
+            ];
+
+            echo json_encode($response);
+        }
+    }
+
+    public function delete(): void
+    {
+        $currentUser = $this->sessionService->current();
+
+        try {
+
+            if (!$currentUser) {
+                throw new ValidationException("Unauthorized.", 401);
+            }
+            $this->userService->deleteBySession($currentUser->email);
+            $this->userService->deleteByEmail($currentUser->email);
+            http_response_code(200);
+
+            $response = [
+                "status" => 200,
+                "message" => "Successfully delete user",
+            ];
+
+            echo json_encode($response);
+        } catch (ValidationException $exception) {
+            http_response_code($exception->getCode() ?? 400);
+
+            $response = [
+                "status" => $exception->getCode() ?? 400,
+                "message" => $exception->getMessage(),
+            ];
+
+            echo json_encode($response);
+        } catch (\Exception $exception) {
+            http_response_code(500);
+            $response = [
+                "status" => 500,
+                "message" => "Something went wrong.",
+            ];
+
+            echo json_encode($response);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Controller/WatchlistController.php b/src/server/app/Controller/WatchlistController.php
new file mode 100644
index 0000000000000000000000000000000000000000..adcee4eb8996fc3c7b2640487c89f0c34642e4cc
--- /dev/null
+++ b/src/server/app/Controller/WatchlistController.php
@@ -0,0 +1,443 @@
+<?php
+require_once __DIR__ . '/../Config/Database.php';
+require_once __DIR__ . '/../App/View.php';
+require_once __DIR__ . '/../Exception/ValidationException.php';
+
+require_once __DIR__ . '/../Repository/CatalogRepository.php';
+require_once __DIR__ . '/../Repository/WatchlistRepository.php';
+require_once __DIR__ . '/../Repository/WatchlistItemRepository.php';
+require_once __DIR__ . '/../Repository/WatchlistLikeRepository.php';
+require_once __DIR__ . '/../Repository/WatchlistSaveRepository.php';
+require_once __DIR__ . '/../Repository/TagRepository.php';
+require_once __DIR__ . '/../Repository/WatchlistTagRepository.php';
+
+require_once __DIR__ . '/../Service/CatalogService.php';
+require_once __DIR__ . '/../Service/WatchlistService.php';
+require_once __DIR__ . '/../Service/SessionService.php';
+require_once __DIR__ . '/../Service/TagService.php';
+
+require_once __DIR__ . '/../Model/CatalogCreateRequest.php';
+require_once __DIR__ . '/../Model/WatchlistAddItemRequest.php';
+require_once __DIR__ . '/../Model/WatchlistCreateRequest.php';
+require_once __DIR__ . '/../Model/watchlist/WatchlistGetOneByUserRequest.php';
+require_once __DIR__ . '/../Model/watchlist/WatchlistGetOneRequest.php';
+require_once __DIR__ . '/../Model/watchlist/WatchlistLikeRequest.php';
+require_once __DIR__ . '/../Model/watchlist/WatchlistSaveRequest.php';
+require_once __DIR__ . '/../Model/watchlist/WatchlistEditRequest.php';
+require_once __DIR__ . '/../Model/watchlist/WatchlistDeleteRequest.php';
+
+class WatchlistController
+{
+    private CatalogService $catalogService;
+    private WatchlistService $watchlistService;
+    private SessionService $sessionService;
+    private TagService $tagService;
+
+    public function __construct()
+    {
+        $connection = Database::getConnection();
+        $catalogRepository = new CatalogRepository($connection);
+        $this->catalogService = new CatalogService($catalogRepository);
+
+        $watchlistRepository = new WatchlistRepository($connection);
+        $watchlistItemRepository = new WatchlistItemRepository($connection);
+        $watchlistLikeRepository = new WatchlistLikeRepository($connection);
+        $watchlistSaveRepository = new WatchlistSaveRepository($connection);
+        $watchlistTagRepository = new WatchlistTagRepository($connection);
+        $this->watchlistService = new WatchlistService($watchlistRepository, $watchlistItemRepository, $watchlistLikeRepository, $watchlistSaveRepository, $watchlistTagRepository);
+
+        $sessionRepository = new SessionRepository($connection);
+        $userRepository = new UserRepository($connection);
+        $this->sessionService = new SessionService($sessionRepository, $userRepository);
+
+        $tagRepository = new TagRepository($connection);
+        $this->tagService = new TagService($tagRepository);
+    }
+
+    public function create(): void
+    {
+        $tags = $this->tagService->findAll();
+
+        View::render('watchlist/createUpdate', [
+            'title' => 'Create Watchlist',
+            'description' => 'Create new watchlist',
+            "data" => [
+                "tags" => $tags["items"],
+            ],
+            'styles' => [
+                '/css/watchlistCreate.css',
+                '/css/components/watchlist/watchlistItem.css',
+                '/css/components/modal/watchlistAddItem.css',
+                '/css/components/modal/watchlistAddSearchItem.css',
+            ],
+            'js' => [
+                '/js/watchlistCreate.js',
+                '/js/watchlist/createUpdate.js',
+                '/js/components/modal/watchlistAddItem.js',
+                '/js/components/watchlist/watchlistItem.js',
+            ]
+        ], $this->sessionService);
+    }
+
+    public function postCreate(): void
+    {
+        $user = $this->sessionService->current();
+
+        $dataRaw = file_get_contents("php://input");
+        $data = json_decode($dataRaw, true);
+
+        $tags = $this->tagService->findAll();
+
+        $request = new WatchlistCreateRequest();
+        $request->title = $data["title"];
+        $request->description = $data["description"];
+        $request->visibility = $data["visibility"];
+        $request->items = $data["items"];
+        $request->tags = $data["tags"];
+        $request->userId = $user->id;
+        $request->initialTags = $tags["items"];
+
+        try {
+            $this->watchlistService->create($request);
+
+            $response = [
+                "status" => 200,
+                "message" => "Watchlist successfully created",
+                "redirectTo" => "/profile/watchlist",
+            ];
+
+            print_r(json_encode($response));
+        } catch (ValidationException $exception) {
+            http_response_code(500);
+
+            $response = [
+                "status" => 500,
+                "message" => $exception->getMessage()
+            ];
+
+            print_r(json_encode($response));
+        }
+    }
+
+    public function edit(string $uuid): void
+    {
+        $user = $this->sessionService->current();
+
+        $tags = $this->tagService->findAll();
+
+        $getRequest = new WatchlistsGetOneRequest();
+        $getRequest->uuid = $uuid;
+        $getRequest->page = 1;
+        $getRequest->pageSize = 100;
+
+        $watchlist = $this->watchlistService->findByUUID($getRequest);
+
+        if ($watchlist == null || $user->uuid !== $watchlist["creator_uuid"]) {
+            View::redirect("/404");
+        }
+
+        View::render('watchlist/createUpdate', [
+            'title' => 'Edit Watchlist',
+            'description' => 'Edit watchlist',
+            'edit' => true,
+            'data' => [
+                "title" => $watchlist["title"],
+                "description" => $watchlist["description"],
+                "visibility" => $watchlist["visibility"],
+                "catalogs" => $watchlist["catalogs"],
+                "tagsSelected" => $watchlist["tags"],
+                "tags" => $tags["items"],
+            ],
+            'styles' => [
+                '/css/watchlistCreate.css',
+                '/css/components/watchlist/watchlistItem.css',
+                '/css/components/modal/watchlistAddItem.css',
+                '/css/components/modal/watchlistAddSearchItem.css',
+            ],
+            'js' => [
+                '/js/watchlistCreate.js',
+                '/js/watchlist/createUpdate.js',
+                '/js/components/modal/watchlistAddItem.js',
+                '/js/components/watchlist/watchlistItem.js',
+            ]
+        ], $this->sessionService);
+    }
+
+    public function putEdit(): void
+    {
+        $user = $this->sessionService->current();
+
+        $dataRaw = file_get_contents("php://input");
+        $data = json_decode($dataRaw, true);
+
+        $getRequest = new WatchlistsGetOneRequest();
+        $getRequest->uuid = $data["watchlistUUID"];
+        $getRequest->page = 1;
+        $getRequest->pageSize = 100;
+
+        $watchlist = $this->watchlistService->findByUUID($getRequest);
+
+        if ($watchlist == null || $user->uuid !== $watchlist["creator_uuid"]) {
+            http_response_code(400);
+
+            $response = [
+                "status" => 400,
+                "message" => "Watchlist not found.",
+            ];
+
+            print_r(json_encode($response));
+        }
+
+        $tags = $this->tagService->findAll();
+
+        $request = new WatchlistEditRequest();
+        $request->watchlist = $watchlist;
+        $request->userId = $user->id;
+        $request->title = $data["title"];
+        $request->description = $data["description"];
+        $request->visibility = $data["visibility"];
+        $request->items = $data["items"];
+        $request->tags = $data["tags"];
+        $request->initialTags = $tags["items"];
+
+        try {
+            $this->watchlistService->edit($request);
+
+            $response = [
+                "status" => 200,
+                "message" => "Watchlist edited successfully",
+                "redirectTo" => "/watchlist/{$watchlist["watchlist_uuid"]}",
+            ];
+
+            print_r(json_encode($response));
+        } catch (Exception $exception) {
+            http_response_code(500);
+
+            $response = [
+                "status" => 500,
+                "message" => "Internal server error. Please try again later."
+            ];
+
+            print_r(json_encode($response));
+        }
+    }
+
+    public function detail(string $uuid): void
+    {
+        $user = $this->sessionService->current();
+        $request = new WatchlistsGetOneRequest();
+        $request->uuid = $uuid;
+        $request->page = $_GET["page"] ?? 1;
+        $request->userId = $user ? $user->id : -1;
+
+        $result = $this->watchlistService->findByUUID($request);
+        if ($result == null) {
+            View::redirect('/404');
+        }
+
+        View::render('watchlist/detail', [
+            'title' => 'Watchlist',
+            'styles' => [
+                '/css/watchlist-detail.css',
+            ],
+            'js' => [
+                '/js/watchlist/detail.js',
+                '/js/watchlist/delete.js',
+            ],
+            'data' => [
+                'item' => $result,
+                'userUUID' => $user ? $user->uuid : null
+            ]
+        ], $this->sessionService);
+    }
+
+    public function item(): void
+    {
+        $request = new WatchlistAddItemRequest();
+        $request->id = $_GET["id"];
+
+        $response = $this->catalogService->findByUUID($request->id);
+        if (isset($response)) {
+            $id = $response->id;
+            $title = $response->title;
+            $poster = $response->poster;
+            $uuid = $response->uuid;
+            $category = $response->category;
+            require __DIR__ . '/../View/components/watchlist/watchlistItem.php';
+        }
+    }
+
+    public function self()
+    {
+        $user = $this->sessionService->current();
+
+        $request = new WatchlistGetOneByUserRequest();
+        $request->visibility = $_GET["visibility"] ?? "";
+        $request->userId = $user->id;
+
+        $result = $this->watchlistService->findByUser($request);
+
+        function posterCompare($element1, $element2)
+        {
+            return $element1["rank"] - $element2["rank"];
+        }
+
+        $watchlists = [];
+
+        foreach ($result["items"] as $item) {
+            $posters = json_decode($item["posters"], true);
+            $tags = json_decode($item["tags"], true);
+            $tags = array_filter($tags, function ($value) {
+                return $value["id"] !== null;
+            });
+            usort($posters, "posterCompare");
+            $item["posters"] = $posters;
+            $item["tags"] = $tags;
+
+            array_push($watchlists, $item);
+        }
+
+        $result["items"] = $watchlists;
+
+        View::render('profile/watchlist', [
+            'title' => 'My Watchlist',
+            'description' => 'My watchlist',
+            'styles' => [
+                '/css/watchlist-self.css',
+            ],
+            'js' => [
+                '/js/profile/watchlist.js',
+            ],
+            'data' => [
+                'visibility' => strtolower($_GET['visibility'] ?? 'all'),
+                'watchlists' => $result,
+                'userUUID' => $user->uuid
+            ]
+        ], $this->sessionService);
+    }
+
+    public function like(): void
+    {
+        $user = $this->sessionService->current();
+
+        if ($user == null) {
+            http_response_code(400);
+            $response = [
+                "message" => "Please login before liking this watchlist.",
+            ];
+            print_r(json_encode($response));
+            return;
+        }
+
+        $dataRaw = file_get_contents("php://input");
+        $data = json_decode($dataRaw, true);
+
+        $watchlistLikeRequest = new WatchlistLikeRequest();
+        $watchlistLikeRequest->watchlistUUID = $data["watchlistUUID"] ?? "";
+        $watchlistLikeRequest->userId = $user->id;
+
+        try {
+            $this->watchlistService->like($watchlistLikeRequest);
+            http_response_code(200);
+            $response = [
+                "status" => 200,
+                "message" => "Success",
+            ];
+
+            print_r(json_encode($response));
+        } catch (ValidationException $exception) {
+            http_response_code(500);
+
+            $response = [
+                "status" => 500,
+                "message" => $exception->getMessage(),
+            ];
+
+            print_r(json_encode($response));
+        }
+
+    }
+
+    public function bookmark(): void
+    {
+        $user = $this->sessionService->current();
+
+        $dataRaw = file_get_contents("php://input");
+        $data = json_decode($dataRaw, true);
+
+        $watchlistSaveRequest = new WatchlistSaveRequest();
+        $watchlistSaveRequest->watchlistUUID = $data["watchlistUUID"] ?? "";
+        $watchlistSaveRequest->userId = $user->id;
+
+        try {
+            $this->watchlistService->bookmark($watchlistSaveRequest);
+            http_response_code(200);
+
+            $response = [
+                "status" => 200,
+                "message" => "Success",
+            ];
+
+            print_r(json_encode($response));
+        } catch (ValidationException $exception) {
+            http_response_code(500);
+
+            $response = [
+                "status" => 500,
+                "message" => $exception->getMessage(),
+            ];
+
+            print_r(json_encode($response));
+        }
+    }
+
+    public function delete()
+    {
+        $user = $this->sessionService->current();
+
+        $dataRaw = file_get_contents("php://input");
+        $data = json_decode($dataRaw, true);
+
+        $getRequest = new WatchlistsGetOneRequest();
+        $getRequest->uuid = $data["watchlistUUID"];
+        $getRequest->page = 1;
+        $getRequest->pageSize = 100;
+
+        $watchlist = $this->watchlistService->findByUUID($getRequest);
+
+        if ($watchlist == null || $user->uuid !== $watchlist["creator_uuid"]) {
+            http_response_code(400);
+
+            $response = [
+                "status" => 400,
+                "message" => "Watchlist not found.",
+            ];
+
+            print_r(json_encode($response));
+        }
+
+        $request = new WatchlistDeleteRequest();
+        $request->watchlistUUID = $data["watchlistUUID"];
+
+        try {
+            $this->watchlistService->deleteByUUID($request);
+
+            $response = [
+                "status" => 200,
+                "message" => "Watchlist deleted successfully",
+                "redirectTo" => "/profile/watchlist",
+            ];
+
+            print_r(json_encode($response));
+        } catch (Exception $exception) {
+            http_response_code(500);
+
+            $response = [
+                "status" => 500,
+                "message" => "Failed to delete watchlist. " . $exception->getMessage(),
+            ];
+
+            print_r(json_encode($response));
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Domain/Catalog.php b/src/server/app/Domain/Catalog.php
new file mode 100644
index 0000000000000000000000000000000000000000..29fee4dd252d8cc3c391f1dfc45f6b01d6001801
--- /dev/null
+++ b/src/server/app/Domain/Catalog.php
@@ -0,0 +1,86 @@
+<?php
+
+require_once __DIR__ . '/../App/Domain.php';
+
+class Catalog extends Domain
+{
+    public int|string $id;
+    public string $uuid;
+    public string $title;
+    public ?string $description = null;
+    public string $poster;
+    public ?string $trailer = null;
+    public string $category;
+
+    public string $createdAt;
+    public string $updatedAt;
+
+    public function toArray(): array
+    {
+        $array = [
+            'uuid' => $this->uuid,
+            'title' => $this->title,
+            'description' => $this->description,
+            'poster' => $this->poster,
+            'trailer' => $this->trailer,
+            'category' => $this->category
+        ];
+
+        if (isset($this->id)) {
+            $array['id'] = $this->id;
+        }
+
+        if (isset($this->createdAt)) {
+            $array['created_at'] = $this->createdAt;
+        }
+
+        if (isset($this->updatedAt)) {
+            $array['updated_at'] = $this->updatedAt;
+        }
+
+        return $array;
+    }
+
+    public function fromArray(array $data)
+    {
+        if (isset($data['id'])) {
+            $this->id = $data['id'];
+        }
+
+        if (isset($data['uuid'])) {
+            $this->uuid = $data['uuid'];
+        }
+
+        if (isset($data['title'])) {
+            $this->title = $data['title'];
+        }
+
+        if (isset($data['description'])) {
+            $this->description = $data['description'];
+        }
+
+        if (isset($data['poster'])) {
+            if (file_exists('assets/images/catalogs/posters/' . $data['poster'])) {
+                $this->poster = $data['poster'];
+            } else {
+                $this->poster = 'no-poster.webp';
+            }
+        }
+
+        if (isset($data['trailer'])) {
+            $this->trailer = $data['trailer'];
+        }
+
+        if (isset($data['category'])) {
+            $this->category = $data['category'];
+        }
+
+        if (isset($data['created_at'])) {
+            $this->createdAt = $data['created_at'];
+        }
+
+        if (isset($data['updated_at'])) {
+            $this->updatedAt = $data['updated_at'];
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Domain/Session.php b/src/server/app/Domain/Session.php
new file mode 100644
index 0000000000000000000000000000000000000000..8120983b55a5fd92e51d558d680c24b29bad9ec9
--- /dev/null
+++ b/src/server/app/Domain/Session.php
@@ -0,0 +1,54 @@
+<?php
+
+require_once __DIR__ . '/../App/Domain.php';
+
+class Session extends Domain
+{
+    public int|string $id;
+    public string $userId;
+    public string $expired;
+    public string $createdAt;
+    public string $updatedAt;
+
+    public function toArray(): array
+    {
+        $array = [
+            "id" => $this->id,
+            "user_id" => $this->userId,
+            "expired" => $this->expired
+        ];
+
+        if (isset($this->createdAt)) {
+            $array["created_at"] = $this->createdAt;
+        }
+
+        if (isset($this->updatedAt)) {
+            $array["updated_at"] = $this->updatedAt;
+        }
+
+        return $array;
+    }
+
+    public function fromArray(array $data)
+    {
+        if (isset($data["id"])) {
+            $this->id = $data["id"];
+        }
+
+        if (isset($data["user_id"])) {
+            $this->userId = $data["user_id"];
+        }
+
+        if (isset($data["expired"])) {
+            $this->expired = $data["expired"];
+        }
+
+        if (isset($data["created_at"])) {
+            $this->createdAt = $data["created_at"];
+        }
+
+        if (isset($data["updated_at"])) {
+            $this->updatedAt = $data["updated_at"];
+        }
+    }
+}
diff --git a/src/server/app/Domain/Tag.php b/src/server/app/Domain/Tag.php
new file mode 100644
index 0000000000000000000000000000000000000000..f48a5e42741c754d928de95c2d5eb527aa9ac221
--- /dev/null
+++ b/src/server/app/Domain/Tag.php
@@ -0,0 +1,42 @@
+<?php
+
+require_once __DIR__ . '/../App/Domain.php';
+
+class Tag extends Domain
+{
+    public int|string $id;
+    public string $name;
+    public string $createdAt;
+
+    public function toArray(): array
+    {
+        $array = [
+            "name" => $this->name,
+        ];
+
+        if (isset($this->id)) {
+            $array["id"] = $this->id;
+        }
+
+        if (isset($this->createdAt)) {
+            $array["created_at"] = $this->createdAt;
+        }
+
+        return $array;
+    }
+
+    public function fromArray(array $data)
+    {
+        if (isset($data["id"])) {
+            $this->id = $data["id"];
+        }
+
+        if (isset($data["name"])) {
+            $this->name = $data["name"];
+        }
+
+        if (isset($data["created_at"])) {
+            $this->createdAt = $data["created_at"];
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Domain/User.php b/src/server/app/Domain/User.php
new file mode 100644
index 0000000000000000000000000000000000000000..68ea518b8760ed50443c8a5e9fd6622918168662
--- /dev/null
+++ b/src/server/app/Domain/User.php
@@ -0,0 +1,78 @@
+<?php
+
+require_once __DIR__ . '/../App/Domain.php';
+
+class User extends Domain
+{
+    public int|string $id;
+    public string $uuid;
+    public string $name;
+    public string $password;
+    public string $email;
+    public string $role;
+    public string $createdAt;
+    public string $updatedAt;
+
+    public function toArray(): array
+    {
+        $array = [
+            "uuid" => $this->uuid,
+            "name" => $this->name,
+            "password" => $this->password,
+            "email" => $this->email,
+        ];
+
+        if (isset($this->id)) {
+            $array["id"] = $this->id;
+        }
+
+        if (isset($this->role)) {
+            $array["role"] = $this->role;
+        }
+
+        if (isset($this->createdAt)) {
+            $array["created_at"] = $this->createdAt;
+        }
+
+        if (isset($this->updatedAt)) {
+            $array["updated_at"] = $this->updatedAt;
+        }
+
+        return $array;
+    }
+
+    public function fromArray(array $data)
+    {
+        if (isset($data["id"])) {
+            $this->id = $data["id"];
+        }
+
+        if (isset($data["uuid"])) {
+            $this->uuid = $data["uuid"];
+        }
+
+        if (isset($data["name"])) {
+            $this->name = $data["name"];
+        }
+
+        if (isset($data["password"])) {
+            $this->password = $data["password"];
+        }
+
+        if (isset($data["email"])) {
+            $this->email = $data["email"];
+        }
+
+        if (isset($data["role"])) {
+            $this->role = $data["role"];
+        }
+
+        if (isset($data["created_at"])) {
+            $this->createdAt = $data["created_at"];
+        }
+
+        if (isset($data["updated_at"])) {
+            $this->updatedAt = $data["updated_at"];
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Domain/Watchlist.php b/src/server/app/Domain/Watchlist.php
new file mode 100644
index 0000000000000000000000000000000000000000..1b8701c41de1d16e1ca1be921dbff60083613e68
--- /dev/null
+++ b/src/server/app/Domain/Watchlist.php
@@ -0,0 +1,117 @@
+<?php
+
+require_once __DIR__ . '/../App/Domain.php';
+
+class Watchlist extends Domain
+{
+    public int|string $id;
+    public string $uuid;
+    public string $title;
+    public ?string $description;
+    public string $category;
+    public int $userId;
+    public int $itemCount;
+    public int $likeCount;
+    public string $visibility;
+    public string $createdAt;
+    public string $updatedAt;
+    public array $items;
+    public User $user;
+
+    public function toArray(): array
+    {
+        $array = [
+            "title" => $this->title,
+            "uuid" => $this->uuid,
+            "description" => $this->description,
+            "category" => $this->category,
+            "user_id" => $this->userId,
+            "visibility" => $this->visibility,
+        ];
+
+        if (isset($this->id)) {
+            $array["id"] = $this->id;
+        }
+
+        if (isset($this->itemCount)) {
+            $array["item_count"] = $this->itemCount;
+        }
+
+        if (isset($this->likeCount)) {
+            $array["like_count"] = $this->likeCount;
+        }
+
+        if (isset($this->createdAt)) {
+            $array["created_at"] = $this->createdAt;
+        }
+
+        if (isset($this->updatedAt)) {
+            $array["updated_at"] = $this->updatedAt;
+        }
+
+        if (isset($this->items)) {
+            $array["items"] = $this->items;
+        }
+
+        if (isset($this->user)) {
+            $array["user"] = $this->user;
+        }
+
+        return $array;
+    }
+
+    public function fromArray(array $data)
+    {
+        if (isset($data["id"]) || isset($data["watchlists_id"])) {
+            $this->id = $data["id"] ?? $data["watchlists_id"];
+        }
+
+        if (isset($data["uuid"]) || isset($data["watchlists_uuid"])) {
+            $this->uuid = $data["uuid"] ?? $data["watchlists_uuid"];
+        }
+
+        if (isset($data["title"]) || isset($data["watchlists_title"])) {
+            $this->title = $data["title"] ?? $data["watchlists_title"];
+        }
+
+        if (isset($data["description"])) {
+            $this->description = $data["description"];
+        }
+
+        if (isset($data["category"])) {
+            $this->category = $data["category"];
+        }
+
+        if (isset($data["user_id"])) {
+            $this->userId = $data["user_id"];
+        }
+
+        if (isset($data["item_count"])) {
+            $this->itemCount = $data["item_count"];
+        }
+
+        if (isset($data["like_count"])) {
+            $this->likeCount = $data["like_count"];
+        }
+
+        if (isset($data["visibility"])) {
+            $this->visibility = $data["visibility"];
+        }
+
+        if (isset($data["created_at"])) {
+            $this->createdAt = $data["created_at"];
+        }
+
+        if (isset($data["updated_at"])) {
+            $this->updatedAt = $data["updated_at"];
+        }
+
+        if (isset($data["items"])) {
+            $this->items = $data["items"];
+        }
+
+        if (isset($data["user"])) {
+            $this->user = $data["user"];
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Domain/WatchlistItem.php b/src/server/app/Domain/WatchlistItem.php
new file mode 100644
index 0000000000000000000000000000000000000000..b2aaef304f661e0c3e744ae891fafe02e1cefa7e
--- /dev/null
+++ b/src/server/app/Domain/WatchlistItem.php
@@ -0,0 +1,63 @@
+<?php
+
+require_once  __DIR__ . '/../App/Domain.php';
+
+class WatchlistItem extends Domain
+{
+    public int|string $id;
+    public string $uuid;
+    public int $rank;
+    public ?string $description;
+    public int $watchlistId;
+    public int $catalogId;
+    public string $createdAt;
+    public string $updatedAt;
+
+    public function toArray(): array
+    {
+        $array = [
+            'uuid' => $this->uuid,
+            'rank' => $this->rank,
+            'description' => $this->description,
+            'watchlist_id' => $this->watchlistId,
+            'catalog_id' => $this->catalogId
+        ];
+
+        return $array;
+    }
+
+    public function fromArray(array $data)
+    {
+        if (isset($data["id"])) {
+            $this->id = $data["id"];
+        }
+
+        if (isset($data["uuid"])) {
+            $this->uuid = $data["uuid"];
+        }
+
+        if (isset($data["rank"])) {
+            $this->rank = $data["rank"];
+        }
+
+        if (isset($data["description"])) {
+            $this->description = $data["description"];
+        }
+
+        if (isset($data["watchlist_id"])) {
+            $this->watchlistId = $data["watchlist_id"];
+        }
+
+        if (isset($data["catalog_id"])) {
+            $this->catalogId = $data["catalog_id"];
+        }
+
+        if (isset($data["created_at"])) {
+            $this->createdAt = $data["created_at"];
+        }
+
+        if (isset($data["updated_at"])) {
+            $this->updatedAt = $data["updated_at"];
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Domain/WatchlistLike.php b/src/server/app/Domain/WatchlistLike.php
new file mode 100644
index 0000000000000000000000000000000000000000..19ed04f8eb83cbbd5480a70878e06419ab5b1d72
--- /dev/null
+++ b/src/server/app/Domain/WatchlistLike.php
@@ -0,0 +1,33 @@
+<?php
+
+class WatchlistLike extends Domain
+{
+    public int|string $watchlistId;
+    public int|string $userId;
+
+    public function toArray(): array
+    {
+        $array = [];
+
+        if (isset($this->watchlistId)) {
+            $array["watchlist_id"] = $this->watchlistId;
+        }
+
+        if (isset($this->userId)) {
+            $array["user_id"] = $this->userId;
+        }
+
+        return $array;
+    }
+
+    public function fromArray(array $data)
+    {
+        if (isset($data["watchlist_id"])) {
+            $this->watchlistId = $data["watchlist_id"];
+        }
+
+        if (isset($data["user_id"])) {
+            $this->userId = $data["user_id"];
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Domain/WatchlistSave.php b/src/server/app/Domain/WatchlistSave.php
new file mode 100644
index 0000000000000000000000000000000000000000..4a631e6217e71cd6a4159df6c2b74c71fa1f5933
--- /dev/null
+++ b/src/server/app/Domain/WatchlistSave.php
@@ -0,0 +1,39 @@
+<?php
+
+require_once __DIR__ . '/../App/Domain.php';
+
+class WatchlistSave extends Domain
+{
+    public int|string $id;
+    public int $user_id;
+    public int $watchlist_id;
+
+    public function toArray(): array
+    {
+        $array = [
+            'user_id' => $this->user_id,
+            'watchlist_id' => $this->watchlist_id,
+        ];
+
+        if (isset($this->id)) {
+            $array['id'] = $this->id;
+        }
+
+        return $array;
+    }
+
+    public function fromArray(array $data)
+    {
+        if (isset($data["id"])) {
+            $this->id = $data["id"];
+        }
+
+        if (isset($data["user_id"])) {
+            $this->user_id = $data["user_id"];
+        }
+
+        if (isset($data["watchlist_id"])) {
+            $this->watchlist_id = $data["watchlist_id"];
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Domain/WatchlistTag.php b/src/server/app/Domain/WatchlistTag.php
new file mode 100644
index 0000000000000000000000000000000000000000..989ddda1e3fb28227d46de65b9b08e55c67840f2
--- /dev/null
+++ b/src/server/app/Domain/WatchlistTag.php
@@ -0,0 +1,39 @@
+<?php
+
+require_once __DIR__ . '/../App/Domain.php';
+
+class WatchlistTag extends Domain
+{
+    public int|string $id;
+    public int $tagId;
+    public int $watchlistId;
+
+    public function toArray(): array
+    {
+        $array = [
+            "tag_id" => $this->tagId,
+            "watchlist_id" => $this->watchlistId
+        ];
+
+        if (isset($this->id)) {
+            $array["id"] = $this->id;
+        }
+
+        return $array;
+    }
+
+    public function fromArray(array $data)
+    {
+        if (isset($data["id"])) {
+            $this->id = $data["id"];
+        }
+
+        if (isset($data["tag_id"])) {
+            $this->tagId = $data["tag_id"];
+        }
+
+        if (isset($data["watchlist_id"])) {
+            $this->watchlistId = $data["watchlist_id"];
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Exception/FileUploaderException.php b/src/server/app/Exception/FileUploaderException.php
new file mode 100644
index 0000000000000000000000000000000000000000..efe6106f0dc538ff63d643f509143d88fd545c43
--- /dev/null
+++ b/src/server/app/Exception/FileUploaderException.php
@@ -0,0 +1,6 @@
+<?php
+
+class FileUploaderException extends \Exception
+{
+
+}
\ No newline at end of file
diff --git a/src/server/app/Exception/ValidationException.php b/src/server/app/Exception/ValidationException.php
new file mode 100644
index 0000000000000000000000000000000000000000..94f0c43539b3f2327b041a5a980bef130da92745
--- /dev/null
+++ b/src/server/app/Exception/ValidationException.php
@@ -0,0 +1,13 @@
+<?php
+
+class ValidationException extends \Exception
+{
+    public function __construct(
+        $message,
+        int|null $code = 0,
+        Throwable|null $previous = null
+    ) {
+        parent::__construct($message, $code, $previous);
+        $this->code = 400;
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Middleware/AdminAuthApiMiddleware.php b/src/server/app/Middleware/AdminAuthApiMiddleware.php
new file mode 100644
index 0000000000000000000000000000000000000000..0bd10075be79260671335a66b1302f3f53da61f6
--- /dev/null
+++ b/src/server/app/Middleware/AdminAuthApiMiddleware.php
@@ -0,0 +1,37 @@
+<?php
+
+require_once __DIR__ . '/../App/Middleware.php';
+require_once __DIR__ . '/../Service/SessionService.php';
+require_once __DIR__ . '/../Config/Database.php';
+
+require_once __DIR__ . '/../Repository/UserRepository.php';
+require_once __DIR__ . '/../Repository/SessionRepository.php';
+
+class AdminAuthApiMiddleware implements Middleware
+{
+    private SessionService $sessionService;
+
+    public function __construct()
+    {
+        $connection = Database::getConnection();
+
+        $userRepository = new UserRepository($connection);
+        $sessionRepository = new SessionRepository($connection);
+        $this->sessionService = new SessionService($sessionRepository, $userRepository);
+    }
+
+    public function run(): void
+    {
+        $user = $this->sessionService->current();
+
+        if ($user == null || $user->role != "ADMIN") {
+            http_response_code(401);
+            $array = [
+                "status" => 401,
+                "message" => "Unauthorized",
+            ];
+            echo json_encode($array);
+            exit();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Middleware/AdminAuthMiddleware.php b/src/server/app/Middleware/AdminAuthMiddleware.php
new file mode 100644
index 0000000000000000000000000000000000000000..656410d7ccbdc0643ddb67a63bb0897dd6d6a28d
--- /dev/null
+++ b/src/server/app/Middleware/AdminAuthMiddleware.php
@@ -0,0 +1,31 @@
+<?php
+
+require_once __DIR__ . '/../App/Middleware.php';
+
+require_once __DIR__ . '/../Service/SessionService.php';
+
+require_once __DIR__ . '/../Repository/UserRepository.php';
+require_once __DIR__ . '/../Repository/SessionRepository.php';
+
+class AdminAuthMiddleware implements Middleware
+{
+    private SessionService $sessionService;
+
+    public function __construct()
+    {
+        $connection = Database::getConnection();
+
+        $userRepository = new UserRepository($connection);
+        $sessionRepository = new SessionRepository($connection);
+        $this->sessionService = new SessionService($sessionRepository, $userRepository);
+    }
+
+    public function run(): void
+    {
+        $user = $this->sessionService->current();
+        if ($user == null || $user->role != "ADMIN") {
+            header("Location: /signin");
+            exit();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Middleware/UserAuthApiMiddleware.php b/src/server/app/Middleware/UserAuthApiMiddleware.php
new file mode 100644
index 0000000000000000000000000000000000000000..b809c9412fba7ebdcc43b5c9476b7aa767808fb5
--- /dev/null
+++ b/src/server/app/Middleware/UserAuthApiMiddleware.php
@@ -0,0 +1,37 @@
+<?php
+
+require_once __DIR__ . '/../App/Middleware.php';
+require_once __DIR__ . '/../Service/SessionService.php';
+require_once __DIR__ . '/../Config/Database.php';
+
+require_once __DIR__ . '/../Repository/UserRepository.php';
+require_once __DIR__ . '/../Repository/SessionRepository.php';
+
+class UserAuthApiMiddleware
+{
+    private SessionService $sessionService;
+
+    public function __construct()
+    {
+        $connection = Database::getConnection();
+
+        $userRepository = new UserRepository($connection);
+        $sessionRepository = new SessionRepository($connection);
+        $this->sessionService = new SessionService($sessionRepository, $userRepository);
+    }
+
+    public function run(): void
+    {
+        $user = $this->sessionService->current();
+
+        if ($user == null) {
+            http_response_code(401);
+            $array = [
+                "status" => 401,
+                "message" => "Unauthorized",
+            ];
+            echo json_encode($array);
+            exit();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Middleware/UserAuthMiddleware.php b/src/server/app/Middleware/UserAuthMiddleware.php
new file mode 100644
index 0000000000000000000000000000000000000000..5159fa72cd300ff57269b2e9be8dfcdf1010e0bc
--- /dev/null
+++ b/src/server/app/Middleware/UserAuthMiddleware.php
@@ -0,0 +1,31 @@
+<?php
+
+require_once __DIR__ . '/../App/Middleware.php';
+require_once __DIR__ . '/../Service/SessionService.php';
+require_once __DIR__ . '/../Config/Database.php';
+
+require_once __DIR__ . '/../Repository/UserRepository.php';
+require_once __DIR__ . '/../Repository/SessionRepository.php';
+
+class UserAuthMiddleware implements Middleware
+{
+    private SessionService $sessionService;
+
+    public function __construct()
+    {
+        $connection = Database::getConnection();
+
+        $userRepository = new UserRepository($connection);
+        $sessionRepository = new SessionRepository($connection);
+        $this->sessionService = new SessionService($sessionRepository, $userRepository);
+    }
+
+    public function run(): void
+    {
+        $user = $this->sessionService->current();
+        if ($user == null) {
+            header("Location: /signin");
+            exit();
+        }
+    }
+}
diff --git a/src/server/app/Model/CatalogCreateRequest.php b/src/server/app/Model/CatalogCreateRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..13ccdaaa0c8144b8a6bbf65bf64d56bee001b159
--- /dev/null
+++ b/src/server/app/Model/CatalogCreateRequest.php
@@ -0,0 +1,10 @@
+<?php
+
+class CatalogCreateRequest
+{
+    public string $title;
+    public string $description;
+    public $poster = null;
+    public $trailer = null;
+    public string $category;
+}
\ No newline at end of file
diff --git a/src/server/app/Model/CatalogCreateResponse.php b/src/server/app/Model/CatalogCreateResponse.php
new file mode 100644
index 0000000000000000000000000000000000000000..1752af5a9e52b5eb30ec5ecb0bc6583496deda25
--- /dev/null
+++ b/src/server/app/Model/CatalogCreateResponse.php
@@ -0,0 +1,8 @@
+<?php
+
+require_once __DIR__ . '/../Domain/Catalog.php';
+
+class CatalogCreateResponse
+{
+    public Catalog $catalog;
+}
\ No newline at end of file
diff --git a/src/server/app/Model/CatalogSearchRequest.php b/src/server/app/Model/CatalogSearchRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..15bb2b8e2e91ac340e8f2fdd8b3643b4557b8d85
--- /dev/null
+++ b/src/server/app/Model/CatalogSearchRequest.php
@@ -0,0 +1,8 @@
+<?php
+
+class CatalogSearchRequest
+{
+    public ?string $title;
+    public ?int $page;
+    public ?int $pageSize;
+}
diff --git a/src/server/app/Model/CatalogSearchResponse.php b/src/server/app/Model/CatalogSearchResponse.php
new file mode 100644
index 0000000000000000000000000000000000000000..19492b3480ee8d2de68cb0e71dbaaf77da789df8
--- /dev/null
+++ b/src/server/app/Model/CatalogSearchResponse.php
@@ -0,0 +1,8 @@
+<?php
+
+require_once __DIR__ . '/../Domain/Catalog.php';
+
+class CatalogSearchResponse
+{
+    public array $catalogs;
+}
diff --git a/src/server/app/Model/UserEditRequest.php b/src/server/app/Model/UserEditRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..fd5c7afc46a4b5c55e248f7f9ffb9d81689de3fc
--- /dev/null
+++ b/src/server/app/Model/UserEditRequest.php
@@ -0,0 +1,9 @@
+<?php
+
+class UserEditRequest
+{
+    public ?string $email = null;
+    public ?string $name = null;
+    public ?string $oldPassword = null;
+    public ?string $newPassword = null;
+}
diff --git a/src/server/app/Model/UserEditResponse.php b/src/server/app/Model/UserEditResponse.php
new file mode 100644
index 0000000000000000000000000000000000000000..f7441cbdeeb4326fe1151c7871e92768cbdc0c6a
--- /dev/null
+++ b/src/server/app/Model/UserEditResponse.php
@@ -0,0 +1,8 @@
+<?php
+
+require_once __DIR__ . '/../Domain/User.php';
+
+class UserEditResponse
+{
+    public User $user;
+}
diff --git a/src/server/app/Model/UserSignInRequest.php b/src/server/app/Model/UserSignInRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a1098ddd5275d1426724c36933e74ac3b2ea81e2
--- /dev/null
+++ b/src/server/app/Model/UserSignInRequest.php
@@ -0,0 +1,7 @@
+<?php
+
+class UserSignInRequest
+{
+    public ?string $email = null;
+    public ?string $password = null;
+}
diff --git a/src/server/app/Model/UserSignInResponse.php b/src/server/app/Model/UserSignInResponse.php
new file mode 100644
index 0000000000000000000000000000000000000000..145017eb6b50600d93dcd41ae8255adbf8a8ea8c
--- /dev/null
+++ b/src/server/app/Model/UserSignInResponse.php
@@ -0,0 +1,8 @@
+<?php
+
+require_once __DIR__ . '/../Domain/User.php';
+
+class UserSignInResponse
+{
+    public User $user;
+}
diff --git a/src/server/app/Model/UserSignUpRequest.php b/src/server/app/Model/UserSignUpRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1aae8f46164d2ac211c125ca66115be241538a62
--- /dev/null
+++ b/src/server/app/Model/UserSignUpRequest.php
@@ -0,0 +1,8 @@
+<?php
+
+class UserSignUpRequest
+{
+    public ?string $email = null;
+    public ?string $password = null;
+    public ?string $confirmPassword = null;
+}
diff --git a/src/server/app/Model/UserSignUpResponse.php b/src/server/app/Model/UserSignUpResponse.php
new file mode 100644
index 0000000000000000000000000000000000000000..443eb03643076dfc2ad352332b4eb834bb9ce755
--- /dev/null
+++ b/src/server/app/Model/UserSignUpResponse.php
@@ -0,0 +1,8 @@
+<?php
+
+require_once __DIR__ . '/../Domain/User.php';
+
+class UserSignUpResponse
+{
+    public User $user;
+}
diff --git a/src/server/app/Model/WatchlistAddItemRequest.php b/src/server/app/Model/WatchlistAddItemRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..0bd24ad714cbc2cfd3182eb47796b7a515032a49
--- /dev/null
+++ b/src/server/app/Model/WatchlistAddItemRequest.php
@@ -0,0 +1,6 @@
+<?php
+
+class WatchlistAddItemRequest
+{
+    public string $id;
+}
diff --git a/src/server/app/Model/WatchlistAddItemResponse.php b/src/server/app/Model/WatchlistAddItemResponse.php
new file mode 100644
index 0000000000000000000000000000000000000000..aed6c8d744471d6e236fe74db823c2725f9723cf
--- /dev/null
+++ b/src/server/app/Model/WatchlistAddItemResponse.php
@@ -0,0 +1,6 @@
+<?php
+
+class WatchlistAdditemResponse
+{
+    public ?Catalog $catalog;
+}
diff --git a/src/server/app/Model/WatchlistCreateRequest.php b/src/server/app/Model/WatchlistCreateRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..66e93e891de398931dec9805c6f7427ee3aacb03
--- /dev/null
+++ b/src/server/app/Model/WatchlistCreateRequest.php
@@ -0,0 +1,12 @@
+<?php
+
+class WatchlistCreateRequest
+{
+    public ?int $userId;
+    public ?string $title;
+    public ?string $description;
+    public ?string $visibility;
+    public ?array $items;
+    public ?array $tags;
+    public ?array $initialTags;
+}
diff --git a/src/server/app/Model/WatchlistsGetRequest.php b/src/server/app/Model/WatchlistsGetRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..47471033e758e583a56fbf2af3a8316c9bb77023
--- /dev/null
+++ b/src/server/app/Model/WatchlistsGetRequest.php
@@ -0,0 +1,13 @@
+<?php
+
+class WatchlistsGetRequest
+{
+    public ?int $userId;
+    public ?string $search;
+    public ?string $category;
+    public ?string $tag;
+    public ?array $tagsInit;
+    public ?string $sortBy;
+    public ?string $order;
+    public ?int $page;
+}
diff --git a/src/server/app/Model/bookmark/BookmarkGetRequest.php b/src/server/app/Model/bookmark/BookmarkGetRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..971d81536ca19d4216a6fe01d869c3cc31370aad
--- /dev/null
+++ b/src/server/app/Model/bookmark/BookmarkGetRequest.php
@@ -0,0 +1,8 @@
+<?php
+
+class BookmarkGetRequest
+{
+    public int $userId;
+    public int $page = 1;
+    public int $pageSize = 10;
+}
\ No newline at end of file
diff --git a/src/server/app/Model/catalog/CatalogUpdateRequest.php b/src/server/app/Model/catalog/CatalogUpdateRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..2f07d18d5988950bfca1742ce1d20eb4ad253b92
--- /dev/null
+++ b/src/server/app/Model/catalog/CatalogUpdateRequest.php
@@ -0,0 +1,11 @@
+<?php
+
+class CatalogUpdateRequest
+{
+    public string $uuid;
+    public string $title;
+    public string $description;
+    public $poster = null;
+    public $trailer = null;
+    public string $category;
+}
\ No newline at end of file
diff --git a/src/server/app/Model/session/SessionCreateRequest.php b/src/server/app/Model/session/SessionCreateRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..11f16689ce28ddd6e48d6d33eeed05b40c13a2b5
--- /dev/null
+++ b/src/server/app/Model/session/SessionCreateRequest.php
@@ -0,0 +1,6 @@
+<?php
+
+class SessionCreateRequest
+{
+    public int $userId;
+}
\ No newline at end of file
diff --git a/src/server/app/Model/watchlist/WatchlistDeleteRequest.php b/src/server/app/Model/watchlist/WatchlistDeleteRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..723cbf52bf9198c5097c42a4165f50013be89b73
--- /dev/null
+++ b/src/server/app/Model/watchlist/WatchlistDeleteRequest.php
@@ -0,0 +1,6 @@
+<?php
+
+class WatchlistDeleteRequest
+{
+    public ?string $watchlistUUID;
+}
\ No newline at end of file
diff --git a/src/server/app/Model/watchlist/WatchlistEditRequest.php b/src/server/app/Model/watchlist/WatchlistEditRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..d5caaeddc2fc321a6716087a800d84955f05d52e
--- /dev/null
+++ b/src/server/app/Model/watchlist/WatchlistEditRequest.php
@@ -0,0 +1,13 @@
+<?php
+
+class WatchlistEditRequest
+{
+    public $watchlist;
+    public ?int $userId;
+    public ?string $title;
+    public ?string $description;
+    public ?string $visibility;
+    public ?array $items;
+    public ?array $tags;
+    public ?array $initialTags;
+}
\ No newline at end of file
diff --git a/src/server/app/Model/watchlist/WatchlistGetOneByUserRequest.php b/src/server/app/Model/watchlist/WatchlistGetOneByUserRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..908494ee4f54842871813c499948d383d8a8a69d
--- /dev/null
+++ b/src/server/app/Model/watchlist/WatchlistGetOneByUserRequest.php
@@ -0,0 +1,7 @@
+<?php
+
+class WatchlistGetOneByUserRequest
+{
+    public int $userId;
+    public ?string $visibility;
+}
\ No newline at end of file
diff --git a/src/server/app/Model/watchlist/WatchlistGetOneRequest.php b/src/server/app/Model/watchlist/WatchlistGetOneRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..15e04b8876d921294b5069915ea59fd9bddb695f
--- /dev/null
+++ b/src/server/app/Model/watchlist/WatchlistGetOneRequest.php
@@ -0,0 +1,9 @@
+<?php
+
+class WatchlistsGetOneRequest
+{
+    public ?int $userId = null;
+    public string $uuid;
+    public int $page = 1;
+    public int $pageSize = 10;
+}
\ No newline at end of file
diff --git a/src/server/app/Model/watchlist/WatchlistLikeRequest.php b/src/server/app/Model/watchlist/WatchlistLikeRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..d254c97b84224860c1299d8f01331a3fe9351fab
--- /dev/null
+++ b/src/server/app/Model/watchlist/WatchlistLikeRequest.php
@@ -0,0 +1,7 @@
+<?php
+
+class WatchlistLikeRequest
+{
+    public ?string $watchlistUUID;
+    public ?string $userId;
+}
\ No newline at end of file
diff --git a/src/server/app/Model/watchlist/WatchlistSaveRequest.php b/src/server/app/Model/watchlist/WatchlistSaveRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ab14182a5796256b542c489bcf3317efa2451fda
--- /dev/null
+++ b/src/server/app/Model/watchlist/WatchlistSaveRequest.php
@@ -0,0 +1,7 @@
+<?php
+
+class WatchlistSaveRequest
+{
+    public ?string $watchlistUUID;
+    public ?string $userId;
+}
\ No newline at end of file
diff --git a/src/server/app/Repository/CatalogRepository.php b/src/server/app/Repository/CatalogRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..340bac22f5485d53d686957307fa20618e35825d
--- /dev/null
+++ b/src/server/app/Repository/CatalogRepository.php
@@ -0,0 +1,43 @@
+<?php
+
+require_once __DIR__ . '/../App/Repository.php';
+require_once __DIR__ . '/../Domain/Catalog.php';
+require_once __DIR__ . '/../Utils/FilterBuilder.php';
+
+class CatalogRepository extends Repository
+{
+    protected string $table = 'catalogs';
+
+    public function __construct(PDO $connection)
+    {
+        parent::__construct($connection);
+    }
+
+    public function findAll(array $projection = [], int|null $page = null, int|null $pageSize = null): array
+    {
+        $result = parent::findAll($projection, $page, $pageSize);
+
+        $result['items'] = array_map(
+            function ($row) {
+                $catalog = new Catalog();
+                $catalog->fromArray($row);
+                return $catalog;
+            },
+            $result['items']
+        );
+        return $result;
+    }
+
+    public function findOne($key, $value, $projection = []): ?Catalog
+    {
+        $result = parent::findOne($key, $value, $projection);
+
+        if ($result) {
+            $catalog = new Catalog();
+            $catalog->fromArray($result);
+            return $catalog;
+        } else {
+            return null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Repository/SessionRepository.php b/src/server/app/Repository/SessionRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..8fe54ac8b3dd657cf3db57f0292056f448d0d31f
--- /dev/null
+++ b/src/server/app/Repository/SessionRepository.php
@@ -0,0 +1,29 @@
+<?php
+require_once __DIR__ . '/../App/Repository.php';
+require_once __DIR__ . '/../App/Domain.php';
+
+require_once __DIR__ . '/../Domain/Session.php';
+
+class SessionRepository extends Repository
+{
+    protected string $table = "sessions";
+
+    public function __construct(PDO $connection)
+    {
+        parent::__construct($connection);
+    }
+
+    public function findOne($key, $value, $projection = []): ?Session
+    {
+        $result = parent::findOne($key, $value, $projection);
+
+        if ($result != null) {
+            $session = new Session();
+            $session->fromArray($result);
+
+            return $session;
+        } else {
+            return null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Repository/TagRepository.php b/src/server/app/Repository/TagRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..b2be3393966503e918e8664e1690eb3e1628867f
--- /dev/null
+++ b/src/server/app/Repository/TagRepository.php
@@ -0,0 +1,44 @@
+<?php
+
+require_once __DIR__ . '/../App/Repository.php';
+
+require_once __DIR__ . '/../Domain/Tag.php';
+
+class TagRepository extends Repository
+{
+    protected string $table = "tags";
+
+    public function __construct(PDO $connection)
+    {
+        parent::__construct($connection);
+    }
+
+    public function findAll(array $projection = [], int|null $page = null, int|null $pageSize = null): array
+    {
+        $result = parent::findAll($projection, $page, $pageSize);
+
+        $result['items'] = array_map(
+            function ($row) {
+                $tags = new Tag();
+                $tags->fromArray($row);
+                return $tags;
+            },
+            $result['items']
+        );
+        return $result;
+    }
+
+    public function findOne($key, $value, $projection = [])
+    {
+        $result = parent::findOne($key, $value, $projection);
+
+        if ($result != null) {
+            $user = new User();
+            $user->fromArray($result);
+
+            return $user;
+        } else {
+            return null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Repository/UserRepository.php b/src/server/app/Repository/UserRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..a29dc04a6e938c8eb7dd407a9a17860d6088284e
--- /dev/null
+++ b/src/server/app/Repository/UserRepository.php
@@ -0,0 +1,59 @@
+<?php
+
+require_once __DIR__ . '/../App/Repository.php';
+require_once __DIR__ . '/../Utils/UUIDGenerator.php';
+
+require_once __DIR__ . '/../Domain/User.php';
+
+class UserRepository extends Repository
+{
+    protected string $table = "users";
+
+    public function __construct(PDO $connection)
+    {
+        parent::__construct($connection);
+    }
+
+    public function findOne($key, $value, $projection = []): ?User
+    {
+        $result = parent::findOne($key, $value, $projection);
+
+        if ($result != null) {
+            $user = new User();
+            $user->fromArray($result);
+
+            return $user;
+        } else {
+            return null;
+        }
+    }
+
+    public function updateName(User $user): User
+    {
+        $statement = $this->connection->prepare("UPDATE users SET name = ? WHERE email = ?");
+        $statement->execute([
+            $user->name,
+            $user->email,
+        ]);
+        return $user;
+    }
+
+    public function updatePassword(User $user): User
+    {
+        $statement = $this->connection->prepare("UPDATE users SET password = ? WHERE email = ?");
+        $statement->execute([
+            $user->password,
+            $user->email,
+        ]);
+        return $user;
+    }
+
+
+    public function deleteBySession(string $email)
+    {
+        $statement = $this->connection->prepare("DELETE FROM sessions WHERE user_id IN
+        (SELECT id FROM users WHERE email = ?)");
+        $statement->execute([$email]);
+        $statement->closeCursor();
+    }
+}
diff --git a/src/server/app/Repository/WatchlistItemRepository.php b/src/server/app/Repository/WatchlistItemRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..f1751b23c67e9bbdc1ee7e00857a4b65791e9795
--- /dev/null
+++ b/src/server/app/Repository/WatchlistItemRepository.php
@@ -0,0 +1,22 @@
+<?php
+
+class WatchlistItemRepository extends Repository
+{
+    protected string $table = 'watchlist_items';
+
+    public function __construct(\PDO $connection, )
+    {
+        parent::__construct($connection);
+    }
+
+    public function findAll(array $projection = [], int|null $page = 1, int|null $pageSize = 10): array
+    {
+        // TO DO: Implemented soon
+        return [];
+    }
+
+    public function findOne($key, $value, $projection = [])
+    {
+        // TO DO: Implemented soon
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Repository/WatchlistLikeRepository.php b/src/server/app/Repository/WatchlistLikeRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..fab36084bc8a1d82b7808de292401f2abca292dc
--- /dev/null
+++ b/src/server/app/Repository/WatchlistLikeRepository.php
@@ -0,0 +1,50 @@
+<?php
+
+require_once __DIR__ . '/../App/Repository.php';
+
+class WatchlistLikeRepository extends Repository
+{
+    protected string $table = "watchlist_like";
+
+    public function __construct(PDO $connection)
+    {
+        parent::__construct($connection);
+    }
+
+    public function saveByWatchlistAndUser(string $watchlistId, string $userId)
+    {
+        $statement = $this->connection->prepare("INSERT INTO $this->table (watchlist_id, user_id) VALUES (:watchlist_id, :user_id)");
+        $statement->bindValue(":watchlist_id", $watchlistId);
+        $statement->bindValue(":user_id", $userId);
+        $statement->execute();
+
+        $statement->closeCursor();
+    }
+
+    public function findOneByWatchlistAndUser(string $watchlistId, string $userId)
+    {
+        $statement = $this->connection->prepare("SELECT * FROM $this->table WHERE watchlist_id = :watchlist_id AND user_id = :user_id");
+        $statement->bindValue(":watchlist_id", $watchlistId);
+        $statement->bindValue(":user_id", $userId);
+        $statement->execute();
+
+        try {
+            if ($row = $statement->fetch()) {
+                return $row;
+            }
+            return null;
+        } finally {
+            $statement->closeCursor();
+        }
+    }
+
+    public function deleteByWatchlistAndUser(string $watchlistId, string $userId)
+    {
+        $statement = $this->connection->prepare("DELETE FROM $this->table WHERE watchlist_id = :watchlist_id AND user_id = :user_id");
+        $statement->bindValue(":watchlist_id", $watchlistId);
+        $statement->bindValue(":user_id", $userId);
+        $statement->execute();
+
+        $statement->closeCursor();
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Repository/WatchlistRepository.php b/src/server/app/Repository/WatchlistRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..b64699cd0a55ef6e1c821f3e404d97e333028f3f
--- /dev/null
+++ b/src/server/app/Repository/WatchlistRepository.php
@@ -0,0 +1,378 @@
+<?php
+
+require_once __DIR__ . '/../App/Repository.php';
+require_once __DIR__ . '/../Domain/Watchlist.php';
+
+class WatchlistRepository extends Repository
+{
+    protected string $table = 'watchlists';
+
+    public function __construct(\PDO $connection)
+    {
+        parent::__construct($connection);
+    }
+
+    public function findAll(array $projection = [], int|null $page = null, int|null $pageSize = null): array
+    {
+        $result = parent::findAll($projection, $page, $pageSize);
+
+        $result['items'] = array_map(
+            function ($row) {
+                $watchlist = new Watchlist();
+                $watchlist->fromArray($row);
+
+                return $watchlist;
+            },
+            $result['items']
+        );
+
+        return $result;
+    }
+
+    public function findOne($key, $value, $projection = []): ?Watchlist
+    {
+        $result = parent::findOne($key, $value, $projection);
+
+        if ($result) {
+            $watchlist = new Watchlist();
+            $watchlist->fromArray($result);
+            return $watchlist;
+        } else {
+            return null;
+        }
+    }
+
+    public function findAllCustom(string $userId, string $search, string $category, string $sortBy, string $order, string $tag, int $page = 1, int $pageSize = 10)
+    {
+        if ($category != "")
+            $category = " AND category = '$category'";
+
+        // Queries
+        $selectFirstQuery = "
+            WITH first_agg AS (
+            SELECT w.id AS watchlist_id, jsonb_agg(jsonb_build_object(
+                'rank', rank,
+                'poster', poster,
+                'catalog_uuid', c.uuid
+                )) AS posters, w.uuid AS watchlist_uuid, u.name AS creator, u.uuid AS creator_uuid, item_count, loved, saved, w.title, w.description, w.category, visibility, love_count, w.created_at AS created_at    
+            ";
+        $selectSecondQuery = "
+            )
+            SELECT 
+                jsonb_agg(jsonb_build_object(
+                'id', t.id,
+                'name', t.name
+                )) AS tags, w.watchlist_id, w.posters as posters, w.watchlist_uuid, w.creator, w.creator_uuid, w.item_count, w.loved, w.saved, w.title, w.description, w.category, w.visibility, w.love_count, w.created_at
+            FROM first_agg AS w
+            LEFT JOIN watchlist_tag as wt ON wt.watchlist_id = w.watchlist_id
+            LEFT JOIN tags as t ON t.id = wt.tag_id
+            GROUP BY
+                w.watchlist_id, w.watchlist_uuid, w.posters, w.creator, w.creator_uuid, w.item_count, w.loved, w.saved, w.title, w.description, w.category, w.visibility, w.love_count, w.created_at
+            ORDER BY
+                $sortBy $order,
+                w.created_at DESC
+        ";
+        $countQuery = "WITH rows AS (SELECT COUNT(*)";
+        $mainQuery = "
+            FROM (
+                SELECT
+                    id, uuid, title, description, category, visibility, like_count AS love_count, item_count, user_id, created_at,
+                    CASE
+                        WHEN id IN (
+                            SELECT watchlist_id
+                            FROM watchlist_like
+                            WHERE user_id = :user_id
+                        ) THEN TRUE
+                        ELSE FALSE
+                    END AS loved,
+                    CASE
+                        WHEN id IN (
+                            SELECT watchlist_id
+                            FROM watchlist_save
+                            WHERE user_id = :user_id
+                        ) THEN TRUE
+                        ELSE FALSE
+                    END AS saved
+                FROM
+                    watchlists w
+                WHERE
+                    (w.title ILIKE :watchlist_title
+                    AND visibility = 'PUBLIC'
+                    $category)
+                    OR
+                    (visibility = 'PUBLIC' 
+                    $category)
+                ORDER BY
+                    $sortBy $order,
+                    created_at DESC
+                ) AS w JOIN users AS u ON w.user_id = u.id
+                JOIN (SELECT * FROM watchlist_items WHERE rank < 5) AS wi ON wi.watchlist_id = w.id
+                JOIN catalogs AS c ON c.id = wi.catalog_id
+            WHERE
+                u.name ILIKE :creator
+                OR w.title ILIKE :watchlist_title
+            GROUP BY
+                w.id, w.uuid, u.name, w.title, u.name, u.uuid, item_count, loved, saved, w.description, w.category, visibility, love_count, w.created_at
+            ORDER BY
+                $sortBy $order,
+                w.created_at DESC
+            ";
+
+        if ($tag) {
+            $selectStatement = $this->connection->prepare("WITH outer_query AS (" .
+                $selectFirstQuery . $mainQuery . $selectSecondQuery .
+                ") SELECT * FROM outer_query as o
+                   WHERE EXISTS 
+                   (SELECT 1 
+                    FROM jsonb_array_elements(o.tags) 
+                    AS elem WHERE elem @> '{\"name\": \"$tag\"}'::jsonb)
+                    LIMIT :limit
+                    OFFSET :offset
+                    ");
+            $pageCountStatement = $this->connection->prepare("WITH outer_query AS (" .
+                $selectFirstQuery . $mainQuery . $selectSecondQuery .
+                ") SELECT COUNT(*) FROM outer_query as o
+                   WHERE EXISTS 
+                   (SELECT 1 
+                    FROM jsonb_array_elements(o.tags) 
+                    AS elem WHERE elem @> '{\"name\": \"$tag\"}'::jsonb)
+                    LIMIT :limit
+                    OFFSET :offset
+                    ");
+        } else {
+            $selectStatement = $this->connection->prepare($selectFirstQuery . $mainQuery . $selectSecondQuery . "                     LIMIT :limit
+                    OFFSET :offset");
+            $pageCountStatement = $this->connection->prepare($countQuery . $mainQuery . ") SELECT COUNT(*) FROM rows");
+        }
+
+
+        // Binding select
+        $selectStatement->bindValue(":user_id", $userId);
+        $selectStatement->bindValue(":watchlist_title", '%' . $search . '%');
+        $selectStatement->bindValue(":limit", $pageSize);
+        $selectStatement->bindValue(":offset", ($page - 1) * $pageSize);
+        $selectStatement->bindValue(":creator", '%' . $search . '%');
+
+        // Binding count
+        $pageCountStatement->bindValue(":user_id", $userId);
+        $pageCountStatement->bindValue(":watchlist_title", '%' . $search . '%');
+        if ($tag) {
+            $pageCountStatement->bindValue(":limit", PHP_INT_MAX);
+            $pageCountStatement->bindValue(":offset", 0);
+        }
+        $pageCountStatement->bindValue(":creator", '%' . $search . '%');
+
+        $selectStatement->execute();
+        $pageCountStatement->execute();
+
+        try {
+            return [
+                "items" => $selectStatement->fetchAll(),
+                "page" => $page,
+                "pageTotal" => ceil($pageCountStatement->fetchColumn() / $pageSize)
+            ];
+        } finally {
+            $selectStatement->closeCursor();
+            $pageCountStatement->closeCursor();
+        }
+    }
+
+    public function findByUser(int $userId, string|null $visibility, int $page = null, int $pageSize = null)
+    {
+        $query = "
+        FROM (
+            SELECT
+                id, uuid, title, description, category, visibility, like_count, item_count, user_id, updated_at, created_at,
+                CASE
+                    WHEN id IN (
+                        SELECT watchlist_id
+                        FROM watchlist_like
+                        WHERE user_id = :user_id
+                    ) THEN TRUE
+                    ELSE FALSE
+                END AS liked
+            FROM
+                watchlists
+            WHERE
+                user_id = :user_id " . (!empty($visibility) ? "AND visibility = :visibility" : "") . "
+                LIMIT :limit
+                OFFSET :offset
+            ) AS w JOIN users AS u ON w.user_id = u.id
+            JOIN (SELECT * FROM watchlist_items WHERE rank < 5) AS wi ON wi.watchlist_id = w.id
+            JOIN catalogs AS c ON c.id = wi.catalog_id
+        ";
+        $selectFirstQuery = "
+        WITH first_agg AS (
+        SELECT w.id AS watchlist_id, jsonb_agg(jsonb_build_object(
+            'rank', rank,
+            'poster', poster,
+            'catalog_uuid', c.uuid
+            )) AS posters, w.uuid AS watchlist_uuid, name AS creator, u.uuid AS creator_uuid, item_count, liked, w.title, w.description, w.category, visibility, like_count, w.updated_at AS updated_at, w.created_at AS created_at";
+        $selectSecondQuery = "
+            )
+            SELECT 
+                jsonb_agg(jsonb_build_object(
+                'id', t.id,
+                'name', t.name
+                )) AS tags, fa.watchlist_id, fa.posters as posters, fa.watchlist_uuid, fa.creator, fa.creator_uuid, fa.item_count, fa.liked, fa.title, fa.description, fa.category, fa.visibility, fa.like_count, fa.created_at
+            FROM first_agg AS fa
+            LEFT JOIN watchlist_tag as wt ON wt.watchlist_id = fa.watchlist_id
+            LEFT JOIN tags as t ON t.id = wt.tag_id
+            GROUP BY
+                fa.watchlist_id, fa.watchlist_uuid, fa.posters, fa.creator, fa.creator_uuid, fa.item_count, fa.liked, fa.title, fa.description, fa.category, fa.visibility, fa.like_count, fa.created_at
+            ORDER BY
+                fa.created_at DESC
+        ";
+        $pageCountQuery = "SELECT COUNT(*) FROM (SELECT DISTINCT w.id ";
+
+        $selectStatement = $this->connection->prepare($selectFirstQuery . $query . "GROUP BY
+        watchlist_id, watchlist_uuid, creator, u.uuid, w.title, w.uuid, u.name, item_count, liked, w.id, w.description, w.category, visibility, like_count, w.updated_at, w.created_at" . $selectSecondQuery);
+        $selectStatement->bindValue(':user_id', $userId, PDO::PARAM_INT);
+        $selectStatement->bindValue(':limit', $pageSize, PDO::PARAM_INT);
+        $offset = ($page - 1) * $pageSize;
+        $selectStatement->bindValue(':offset', $offset, PDO::PARAM_INT);
+
+        $pageCountStatement = $this->connection->prepare($pageCountQuery . $query . ") AS w");
+        $pageCountStatement->bindValue(':user_id', $userId, PDO::PARAM_INT);
+        $pageCountStatement->bindValue(':limit', PHP_INT_MAX, PDO::PARAM_INT);
+        $pageCountStatement->bindValue(':offset', 0, PDO::PARAM_INT);
+        if (!empty($visibility)) {
+            $selectStatement->bindValue(':visibility', $visibility, PDO::PARAM_STR);
+            $pageCountStatement->bindValue(':visibility', $visibility, PDO::PARAM_STR);
+        }
+
+        $selectStatement->execute();
+        $pageCountStatement->execute();
+
+        try {
+            $rows = $selectStatement->fetchAll();
+            return [
+                'items' => $rows,
+                'page' => max(1, $page),
+                'totalPage' => $pageSize > 0 ? ceil($pageCountStatement->fetchColumn() / $pageSize) : 1
+            ];
+        } finally {
+            $selectStatement->closeCursor();
+            $pageCountStatement->closeCursor();
+        }
+    }
+
+    public function findByUUID(string $uuid, int|null $user_id, int $page = 1, int $pageSize = 10)
+    {
+        $selectQuery = "
+        WITH first_agg AS (
+        WITH w AS (
+            SELECT
+                id, uuid, title, description, category, visibility, like_count, item_count, user_id, created_at
+                ,CASE
+                    WHEN id IN (
+                        SELECT watchlist_id
+                        FROM watchlist_like
+                        WHERE user_id = :user_id
+                    ) THEN TRUE
+                    ELSE FALSE
+                END AS liked,
+                CASE
+                    WHEN id IN (
+                        SELECT watchlist_id
+                        FROM watchlist_save
+                        WHERE user_id = :user_id
+                    ) THEN TRUE
+                    ELSE FALSE
+                END AS saved
+            FROM
+                watchlists
+            WHERE
+                watchlists.uuid = :uuid
+            LIMIT 1
+            )
+        SELECT w.id AS watchlist_id, jsonb_agg(jsonb_build_object(
+            'rank', rank,
+            'poster', poster,
+            'catalog_uuid', c.uuid,
+            'catalog_id', c.id,
+            'description', wi.description,
+            'title', c.title,
+            'category', c.category
+            )) AS catalogs, w.uuid AS watchlist_uuid, name AS creator, item_count, w.title, w.description, w.category, visibility, like_count, w.created_at, u.uuid AS creator_uuid
+            ,liked, saved
+        FROM w JOIN users AS u ON w.user_id = u.id
+            , (SELECT * FROM watchlist_items WHERE watchlist_id IN (SELECT id FROM w) ORDER BY rank LIMIT :limit OFFSET :offset) AS wi
+            JOIN catalogs AS c ON c.id = wi.catalog_id
+            GROUP BY
+            watchlist_id, watchlist_uuid, creator, w.title, w.uuid, name, item_count, w.id, w.description, w.category, visibility, like_count, w.created_at, u.uuid
+            ,liked, saved
+        ) 
+        SELECT 
+            jsonb_agg(jsonb_build_object(
+            'id', t.id,
+            'name', t.name
+            )) AS tags, fa.watchlist_id, fa.catalogs, fa.watchlist_uuid, fa.creator, fa.creator_uuid, fa.item_count, fa.liked, fa.saved, fa.title, fa.description, fa.category, fa.visibility, fa.like_count, fa.created_at
+        FROM first_agg AS fa
+        LEFT JOIN watchlist_tag as wt ON wt.watchlist_id = fa.watchlist_id
+        LEFT JOIN tags as t ON t.id = wt.tag_id
+        GROUP BY
+            fa.watchlist_id, fa.watchlist_uuid, fa.catalogs, fa.creator, fa.creator_uuid, fa.item_count, fa.liked, fa.saved, fa.title, fa.description, fa.category, fa.visibility, fa.like_count, fa.created_at
+        ORDER BY
+            fa.created_at DESC
+        ";
+
+        $pageCountQuery = "
+        SELECT COUNT(*) 
+        FROM (
+            SELECT id, user_id
+            FROM
+                watchlists
+            WHERE
+                watchlists.uuid = :uuid
+            LIMIT 1) AS w JOIN users AS u ON w.user_id = u.id
+            JOIN (SELECT * FROM watchlist_items ORDER BY rank) AS wi ON wi.watchlist_id = w.id
+            JOIN catalogs AS c ON c.id = wi.catalog_id
+        ";
+
+        $selectStatement = $this->connection->prepare($selectQuery);
+        $selectStatement->bindParam(':uuid', $uuid, PDO::PARAM_STR);
+        $selectStatement->bindParam(':limit', $pageSize, PDO::PARAM_INT);
+        $offset = ($page - 1) * $pageSize;
+        $selectStatement->bindParam(':offset', $offset, PDO::PARAM_INT);
+        $selectStatement->bindParam(':user_id', $user_id, PDO::PARAM_INT);
+
+
+        $pageCountStatement = $this->connection->prepare($pageCountQuery);
+        $pageCountStatement->bindParam(':uuid', $uuid, PDO::PARAM_STR);
+
+        $selectStatement->execute();
+        $pageCountStatement->execute();
+
+        try {
+            function catalogCompare($element1, $element2)
+            {
+                return $element1["rank"] - $element2["rank"];
+            }
+
+            $totalPage = $pageSize > 0 ? ceil($pageCountStatement->fetchColumn() / $pageSize) : 1;
+            if ($rows = $selectStatement->fetch()) {
+                $catalogs = json_decode($rows["catalogs"], true);
+                $tags = json_decode($rows["tags"], true);
+                $tags = array_filter($tags, function ($value) {
+                    return $value["id"] !== null;
+                });
+                usort($catalogs, "catalogCompare");
+                $rows["tags"] = $tags;
+
+                $rows["catalogs"] = [
+                    "items" => $catalogs,
+                    "page" => max(1, $page),
+                    'totalPage' => $totalPage
+                ];
+
+                return $rows;
+            }
+
+            return null;
+        } finally {
+            $selectStatement->closeCursor();
+            $pageCountStatement->closeCursor();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Repository/WatchlistSaveRepository.php b/src/server/app/Repository/WatchlistSaveRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..5fae6c6826ef1808ee4fbe76993b87da0c1e5d97
--- /dev/null
+++ b/src/server/app/Repository/WatchlistSaveRepository.php
@@ -0,0 +1,152 @@
+<?php
+
+class WatchlistSaveRepository extends Repository
+{
+    protected string $table = "watchlist_save";
+
+    public function __construct(PDO $connection)
+    {
+        parent::__construct($connection);
+    }
+
+    public function findByUser(string $userId, int $page, int $pageSize)
+    {
+        $selectQuery = "
+        WITH first_agg AS (
+            SELECT
+                w.id AS watchlist_id,
+                jsonb_agg(
+                    jsonb_build_object(
+                        'rank', rank,
+                        'poster', poster,
+                        'catalog_uuid', c.uuid
+                    )
+                ) AS posters,
+                w.uuid AS watchlist_uuid, name AS creator, u.uuid AS creator_uuid, item_count,
+                liked, w.title, w.description, w.category, visibility, 
+                like_count, w.created_at AS created_at, w.updated_at AS updated_at
+            FROM
+                (
+                SELECT
+                    w.id, uuid, title, description, category, 
+                    visibility, like_count, item_count, w.user_id, 
+                    created_at, updated_at, 
+                    CASE WHEN w.id IN(
+                        SELECT watchlist_id
+                        FROM watchlist_like
+                        WHERE user_id = :user_id
+                    ) THEN TRUE ELSE FALSE
+                    END AS liked
+                FROM watchlists w
+                JOIN watchlist_save wv ON
+                    w.id = wv.watchlist_id
+                WHERE wv.user_id = :user_id
+                AND w.visibility = 'PUBLIC'
+                LIMIT :limit 
+                OFFSET :offset
+            ) AS w
+            JOIN users AS u ON
+                w.user_id = u.id
+            JOIN(
+                SELECT *
+                FROM watchlist_items
+                WHERE rank < 5
+            ) AS wi
+            ON wi.watchlist_id = w.id
+            JOIN catalogs AS c
+            ON c.id = wi.catalog_id
+            GROUP BY w.id, w.uuid, creator,
+                w.title, name, u.uuid, item_count, liked, w.description,
+                w.category, visibility, like_count, w.created_at, w.updated_at
+        )
+        SELECT 
+            jsonb_agg(jsonb_build_object(
+            'id', t.id,
+            'name', t.name
+            )) AS tags, fa.watchlist_id, fa.posters as posters, fa.watchlist_uuid, fa.creator, fa.creator_uuid, fa.item_count, fa.liked, fa.title, fa.description, fa.category, fa.visibility, fa.like_count, fa.created_at
+        FROM first_agg AS fa
+        LEFT JOIN watchlist_tag as wt ON wt.watchlist_id = fa.watchlist_id
+        LEFT JOIN tags as t ON t.id = wt.tag_id
+        GROUP BY
+            fa.watchlist_id, fa.watchlist_uuid, fa.posters, fa.creator, fa.creator_uuid, fa.item_count, fa.liked, fa.title, fa.description, fa.category, fa.visibility, fa.like_count, fa.created_at
+        ORDER BY
+            fa.created_at DESC
+        ";
+
+        $pageCountQuery = "
+            SELECT COUNT(*)
+            FROM ( SELECT DISTINCT w.id
+            FROM (
+                SELECT w.id, w.user_id
+                FROM watchlists w 
+                JOIN watchlist_save wv 
+                ON w.id = wv.watchlist_id
+                WHERE wv.user_id = :user_id
+            ) AS w JOIN users AS u 
+            ON w.user_id = u.id
+            JOIN (SELECT * FROM watchlist_items WHERE rank < 5) AS wi ON wi.watchlist_id = w.id
+            JOIN catalogs AS c ON c.id = wi.catalog_id ) AS w
+        ";
+
+        $selectStatement = $this->connection->prepare($selectQuery);
+        $selectStatement->bindParam(":user_id", $userId, PDO::PARAM_INT);
+        $selectStatement->bindParam(":limit", $pageSize, PDO::PARAM_INT);
+        $selectStatement->bindValue(":offset", ($page - 1) * $pageSize, PDO::PARAM_INT);
+        $selectStatement->execute();
+
+        $pageCountStatement = $this->connection->prepare($pageCountQuery);
+        $pageCountStatement->bindParam(":user_id", $userId, PDO::PARAM_INT);
+        $pageCountStatement->execute();
+
+        try {
+            $result = $selectStatement->fetchAll(PDO::FETCH_ASSOC);
+            $pageCount = $pageCountStatement->fetchColumn();
+
+            return [
+                'items' => $result,
+                'page' => max(1, $page),
+                'totalPage' => $pageSize > 0 ? ceil($pageCount / $pageSize) : 1
+            ];
+        } finally {
+            $selectStatement->closeCursor();
+            $pageCountStatement->closeCursor();
+        }
+    }
+
+    public function saveByWatchlistAndUser(string $watchlistId, string $userId)
+    {
+        $statement = $this->connection->prepare("INSERT INTO $this->table (watchlist_id, user_id) VALUES (:watchlist_id, :user_id)");
+        $statement->bindValue(":watchlist_id", $watchlistId);
+        $statement->bindValue(":user_id", $userId);
+        $statement->execute();
+
+        $statement->closeCursor();
+    }
+
+    public function findOneByWatchlistAndUser(string $watchlistId, string $userId)
+    {
+        $statement = $this->connection->prepare("SELECT * FROM $this->table WHERE watchlist_id = :watchlist_id AND user_id = :user_id");
+        $statement->bindValue(":watchlist_id", $watchlistId);
+        $statement->bindValue(":user_id", $userId);
+        $statement->execute();
+
+        try {
+            if ($row = $statement->fetch()) {
+                return $row;
+            }
+            return null;
+        } finally {
+            $statement->closeCursor();
+        }
+    }
+
+    public function deleteByWatchlistAndUser(string $watchlistId, string $userId)
+    {
+        $statement = $this->connection->prepare("DELETE FROM $this->table WHERE watchlist_id = :watchlist_id AND user_id = :user_id");
+        $statement->bindValue(":watchlist_id", $watchlistId);
+        $statement->bindValue(":user_id", $userId);
+        $statement->execute();
+
+        $statement->closeCursor();
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Repository/WatchlistTagRepository.php b/src/server/app/Repository/WatchlistTagRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..6e30dbc467ea439fbff7961932eaae4eb4bf98fc
--- /dev/null
+++ b/src/server/app/Repository/WatchlistTagRepository.php
@@ -0,0 +1,13 @@
+<?php
+
+require_once __DIR__ . '/../App/Repository.php';
+
+class WatchlistTagRepository extends Repository
+{
+    protected string $table = "watchlist_tag";
+
+    public function __construct(PDO $connection)
+    {
+        parent::__construct($connection);
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Service/BookmarkService.php b/src/server/app/Service/BookmarkService.php
new file mode 100644
index 0000000000000000000000000000000000000000..077f6d58632999d112ab53897974f600af96e1d0
--- /dev/null
+++ b/src/server/app/Service/BookmarkService.php
@@ -0,0 +1,26 @@
+<?php
+require_once __DIR__ . '/../Config/Database.php';
+require_once __DIR__ . '/../Exception/ValidationException.php';
+require_once __DIR__ . '/../Utils/UUIDGenerator.php';
+
+require_once __DIR__ . '/../Domain/WatchlistSave.php';
+
+require_once __DIR__ . '/../Repository/WatchlistSaveRepository.php';
+
+require_once __DIR__ . '/../Model/bookmark/BookmarkGetRequest.php';
+
+class BookmarkService
+{
+    private WatchlistSaveRepository $watchlistSaveRepository;
+
+    public function __construct(WatchlistSaveRepository $watchlistSaveRepository)
+    {
+        $this->watchlistSaveRepository = $watchlistSaveRepository;
+    }
+
+    public function findByUser(BookmarkGetRequest $request)
+    {
+        $result = $this->watchlistSaveRepository->findByUser($request->userId, $request->page, $request->pageSize);
+        return $result;
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Service/CatalogService.php b/src/server/app/Service/CatalogService.php
new file mode 100644
index 0000000000000000000000000000000000000000..f340805663344e6e679335c39b987cb5feb592ff
--- /dev/null
+++ b/src/server/app/Service/CatalogService.php
@@ -0,0 +1,223 @@
+<?php
+
+require_once __DIR__ . '/../Repository/CatalogRepository.php';
+require_once __DIR__ . '/../Config/Database.php';
+require_once __DIR__ . '/../Utils/FileUploader.php';
+require_once __DIR__ . '/../Utils/UUIDGenerator.php';
+
+require_once __DIR__ . '/../Model/CatalogCreateRequest.php';
+require_once __DIR__ . '/../Model/catalog/CatalogUpdateRequest.php';
+require_once __DIR__ . '/../Model/CatalogSearchRequest.php';
+
+require_once __DIR__ . '/../Model/CatalogCreateResponse.php';
+require_once __DIR__ . '/../Model/CatalogSearchResponse.php';
+
+class CatalogService
+{
+    private CatalogRepository $catalogRepository;
+    private FileUploader $posterUploader;
+    private FileUploader $trailerUploader;
+
+    public function __construct(CatalogRepository $catalogRepository)
+    {
+        $this->catalogRepository = $catalogRepository;
+        $this->posterUploader = new FileUploader('Poster', 'assets/images/catalogs/posters/');
+        $this->trailerUploader = new FileUploader('Trailer', 'assets/videos/catalogs/trailers/');
+
+        $this->trailerUploader->allowedExtTypes = ["mp4"];
+        $this->trailerUploader->allowedMimeTypes = ["video/mp4"];
+        $this->trailerUploader->maxFileSize = 100000000;
+    }
+
+    public function findAll(int $page = 1, string $category = "MIXED"): array
+    {
+        $query = $this->catalogRepository->query();
+        if ($category != "MIXED") {
+            $category = strtoupper(trim($category));
+            $query = $query->whereEquals('category', $category);
+        }
+        $projection = ['id', 'uuid', 'title', 'category', 'description', 'poster'];
+        $catalogs = $query->get($projection, $page, 10);
+        return $catalogs;
+    }
+
+    public function findByUUID(string $uuid): ?Catalog
+    {
+        $catalog = $this->catalogRepository->findOne('uuid', $uuid);
+        return $catalog;
+    }
+
+    public function deleteByUUID(string $uuid): void
+    {
+        $catalog = $this->catalogRepository->findOne('uuid', $uuid);
+        if ($catalog) {
+            $this->catalogRepository->deleteBy('uuid', $uuid);
+        } else {
+            throw new ValidationException("Catalog not found.");
+        }
+    }
+
+    public function deleteById(int $id): void
+    {
+        $this->catalogRepository->deleteBy('id', $id);
+    }
+
+    public function create(CatalogCreateRequest $request): CatalogCreateResponse
+    {
+        $this->validateCatalogCreateRequest($request);
+
+        try {
+            Database::beginTransaction();
+
+            $catalog = new Catalog();
+
+            $catalog->uuid = UUIDGenerator::uuid4();
+            $catalog->title = trim($request->title);
+            $catalog->description = trim($request->description);
+
+            $postername = $this->posterUploader->uploadFie($request->poster, $catalog->title);
+            if ($request->trailer && $request->trailer['error'] == UPLOAD_ERR_OK) {
+                $trailername = $this->trailerUploader->uploadFie($request->trailer, $catalog->title);
+            }
+
+            $catalog->poster = $postername;
+            $catalog->trailer = $trailername ?? null;
+            $catalog->category = strtoupper(trim($request->category));
+
+            $this->catalogRepository->save($catalog);
+
+            $response = new CatalogCreateResponse();
+            $response->catalog = $catalog;
+
+            Database::commitTransaction();
+            return $response;
+        } catch (FileUploaderException $exception) {
+            Database::rollbackTransaction();
+            throw new ValidationException($exception->getMessage());
+        } catch (\Exception $exception) {
+            Database::rollbackTransaction();
+            throw $exception;
+        }
+    }
+
+    private function validateCatalogCreateRequest(CatalogCreateRequest $request)
+    {
+        if (
+            $request->title == null || trim($request->title) == ""
+        ) {
+            throw new ValidationException("Title cannot be blank.");
+        }
+
+        if (strlen($request->title) > 40) {
+            throw new ValidationException("Title cannot be more than 40 characters.");
+        }
+
+        if (strlen($request->description) > 255) {
+            throw new ValidationException("Description cannot be more than 255 characters.");
+        }
+
+        if ($request->category == null || trim($request->category) == "") {
+            throw new ValidationException("Category cannot be blank.");
+        }
+
+        if ($request->category != "ANIME" && $request->category != "DRAMA") {
+            throw new ValidationException("Category must be either ANIME or DRAMA.");
+        }
+
+        if ($request->poster == null || $request->poster['error'] != UPLOAD_ERR_OK) {
+            throw new ValidationException("Poster cannot be blank.");
+        }
+    }
+
+    public function update(CatalogUpdateRequest $request)
+    {
+        $this->validateCatalogUpdateRequest($request);
+
+        try {
+            Database::beginTransaction();
+
+            $catalog = $this->catalogRepository->findOne('uuid', $request->uuid);
+
+            if (!$catalog) {
+                throw new ValidationException("Catalog not found.");
+            }
+
+            $catalog->title = trim($request->title);
+            $catalog->description = trim($request->description);
+
+            if ($request->poster && $request->poster['error'] == UPLOAD_ERR_OK) {
+                $postername = $this->posterUploader->uploadFie($request->poster, $catalog->title);
+                $catalog->poster = $postername;
+            }
+
+            if ($request->trailer && $request->trailer['error'] == UPLOAD_ERR_OK) {
+                $trailername = $this->trailerUploader->uploadFie($request->trailer, $catalog->title);
+                $catalog->trailer = $trailername;
+            }
+
+            if ($request->category != null && trim($request->category) != "") {
+                $catalog->category = strtoupper(trim($request->category));
+            }
+
+            $this->catalogRepository->update($catalog);
+
+            Database::commitTransaction();
+        } catch (FileUploaderException $exception) {
+            Database::rollbackTransaction();
+            throw new ValidationException($exception->getMessage());
+        } catch (\Exception $exception) {
+            Database::rollbackTransaction();
+            throw $exception;
+        }
+    }
+
+    private function validateCatalogUpdateRequest(CatalogUpdateRequest $request)
+    {
+        if ($request->uuid == null || trim($request->uuid) == "") {
+            throw new ValidationException("UUID cannot be blank.");
+        }
+
+        if (
+            $request->title == null || trim($request->title) == ""
+        ) {
+            throw new ValidationException("Title cannot be blank.");
+        }
+
+        if (strlen($request->title) > 40) {
+            throw new ValidationException("Title cannot be more than 40 characters.");
+        }
+
+        if (strlen($request->description) > 255) {
+            throw new ValidationException("Description cannot be more than 255 characters.");
+        }
+
+        if ($request->category == null || trim($request->category) == "") {
+            throw new ValidationException("Category cannot be blank.");
+        }
+
+        if ($request->category != "ANIME" && $request->category != "DRAMA") {
+            throw new ValidationException("Category must be either ANIME or DRAMA.");
+        }
+    }
+
+    public function search(CatalogSearchRequest $catalogSearchRequest): CatalogSearchResponse
+    {
+        $this->validateCatalogSearchRequest($catalogSearchRequest);
+
+        $query = $this->catalogRepository->query();
+        $query = $query->whereContains('title', $catalogSearchRequest->title);
+        $catalogs = $query->get(["id", "uuid", "title", "poster", "category"], $catalogSearchRequest->page, $catalogSearchRequest->pageSize);
+
+        $response = new CatalogSearchResponse();
+        $response->catalogs = $catalogs;
+
+        return $response;
+    }
+
+    private function validateCatalogSearchRequest(CatalogSearchRequest $catalogSearchRequest): void
+    {
+        if (!isset($catalogSearchRequest->title)) {
+            throw new ValidationException("Search field is required");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Service/SessionService.php b/src/server/app/Service/SessionService.php
new file mode 100644
index 0000000000000000000000000000000000000000..cff1a119e380f874d4d8655b32be0a00597bba39
--- /dev/null
+++ b/src/server/app/Service/SessionService.php
@@ -0,0 +1,59 @@
+<?php
+
+require_once __DIR__ . '/../Utils/UUIDGenerator.php';
+
+require_once __DIR__ . '/../Repository/SessionRepository.php';
+require_once __DIR__ . '/../Repository/UserRepository.php';
+
+require_once __DIR__ . '/../Domain/Session.php';
+require_once __DIR__ . '/../Domain/User.php';
+
+require_once __DIR__ . '/../Model/session/SessionCreateRequest.php';
+
+class SessionService
+{
+    public static string $COOKIE_NAME = 'bogoshipo__ohimesama';
+    private SessionRepository $sessionRepository;
+    private UserRepository $userRepository;
+
+    public function __construct(SessionRepository $sessionRepository, UserRepository $userRepository)
+    {
+        $this->sessionRepository = $sessionRepository;
+        $this->userRepository = $userRepository;
+    }
+
+    public function create(SessionCreateRequest $sessionCreateRequest): Session
+    {
+        $session = new Session();
+        $session->id = UUIDGenerator::uuid4();
+        $session->userId = $sessionCreateRequest->userId;
+        $session->expired = gmdate(DATE_RFC3339, strtotime("+1 week"));
+
+        $this->sessionRepository->save($session);
+
+        setcookie(self::$COOKIE_NAME, $session->id, time() + (60 * 60 * 24 * 7), "/", "", false, true);
+
+        return $session;
+    }
+
+    public function destroy()
+    {
+        $sessionId = $_COOKIE[self::$COOKIE_NAME] ?? '';
+        $this->sessionRepository->deleteBy("id", $sessionId);
+
+        setcookie(self::$COOKIE_NAME, '', 1, "/");
+    }
+
+    public function current(): ?User
+    {
+        $sessionId = $_COOKIE[self::$COOKIE_NAME] ?? '';
+        $session = $this->sessionRepository->findOne("id", $sessionId);
+
+        if ($session == null || $session->expired < gmdate(DATE_RFC3339)) {
+            $this->destroy();
+            return null;
+        }
+
+        return $this->userRepository->findOne("id", $session->userId);
+    }
+}
diff --git a/src/server/app/Service/TagService.php b/src/server/app/Service/TagService.php
new file mode 100644
index 0000000000000000000000000000000000000000..7923586703f3b12646372e74bfde5db0017e2bde
--- /dev/null
+++ b/src/server/app/Service/TagService.php
@@ -0,0 +1,19 @@
+<?php
+
+class TagService
+{
+    private TagRepository $tagRepository;
+
+    public function __construct(TagRepository $tagRepository)
+    {
+        $this->tagRepository = $tagRepository;
+    }
+
+    public function findAll()
+    {
+        $query = $this->tagRepository->query();
+        $projection = ["id", "name"];
+        $tags = $query->get($projection, 1, 100);
+        return $tags;
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Service/UserService.php b/src/server/app/Service/UserService.php
new file mode 100644
index 0000000000000000000000000000000000000000..f3f768014bead8cdcd6e7dcc393e3819b7939d40
--- /dev/null
+++ b/src/server/app/Service/UserService.php
@@ -0,0 +1,202 @@
+<?php
+
+require_once __DIR__ . '/../Config/Database.php';
+require_once __DIR__ . '/../Utils/UUIDGenerator.php';
+
+require_once __DIR__ . '/../Repository/UserRepository.php';
+
+require_once __DIR__ . '/../Model/UserSignUpRequest.php';
+require_once __DIR__ . '/../Model/UserSignUpResponse.php';
+require_once __DIR__ . '/../Model/UserSignInRequest.php';
+require_once __DIR__ . '/../Model/UserSignInResponse.php';
+require_once __DIR__ . '/../Model/UserEditRequest.php';
+require_once __DIR__ . '/../Model/UserEditResponse.php';
+
+
+class UserService
+{
+    private UserRepository $userRepository;
+
+    public function __construct(UserRepository $userRepository)
+    {
+        $this->userRepository = $userRepository;
+    }
+
+    public function signUp(UserSignUpRequest $request): UserSignUpResponse
+    {
+        $this->validateUserSignUpRequest($request);
+
+        try {
+            Database::beginTransaction();
+
+            $user = $this->userRepository->findOne("email", $request->email);
+            if (isset($user)) {
+                throw new ValidationException("User already exist");
+            }
+
+            $request->email = trim($request->email);
+            $request->password = trim($request->password);
+            $request->confirmPassword = trim($request->confirmPassword);
+
+            $user = new User();
+            $user->uuid = UUIDGenerator::uuid4();
+            $user->name = explode("@", $request->email)[0];
+            $user->email = $request->email;
+            $user->password = password_hash($request->password, PASSWORD_BCRYPT);
+
+            $this->userRepository->save($user);
+
+            $response = new UserSignUpResponse();
+            $response->user = $user;
+
+            Database::commitTransaction();
+
+            return $response;
+        } catch (Exception $exception) {
+            Database::rollbackTransaction();
+            throw $exception;
+        }
+    }
+
+    public function signIn(UserSignInRequest $request): UserSignInResponse
+    {
+        $this->validateUserSignInRequest($request);
+
+        $user = $this->userRepository->findOne("email", $request->email);
+        if ($user == null) {
+            throw new ValidationException("Invalid email or password.");
+        }
+
+        if (password_verify($request->password, $user->password)) {
+            $response = new UserSignInResponse();
+            $response->user = $user;
+
+            return $response;
+        } else {
+            throw new ValidationException("Invalid email or password.");
+        }
+    }
+
+    public function update(User $currentuser, UserEditRequest $request)
+    {
+        $this->validateEditProfileRequest($currentuser, $request);
+        try {
+            //code...
+            Database::beginTransaction();
+
+            if (!($request->name == null || trim($request->name == ""))) {
+                $currentuser->name = trim($request->name);
+                $this->userRepository->updateName($currentuser);
+            }
+
+            if (
+                !($request->oldPassword == null || trim($request->oldPassword) == "")
+                && !($request->newPassword == null || trim($request->newPassword) == "")
+            ) {
+                $currentuser->password = password_hash(trim($request->newPassword), PASSWORD_BCRYPT);
+                $this->userRepository->updatePassword($currentuser);
+            }
+            Database::commitTransaction();
+        } catch (\Exception $exception) {
+            //throw $th;
+            Database::rollbackTransaction();
+            throw $exception;
+        }
+    }
+
+    private function validateUserSignUpRequest(UserSignUpRequest $request)
+    {
+        if (
+            $request->email == null ||
+            $request->password == null ||
+            $request->confirmPassword == null ||
+            trim($request->email) == "" ||
+            trim($request->password) == "" ||
+            trim($request->confirmPassword) == ""
+        ) {
+            throw new ValidationException("Email, password, and confirm password cannot be blank.");
+        }
+
+        if ($request->password != $request->confirmPassword) {
+            throw new ValidationException("Make sure both passwords are typed the same.");
+        }
+    }
+
+    private function validateUserSignInRequest(UserSignInRequest $request)
+    {
+
+        if (
+            $request->email == null ||
+            $request->password == null ||
+            trim($request->email) == "" ||
+            trim($request->password) == ""
+        ) {
+            throw new ValidationException("Email and password cannot be blank.");
+        }
+    }
+
+    public function validateEditProfileRequest(User $currentuser, UserEditRequest $request)
+    {
+        if (
+            ($request->oldPassword == null || trim($request->oldPassword) == "")
+            && ($request->newPassword == null || trim($request->newPassword) == "")
+            && ($request->name == null || trim($request->name == ""))
+        ) {
+            throw new ValidationException("Data cannot be empty.");
+        } else if (
+            !($request->name == null || trim($request->name == ""))
+            && !($request->oldPassword == null || trim($request->oldPassword) == "")
+            && ($request->newPassword == null || trim($request->newPassword) == "")
+        ) {
+            throw new ValidationException("New password cannot be blank.");
+        } else if (
+            !($request->name == null || trim($request->name == ""))
+            && ($request->oldPassword == null || trim($request->oldPassword) == "")
+            && !($request->newPassword == null || trim($request->newPassword) == "")
+        ) {
+            throw new ValidationException("Old password cannot be blank.");
+        } else if (
+            ($request->name == null || trim($request->name == ""))
+            && !($request->oldPassword == null || trim($request->oldPassword) == "")
+            && ($request->newPassword == null || trim($request->newPassword) == "")
+        ) {
+            throw new ValidationException("New password cannot be blank.");
+        } else if (
+            ($request->name == null || trim($request->name == ""))
+            && ($request->oldPassword == null || trim($request->oldPassword) == "")
+            && !($request->newPassword == null || trim($request->newPassword) == "")
+        ) {
+            throw new ValidationException("Old password cannot be blank.");
+        }
+
+        if (
+            (!($request->oldPassword == null || trim($request->oldPassword) == "")
+                && !($request->newPassword == null || trim($request->newPassword) == "")) &&
+            ($request->oldPassword == $request->newPassword)
+        ) {
+            throw new ValidationException("New password cannot be the same as old password.");
+        }
+
+        if (
+            !($request->oldPassword == null || trim($request->oldPassword) == "") &&
+            !password_verify($request->oldPassword, $currentuser->password)
+        ) {
+            throw new ValidationException("Old password is incorrect.");
+        }
+    }
+
+    public function findByEmail(string $email): User
+    {
+        return $this->userRepository->findOne('email', $email);
+    }
+
+    public function deleteByEmail(string $email)
+    {
+        $this->userRepository->deleteBy('email', $email);
+    }
+
+    public function deleteBySession(string $email)
+    {
+        $this->userRepository->deleteBySession($email);
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Service/WatchlistService.php b/src/server/app/Service/WatchlistService.php
new file mode 100644
index 0000000000000000000000000000000000000000..426fd5d40406b2fa507e078dfc327fd718ebcd13
--- /dev/null
+++ b/src/server/app/Service/WatchlistService.php
@@ -0,0 +1,329 @@
+<?php
+require_once __DIR__ . '/../Config/Database.php';
+require_once __DIR__ . '/../Exception/ValidationException.php';
+require_once __DIR__ . '/../Utils/UUIDGenerator.php';
+
+require_once __DIR__ . '/../Domain/Watchlist.php';
+require_once __DIR__ . '/../Domain/WatchlistItem.php';
+require_once __DIR__ . '/../Domain/WatchlistLike.php';
+require_once __DIR__ . '/../Domain/WatchlistTag.php';
+
+require_once __DIR__ . '/../Repository/WatchlistRepository.php';
+require_once __DIR__ . '/../Repository/WatchlistItemRepository.php';
+require_once __DIR__ . '/../Repository/WatchlistLikeRepository.php';
+require_once __DIR__ . '/../Repository/WatchlistTagRepository.php';
+
+require_once __DIR__ . '/../Model/WatchlistsGetRequest.php';
+require_once __DIR__ . '/../Model/WatchlistCreateRequest.php';
+require_once __DIR__ . '/../Model/watchlist/WatchlistGetOneByUserRequest.php';
+require_once __DIR__ . '/../Model/watchlist/WatchlistLikeRequest.php';
+require_once __DIR__ . '/../Model/watchlist/WatchlistSaveRequest.php';
+require_once __DIR__ . '/../Model/watchlist/WatchlistGetOneRequest.php';
+require_once __DIR__ . '/../Model/watchlist/WatchlistEditRequest.php';
+require_once __DIR__ . '/../Model/watchlist/WatchlistDeleteRequest.php';
+
+class WatchlistService
+{
+    private WatchlistRepository $watchlistRepository;
+    private WatchlistItemRepository $watchlistItemRepository;
+    private WatchlistLikeRepository $watchlistLikeRepository;
+    private WatchlistSaveRepository $watchlistSaveRepository;
+    private WatchlistTagRepository $watchlistTagRepository;
+
+    public function __construct(WatchlistRepository $watchlistRepository, WatchlistItemRepository $watchlistItemRepository, WatchlistLikeRepository $watchlistLikeRepository, WatchlistSaveRepository $watchlistSaveRepository, WatchlistTagRepository $watchlistTagRepository)
+    {
+        $this->watchlistRepository = $watchlistRepository;
+        $this->watchlistItemRepository = $watchlistItemRepository;
+        $this->watchlistLikeRepository = $watchlistLikeRepository;
+        $this->watchlistSaveRepository = $watchlistSaveRepository;
+        $this->watchlistTagRepository = $watchlistTagRepository;
+    }
+
+
+    public function create(WatchlistCreateRequest $watchlistCreateRequest)
+    {
+        $this->validateWatchlistCreateEditRequest($watchlistCreateRequest);
+
+        try {
+            Database::beginTransaction();
+
+            // Create watchlist
+            $watchlist = new Watchlist();
+            $watchlist->uuid = UUIDGenerator::uuid4();
+            $watchlist->title = $watchlistCreateRequest->title;
+            $watchlist->description = $watchlistCreateRequest->description;
+            $watchlist->visibility = $watchlistCreateRequest->visibility;
+            $watchlist->category = "DRAMA";
+            $watchlist->userId = $watchlistCreateRequest->userId;
+
+            // check watchlist category by travers through items
+            $cntDrama = 0;
+            $cntAnime = 0;
+            foreach ($watchlistCreateRequest->items as $item) {
+                if ($item["category"] == "ANIME")
+                    $cntAnime++;
+                if ($item["category"] == "DRAMA")
+                    $cntDrama++;
+            }
+
+            if ($cntDrama != 0 && $cntAnime != 0)
+                $watchlist->category = "MIXED";
+            else if ($cntAnime != 0)
+                $watchlist->category = "ANIME";
+
+            $watchlistNew = $this->watchlistRepository->save($watchlist);
+
+            // save the items
+            $currRank = 1;
+            foreach ($watchlistCreateRequest->items as $item) {
+                $watchlistItem = new WatchlistItem();
+                $watchlistItem->uuid = UUIDGenerator::uuid4();
+                $watchlistItem->rank = $currRank;
+                $watchlistItem->description = $item["description"];
+                $watchlistItem->watchlistId = $watchlistNew->id;
+                $watchlistItem->catalogId = $item["id"];
+
+                $this->watchlistItemRepository->save($watchlistItem);
+
+                $currRank++;
+            }
+
+            // save the tags
+            foreach ($watchlistCreateRequest->tags as $tag) {
+                $watchlistTag = new WatchlistTag();
+                $watchlistTag->tagId = $tag["id"];
+                $watchlistTag->watchlistId = $watchlist->id;
+
+                $this->watchlistTagRepository->save($watchlistTag);
+            }
+
+            Database::commitTransaction();
+        } catch (\Exception $exception) {
+            Database::rollbackTransaction();
+            throw $exception;
+        }
+    }
+
+    public function edit(WatchlistEditRequest $watchlistEditRequest)
+    {
+        $this->validateWatchlistCreateEditRequest($watchlistEditRequest);
+
+        try {
+            Database::beginTransaction();
+
+            // Create watchlist
+            $watchlist = new Watchlist();
+            $watchlist->id = $watchlistEditRequest->watchlist["watchlist_id"];
+            $watchlist->uuid = $watchlistEditRequest->watchlist["watchlist_uuid"];
+            $watchlist->title = $watchlistEditRequest->title;
+            $watchlist->description = $watchlistEditRequest->description;
+            $watchlist->visibility = $watchlistEditRequest->visibility;
+            $watchlist->category = "DRAMA";
+            $watchlist->userId = $watchlistEditRequest->userId;
+
+            // check watchlist category by travers through items
+            $cntDrama = 0;
+            $cntAnime = 0;
+            foreach ($watchlistEditRequest->items as $item) {
+                if ($item["category"] == "ANIME")
+                    $cntAnime++;
+                if ($item["category"] == "DRAMA")
+                    $cntDrama++;
+            }
+
+            if ($cntDrama != 0 && $cntAnime != 0)
+                $watchlist->category = "MIXED";
+            else if ($cntAnime != 0)
+                $watchlist->category = "ANIME";
+
+            $watchlistNew = $this->watchlistRepository->update($watchlist);
+
+            // delete all items with corresponding watchlistId
+            $this->watchlistItemRepository->deleteBy("watchlist_id", $watchlistNew->id);
+
+            // save the items
+            $currRank = 1;
+            foreach ($watchlistEditRequest->items as $item) {
+                $watchlist_item = new WatchlistItem();
+                $watchlist_item->uuid = UUIDGenerator::uuid4();
+                $watchlist_item->rank = $currRank;
+                $watchlist_item->description = $item["description"];
+                $watchlist_item->watchlistId = $watchlistNew->id;
+                $watchlist_item->catalogId = $item["id"];
+
+                $this->watchlistItemRepository->save($watchlist_item);
+
+                $currRank++;
+            }
+
+            // delete all tags with corresponding watchlistId
+            $this->watchlistTagRepository->deleteBy("watchlist_id", $watchlistNew->id);
+
+            // save the tags
+            foreach ($watchlistEditRequest->tags as $tag) {
+                $watchlistTag = new WatchlistTag();
+                $watchlistTag->tagId = $tag["id"];
+                $watchlistTag->watchlistId = $watchlist->id;
+
+                $this->watchlistTagRepository->save($watchlistTag);
+            }
+
+            Database::commitTransaction();
+        } catch (\Exception $exception) {
+            Database::rollbackTransaction();
+            throw $exception;
+        }
+    }
+
+    public function findAll(WatchlistsGetRequest $watchlistsGetRequest)
+    {
+        if (!in_array(strtoupper(trim($watchlistsGetRequest->category)), ["", "MIXED", "ANIME", "DRAMA"])) {
+            $watchlistsGetRequest->category = "";
+        }
+        if (!isset($watchlistsGetRequest->order) || !in_array(strtoupper(trim($watchlistsGetRequest->order)), ["ASC", "DESC"])) {
+            $watchlistsGetRequest->order = "DESC";
+        }
+        if (!isset($watchlistsGetRequest->sortBy) || !in_array(strtoupper(trim($watchlistsGetRequest->sortBy)), ["DATE", "LOVE"])) {
+            $watchlistsGetRequest->sortBy = "LOVE";
+        }
+        if (!isset($watchlistsGetRequest->tag) || !in_array(strtoupper(trim($watchlistsGetRequest->tag)), $watchlistsGetRequest->tagsInit)) {
+            $watchlistsGetRequest->tag = "";
+        }
+
+        if ($watchlistsGetRequest->sortBy == "LOVE")
+            $watchlistsGetRequest->sortBy = "love_count";
+        if ($watchlistsGetRequest->sortBy == "DATE")
+            $watchlistsGetRequest->sortBy = "w.created_at";
+
+        $result = $this->watchlistRepository->findAllCustom($watchlistsGetRequest->userId, $watchlistsGetRequest->search, $watchlistsGetRequest->category, $watchlistsGetRequest->sortBy, $watchlistsGetRequest->order, $watchlistsGetRequest->tag, $watchlistsGetRequest->page, 10);
+        return $result;
+    }
+
+    public function findByUser(WatchlistGetOneByUserRequest $request)
+    {
+        if (!isset($request->visibility) || !in_array(strtoupper(trim($request->visibility)), ["ALL", "PUBLIC", "PRIVATE"]) || strtoupper($request->visibility) == "ALL") {
+            $request->visibility = "";
+        }
+
+        $result = $this->watchlistRepository->findByUser($request->userId, strtoupper($request->visibility), 1, 10);
+        return $result;
+    }
+
+    public function findByUUID(WatchlistsGetOneRequest $request)
+    {
+        $result = $this->watchlistRepository->findByUUID($request->uuid, $request->userId, $request->page, $request->pageSize);
+        return $result;
+    }
+
+
+    public function like(WatchlistLikeRequest $watchlistLikeRequest): void
+    {
+        $this->validateWatchlistLikeAndSaveRequest($watchlistLikeRequest);
+
+        try {
+            Database::beginTransaction();
+
+            // 1. Get watchlist by UUID
+            $watchlist = $this->watchlistRepository->findOne("uuid", $watchlistLikeRequest->watchlistUUID, ["id"]);
+            if ($watchlist == null) {
+                throw new ValidationException("Watchlist not found.");
+            }
+
+            // 2. Insert or delete a row from watchlist_like table
+            $watchlistLike = $this->watchlistLikeRepository->findOneByWatchlistAndUser($watchlist->id, $watchlistLikeRequest->userId);
+            if ($watchlistLike == null) {
+                $this->watchlistLikeRepository->saveByWatchlistAndUser($watchlist->id, $watchlistLikeRequest->userId);
+            } else {
+                $this->watchlistLikeRepository->deleteByWatchlistAndUser($watchlist->id, $watchlistLikeRequest->userId);
+            }
+
+            Database::commitTransaction();
+        } catch (\Exception $exception) {
+            Database::rollbackTransaction();
+            throw $exception;
+        }
+    }
+
+    public function bookmark(WatchlistSaveRequest $watchlistSaveRequest): void
+    {
+        $this->validateWatchlistLikeAndSaveRequest($watchlistSaveRequest);
+
+        try {
+            Database::beginTransaction();
+
+            // 1. Get watchlist by UUID
+            $watchlist = $this->watchlistRepository->findOne("uuid", $watchlistSaveRequest->watchlistUUID, ["id"]);
+            if ($watchlist == null) {
+                throw new ValidationException("Watchlist not found.");
+            }
+
+            // 2. Insert or delete a row from watchlist_save table
+            $watchlistLike = $this->watchlistSaveRepository->findOneByWatchlistAndUser($watchlist->id, $watchlistSaveRequest->userId);
+            if ($watchlistLike == null) {
+                $this->watchlistSaveRepository->saveByWatchlistAndUser($watchlist->id, $watchlistSaveRequest->userId);
+            } else {
+                $this->watchlistSaveRepository->deleteByWatchlistAndUser($watchlist->id, $watchlistSaveRequest->userId);
+            }
+
+            Database::commitTransaction();
+        } catch (\Exception $exception) {
+            Database::rollbackTransaction();
+            throw $exception;
+        }
+    }
+
+    public function deleteByUUID(WatchlistDeleteRequest $watchlistDeleteRequest)
+    {
+        $this->watchlistRepository->deleteBy("uuid", $watchlistDeleteRequest->watchlistUUID);
+    }
+
+    private function validateWatchlistCreateEditRequest(WatchlistCreateRequest|WatchlistEditRequest $watchlistCreateUpdateRequest)
+    {
+        if (!isset($watchlistCreateUpdateRequest->title) || trim($watchlistCreateUpdateRequest->title) == "") {
+            throw new ValidationException("Title is required.");
+        }
+        if (strlen($watchlistCreateUpdateRequest->title) > 40) {
+            throw new ValidationException("Title is too long. Maximum 40 chars.");
+        }
+        if (!isset($watchlistCreateUpdateRequest->visibility) || !in_array($watchlistCreateUpdateRequest->visibility, ["PUBLIC", "PRIVATE"])) {
+            throw new ValidationException("Visibility is invalid.");
+        }
+        if (isset($watchlistCreateUpdateRequest->description) && strlen($watchlistCreateUpdateRequest->description) > 255) {
+            throw new ValidationException("Description is too long. Maximum 255 characters.");
+        }
+        if (!isset($watchlistCreateUpdateRequest->items) || count($watchlistCreateUpdateRequest->items) == 0) {
+            throw new ValidationException("Watchlist must contain 1 item.");
+        }
+        if (count($watchlistCreateUpdateRequest->items) > 50) {
+            throw new ValidationException("Too many items. Maximum 50 items.");
+        }
+
+        foreach ($watchlistCreateUpdateRequest->items ?? [] as $item) {
+            if (strlen($item["description"]) > 255) {
+                throw new ValidationException("Description is too long for item ${item["title"]}. Maximum 255 chars.");
+            }
+        }
+
+        $selectedTags = [];
+        foreach ($watchlistCreateUpdateRequest->tags ?? [] as $tag) {
+            $found = false;
+            foreach ($watchlistCreateUpdateRequest->initialTags ?? [] as $initTag) {
+                if ($tag["id"] == $initTag->id && !in_array($initTag->id, $selectedTags)) {
+                    array_push($selectedTags, $initTag->id);
+                    $found = true;
+                    break;
+                }
+            }
+            if (!$found) {
+                throw new ValidationException("Tags is invalid.");
+            }
+        }
+    }
+
+    private function validateWatchlistLikeAndSaveRequest(WatchlistLikeRequest|WatchlistSaveRequest $watchlistRequest)
+    {
+        if (!isset($watchlistRequest->watchlistUUID) || trim($watchlistRequest->watchlistUUID) == "") {
+            throw new ValidationException("Watchlist UUID is required");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Utils/EnvLoader.php b/src/server/app/Utils/EnvLoader.php
new file mode 100644
index 0000000000000000000000000000000000000000..e5dbbb81ebe09b738b8deef75e583bb8931d1d1c
--- /dev/null
+++ b/src/server/app/Utils/EnvLoader.php
@@ -0,0 +1,38 @@
+<?php
+class EnvLoader
+{
+    protected $path;
+
+    public function __construct(string $path)
+    {
+        if (!file_exists($path)) {
+            throw new \InvalidArgumentException(sprintf('%s does not exist', $path));
+        }
+        $this->path = $path;
+    }
+
+    public function load(): void
+    {
+        if (!is_readable($this->path)) {
+            throw new \RuntimeException(sprintf('%s file is not readable', $this->path));
+        }
+
+        $lines = file($this->path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+        foreach ($lines as $line) {
+
+            if (strpos(trim($line), '#') === 0) {
+                continue;
+            }
+
+            list($name, $value) = explode('=', $line, 2);
+            $name = trim($name);
+            $value = trim($value);
+
+            if (!array_key_exists($name, $_SERVER) && !array_key_exists($name, $_ENV)) {
+                putenv(sprintf('%s=%s', $name, $value));
+                $_ENV[$name] = $value;
+                $_SERVER[$name] = $value;
+            }
+        }
+    }
+}
diff --git a/src/server/app/Utils/FileUploader.php b/src/server/app/Utils/FileUploader.php
new file mode 100644
index 0000000000000000000000000000000000000000..a16044c730cb3dead72bce52ffbebe47a814fcb9
--- /dev/null
+++ b/src/server/app/Utils/FileUploader.php
@@ -0,0 +1,76 @@
+<?php
+
+require_once __DIR__ . '/../Exception/FileUploaderException.php';
+
+class FileUploader
+{
+    public string $id;
+    public int $maxFilenameSize = 255;
+    public int $maxFileSize = 200000;
+    public string $targetDir = '.';
+    public array $allowedExtTypes = ["jpg", "jpeg", "png", "webp"];
+    public array $allowedMimeTypes = ["image/jpeg", "image/png", "image/webp"];
+
+    public function __construct($id, $targetDir)
+    {
+        $this->id = $id;
+        $this->targetDir = $targetDir;
+    }
+    public function uploadFie($file, $filename)
+    {
+        if ($file == null) {
+            throw new FileUploaderException($this->id . " File is required.");
+        }
+
+        $this->validateFile($file);
+        $isImage = in_array("image/jpeg", $this->allowedMimeTypes) || in_array("image/png", $this->allowedMimeTypes);
+        if ($isImage) {
+            $fileext = "webp";
+        } else {
+            $fileext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
+        }
+        $filename = UUIDGenerator::uuid4() . '.' . $fileext;
+        $targetFile = $this->targetDir . $filename;
+
+        if (file_exists($targetFile)) {
+            throw new FileUploaderException($this->id . " File already exists.");
+        }
+
+
+        if (move_uploaded_file($file["tmp_name"], $targetFile)) {
+            return $filename;
+        }
+        throw new FileUploaderException($this->id . " File is not a valid upload file.");
+    }
+
+    private function validateFile($file)
+    {
+        $this->checkFileUploadedExt($file);
+        $this->checkFileUploadedSize($file);
+        $this->checkFileUploadedMimeType($file);
+    }
+
+    private function checkFileUploadedExt($file)
+    {
+        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
+        if (!in_array($ext, $this->allowedExtTypes)) {
+            throw new FileUploaderException($this->id . " File does not have a valid extension. Only allowed " . implode(", ", $this->allowedExtTypes) . ".");
+        }
+    }
+
+    private function checkFileUploadedSize($file)
+    {
+        if ($file["size"] > $this->maxFileSize) {
+            throw new FileUploaderException($this->id . " File is too big. Maximum " . $this->maxFileSize . " bytes.");
+        }
+    }
+
+    private function checkFileUploadedMimeType($file)
+    {
+        $mimeType = $file['type'];
+
+        if (!in_array($mimeType, $this->allowedMimeTypes)) {
+            throw new FileUploaderException($this->id . " File does not have a valid mime type.");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Utils/FilterBuilder.php b/src/server/app/Utils/FilterBuilder.php
new file mode 100644
index 0000000000000000000000000000000000000000..cd226e72b8023aaddbe19f5185cb5e9fdb08d5e8
--- /dev/null
+++ b/src/server/app/Utils/FilterBuilder.php
@@ -0,0 +1,46 @@
+<?php
+
+class FilterBuilder
+{
+    public string $filterQuery;
+    public function __construct()
+    {
+        $this->filterQuery = "";
+    }
+
+    public function whereEquals(string $key, $value): FilterBuilder
+    {
+        $this->filterQuery .= " WHERE $key = '$value'";
+        return $this;
+    }
+
+    public function whereContains(string $key, $value): FilterBuilder
+    {
+        $this->filterQuery .= " WHERE $key ILIKE '%$value%'";
+        return $this;
+    }
+
+    public function andWhereEquals(string $key, $value): FilterBuilder
+    {
+        $this->filterQuery .= " AND ";
+        return $this->whereEquals($key, $value);
+    }
+
+    public function andWhereContains(string $key, $value): FilterBuilder
+    {
+        $this->filterQuery .= " AND ";
+        return $this->whereContains($key, $value);
+    }
+
+    public function orWhereEquals(string $key, $value): FilterBuilder
+    {
+        $this->filterQuery .= " OR ";
+        return $this->whereEquals($key, $value);
+    }
+
+    public function orWhereContains(string $key, $value): FilterBuilder
+    {
+        $this->filterQuery .= " OR ";
+        return $this->whereContains($key, $value);
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/Utils/QueryBuilder.php b/src/server/app/Utils/QueryBuilder.php
new file mode 100644
index 0000000000000000000000000000000000000000..c145b32ba1e489a7caef1a3261be2c3417e5493d
--- /dev/null
+++ b/src/server/app/Utils/QueryBuilder.php
@@ -0,0 +1,71 @@
+<?php
+
+require_once __DIR__ . '/../App/Repository.php';
+
+
+class QueryBuilder
+{
+    public string $query;
+    protected Repository $repository;
+
+    public function __construct(Repository $repository)
+    {
+        $this->query = "";
+        $this->repository = $repository;
+    }
+
+    public function whereEquals(string $key, $value): QueryBuilder
+    {
+        if (is_int($value))
+            $this->query .= " WHERE $key = $value";
+        else if (is_string($value))
+            $this->query .= " WHERE $key = '$value'";
+        return $this;
+    }
+
+    public function whereContains(string $key, $value): QueryBuilder
+    {
+        $this->query .= " WHERE $key ILIKE '%$value%'";
+        return $this;
+    }
+
+    public function andWhereEquals(string $key, $value): QueryBuilder
+    {
+        $this->query .= " AND ";
+        return $this->whereEquals($key, $value);
+    }
+
+    public function andWhereContains(string $key, $value): QueryBuilder
+    {
+        $this->query .= " AND ";
+        return $this->whereContains($key, $value);
+    }
+
+    public function orWhereEquals(string $key, $value): QueryBuilder
+    {
+        $this->query .= " OR ";
+        return $this->whereEquals($key, $value);
+    }
+
+    public function orWhereContains(string $key, $value): QueryBuilder
+    {
+        $this->query .= " OR ";
+        return $this->whereContains($key, $value);
+    }
+
+    public function join(string $foreignKey, string $table, string $key): QueryBuilder
+    {
+        $this->query .= " JOIN $table ON " . $this->repository->getTable() . ".$foreignKey = $table.$key";
+        return $this;
+    }
+
+    public function get(
+        array $projection = [],
+        int   $page = null,
+        int   $pageSize = null
+    )
+    {
+        return $this->repository->findAll($projection, $page, $pageSize);
+    }
+
+}
\ No newline at end of file
diff --git a/src/server/app/Utils/UUIDGenerator.php b/src/server/app/Utils/UUIDGenerator.php
new file mode 100644
index 0000000000000000000000000000000000000000..43f8559874d53f2d35b43558f7e3bce4d68689e5
--- /dev/null
+++ b/src/server/app/Utils/UUIDGenerator.php
@@ -0,0 +1,14 @@
+<?php
+
+class UUIDGenerator
+{
+    public static function uuid(int $half = 16)
+    {
+        return bin2hex(random_bytes($half));
+    }
+
+    public static function uuid4(): string
+    {
+        return vsprintf('%s%s%s%s', str_split(self::uuid(8), 4));
+    }
+}
\ No newline at end of file
diff --git a/src/server/app/View/404.php b/src/server/app/View/404.php
new file mode 100644
index 0000000000000000000000000000000000000000..bb604b98b3c1aa70e96b4ffd62a163814ed374b7
--- /dev/null
+++ b/src/server/app/View/404.php
@@ -0,0 +1,10 @@
+<main class="error-page-container">
+    <div>
+        <h1>404</h1>
+        <div>
+            <h2>There's <span>NOTHING</span> here...</h2>
+            <p>...maybe the page you're looking for is not found or never existed</p>
+        </div>
+        <a href="/" class="btn btn-bold">Back to Home</a>
+    </div>
+</main>
\ No newline at end of file
diff --git a/src/server/app/View/500.php b/src/server/app/View/500.php
new file mode 100644
index 0000000000000000000000000000000000000000..2de360de2645c69868ab342da52035f357a6a3e6
--- /dev/null
+++ b/src/server/app/View/500.php
@@ -0,0 +1,13 @@
+<main class="error-page-container">
+    <div>
+        <h1>500</h1>
+        <div>
+            <h2>Sorry, It's not you. It's us.</h2>
+            <p>We're expecting an internal server problem.
+                <br>
+                Please try again later.
+            </p>
+        </div>
+        <a href="/" class="btn btn-bold">Back to Home</a>
+    </div>
+</main>
\ No newline at end of file
diff --git a/src/server/app/View/catalog/detail.php b/src/server/app/View/catalog/detail.php
new file mode 100644
index 0000000000000000000000000000000000000000..181a5cba49a09829da9b10b8b3f0fbbf5e680155
--- /dev/null
+++ b/src/server/app/View/catalog/detail.php
@@ -0,0 +1,42 @@
+<?php
+$catalog = $model['data']['item'];
+$userRole = $model['data']['userRole'];
+?>
+
+<main>
+    <div class="catalog-detail-header">
+        <div class="catalog-detail-header-poster"></div>
+        <img class="poster" src="<?= '/assets/images/catalogs/posters/' . $catalog['poster'] ?>"
+            alt="<?= 'Poster of ' . $catalog['title'] ?>">
+    </div>
+    <article class="catalog-detail-content">
+        <h2>
+            <?= $catalog['title'] ?>
+        </h2>
+        <div class="tag">
+            <?= $catalog['category'] ?>
+        </div>
+        <p>
+            <?= (!isset($catalog['description']) || empty($catalog['description'])) ? "No description" : $catalog['description'] ?>
+        </p>
+        <?php if (isset($catalog['trailer']) && $catalog['trailer'] !== null): ?>
+            <h3>Trailer</h3>
+            <video class="catalog-trailer" controls>
+                <source src="<?= '/assets/videos/catalogs/trailers/' . $catalog['trailer'] ?>" type="video/mp4">
+            </video>
+        <?php endif; ?>
+    </article>
+    <?php if ($userRole && $userRole === "ADMIN"): ?>
+        <div class="button-container">
+            <a href="/catalog/<?= $catalog['uuid'] ?>/edit" id="edit" aria-label="Edit <?= $catalog['title'] ?>"
+                class="btn-icon">
+                <?php require PUBLIC_PATH . 'assets/icons/edit.php' ?>
+            </a>
+            <button id="delete-trigger" type="submit" data-uuid="<?= $catalog['uuid'] ?>"
+                data-title="<?= $catalog['title'] ?>" aria-label="Delete <?= $catalog['title'] ?>"
+                class="catalog-delete-trigger dialog-trigger btn-icon">
+                <?php require PUBLIC_PATH . 'assets/icons/trash.php' ?>
+            </button>
+        </div>
+    <?php endif; ?>
+</main>
\ No newline at end of file
diff --git a/src/server/app/View/catalog/edit.php b/src/server/app/View/catalog/edit.php
new file mode 100644
index 0000000000000000000000000000000000000000..5525bd2a388515ec66156535ae217a649f37738b
--- /dev/null
+++ b/src/server/app/View/catalog/edit.php
@@ -0,0 +1,52 @@
+<?php function selectCategory($selected)
+{
+    $id = 'category';
+    $placeholder = 'Select Category';
+    $content = [
+        "DRAMA",
+        "ANIME"
+    ];
+    require __DIR__ . '/../components/select.php';
+}
+?>
+
+<main>
+    <h2>
+        <?= $model['title'] ?>
+    </h2>
+    <form id="catalog-edit-form" enctype="multipart/form-data">
+        <div class="input-group">
+            <label class="input-required">Category</label>
+            <?php selectCategory($model['data']['category'] ?? 'ANIME'); ?>
+        </div>
+        <div class="input-group">
+            <label for="titleField" class="input-required">Title</label>
+            <input type="text" id="titleField" name="title" placeholder="Title"
+                value="<?= $model['data']['title'] ?? "" ?>" maxlength="40" required>
+        </div>
+        <div class="input-group">
+            <label for="descriptionField">Description</label>
+            <textarea placeholder="Enter description" name="description" id="descriptionField" maxlength="255"><?php if (isset($model['data'])) {
+                echo $model['data']['description'];
+            } ?></textarea>
+        </div>
+        <div class="input-group">
+            <label for="posterField" class="input-required">Poster</label>
+            <img class="poster" src="<?= '/assets/images/catalogs/posters/' . $model['data']['poster'] ?>"
+                alt="<?= 'Poster of ' . $model['data']['title'] ?>">
+            <input type="file" id="posterField" name="poster" accept="image/*">
+        </div>
+        <div class="input-group">
+            <?php if (isset($model['data']['trailer']) && $model['data']['trailer'] !== null): ?>
+                <video class="catalog-trailer" controls>
+                    <source src="<?= '/assets/videos/catalogs/trailers/' . $model['data']['trailer'] ?>" type="video/mp4">
+                </video>
+            <?php endif; ?>
+            <label for="trailerField">Trailer</label>
+            <input type="file" id="trailerField" name="trailer" accept="video/mp4">
+        </div>
+        <button id="edit" class="btn-bold" type="submit">
+            Edit
+        </button>
+    </form>
+</main>
\ No newline at end of file
diff --git a/src/server/app/View/catalog/form.php b/src/server/app/View/catalog/form.php
new file mode 100644
index 0000000000000000000000000000000000000000..830b7ea5908b78c98b8aa21f80985f38b3c29c31
--- /dev/null
+++ b/src/server/app/View/catalog/form.php
@@ -0,0 +1,66 @@
+<?php function selectCategory($selected)
+{
+    $id = 'category';
+    $placeholder = 'Select Category';
+    $content = [
+        "DRAMA",
+        "ANIME"
+    ];
+    require __DIR__ . '/../components/select.php';
+}
+
+function alert($title, $message)
+{
+    $type = 'error';
+    require __DIR__ . '/../components/alert.php';
+}
+
+?>
+
+<main>
+    <h2>
+        <?= $model['title'] ?>
+    </h2>
+    <form id="catalog-create-update">
+        <div class="input-group">
+            <label class="input-required">Category</label>
+            <?php selectCategory($model['data']['category'] ?? 'ANIME'); ?>
+        </div>
+
+        <div class="input-group">
+            <label for="titleField" class="input-required">Title</label>
+            <input type="text" id="titleField" name="title" placeholder="Title"
+                value="<?= $model['data']['title'] ?? "" ?>" maxlength="40" required>
+        </div>
+
+        <div class="input-group">
+            <label for="descriptionField">Description</label>
+            <textarea placeholder="Enter description" name="description" id="descriptionField" maxlength="255"><?php if (isset($model['data'])) {
+                echo $model['data']['description'];
+            } ?></textarea>
+        </div>
+
+        <div class="input-group">
+            <label for="posterField" class="input-required">Poster</label>
+            <?php if (isset($model['data']['poster'])): ?>
+                <img id="poster" class="poster" src="<?= '/assets/images/catalogs/posters/' . $model['data']['poster'] ?>"
+                    alt="<?= 'Poster of ' . $model['data']['title'] ?>">
+                <input type="file" id="posterField" name="poster" accept="image/*">
+            <?php else: ?>
+                <input type="file" id="posterField" name="poster" accept="image/*" required>
+            <?php endif; ?>
+        </div>
+        <div class="input-group">
+            <?php if (isset($model['data']['trailer']) && $model['data']['trailer'] !== null): ?>
+                <video id="trailer" class="catalog-trailer" controls>
+                    <source src="<?= '/assets/videos/catalogs/trailers/' . $model['data']['trailer'] ?>" type="video/mp4">
+                </video>
+            <?php endif; ?>
+            <label for="trailerField">Trailer</label>
+            <input type="file" id="trailerField" name="trailer" accept="video/mp4">
+        </div>
+        <button id="save" class="btn-bold" type="submit">
+            Save
+        </button>
+    </form>
+</main>
\ No newline at end of file
diff --git a/src/server/app/View/catalog/index.php b/src/server/app/View/catalog/index.php
new file mode 100644
index 0000000000000000000000000000000000000000..a6df824829344d67024db762795792956b11baeb
--- /dev/null
+++ b/src/server/app/View/catalog/index.php
@@ -0,0 +1,66 @@
+<?php
+function selectCategory($selected)
+{
+    $id = 'category';
+    $placeholder = 'MIXED';
+    $content = [
+        "MIXED",
+        "DRAMA",
+        "ANIME"
+    ];
+    require __DIR__ . '/../components/select.php';
+}
+
+function catalogCard(Catalog $catalog, bool $isAdmin = false)
+{
+    $title = $catalog->title;
+    $poster = $catalog->poster;
+    $category = $catalog->category;
+    $description = $catalog->description;
+    $uuid = $catalog->uuid;
+    $id = $catalog->id;
+    $editable = $isAdmin;
+    require __DIR__ . '/../components/card/catalogCard.php';
+}
+
+function pagination(int $currentPage, int $totalPage)
+{
+    require __DIR__ . '/../components/pagination.php';
+}
+?>
+<main>
+    <section class="search-filter">
+        <form action="/catalog">
+            <div class="input">
+                <label>Category</label>
+                <?php selectCategory($model['data']['category'] ?? ""); ?>
+            </div>
+            <button class="btn-primary" type="submit">
+                Apply
+            </button>
+        </form>
+        <?php if ($model['data']['userRole'] && $model['data']['userRole'] === "ADMIN"): ?>
+            <a href="/catalog/create" class="btn btn-bold">
+                <span class="icon-new">
+                    <?php require PUBLIC_PATH . 'assets/icons/plus.php' ?>
+                </span>
+                Add Catalog
+            </a>
+        <?php endif; ?>
+    </section>
+    <?php if (count($model['data']['catalogs']['items']) == 0): ?>
+        <div class="no-item__container">
+            <h1>Oops! 😣</h1>
+            <div>
+                <h2>There's No Catalog Yet...</h2>
+                <p>...we will add more soon!</p>
+            </div>
+        </div>
+    <?php endif; ?>
+    <section class="content">
+        <?php foreach ($model['data']['catalogs']['items'] ?? [] as $catalog): ?>
+            <?php catalogCard($catalog, $model['data']['userRole'] && $model['data']['userRole'] === "ADMIN"); ?>
+        <?php endforeach; ?>
+        <?php pagination($model['data']['catalogs']['page'], $model['data']['catalogs']['totalPage']); ?>
+    </section>
+</main>
\ No newline at end of file
diff --git a/src/server/app/View/components/alert.php b/src/server/app/View/components/alert.php
new file mode 100644
index 0000000000000000000000000000000000000000..4cc4bdb6064465c40e9e8e166bb364dbe6f02cb0
--- /dev/null
+++ b/src/server/app/View/components/alert.php
@@ -0,0 +1,14 @@
+<div class="alert" data-type="<?= $type ?>">
+    <?php require PUBLIC_PATH . "assets/icons/" . $type . ".php" ?>
+    <div>
+        <h3>
+            <?= $title ?>
+        </h3>
+        <p>
+            <?= $message ?>
+        </p>
+    </div>
+    <button id="close" class="btn-ghost">
+        <?php require PUBLIC_PATH . "assets/icons/cancel.php" ?>
+    </button>
+</div>
\ No newline at end of file
diff --git a/src/server/app/View/components/card/catalogCard.php b/src/server/app/View/components/card/catalogCard.php
new file mode 100644
index 0000000000000000000000000000000000000000..7fd441154cc70666d2fac417cfbf7fbf08649446
--- /dev/null
+++ b/src/server/app/View/components/card/catalogCard.php
@@ -0,0 +1,39 @@
+<?php
+if (!file_exists('assets/images/catalogs/posters/' . $poster)) {
+    $poster = 'no-poster.webp';
+}
+?>
+
+<div id="card-<?= $uuid ?>" class="card card-catalog">
+    <div class="card-content">
+        <a href="/catalog/<?= $uuid ?>">
+            <img width="86.4" height="128" onerror="this.src = '/assets/images/catalogs/posters/no-poster.webp'"
+                src="<?= "/assets/images/catalogs/posters/" . $poster ?>" alt=<?= $title ?> class="poster"
+                alt="<?= $title ?>" />
+        </a>
+        <div class="card-body">
+            <a href="/catalog/<?= $uuid ?>">
+                <h3 class="card-title">
+                    <?= $title ?>
+                </h3>
+            </a>
+            <div class="tag">
+                <?= $category ?>
+            </div>
+            <p>
+                <?= $description ?>
+            </p>
+        </div>
+    </div>
+    <?php if (isset($editable) && $editable): ?>
+        <div class="card-button-container">
+            <a aria-label="Edit <?= $title ?>" href="/catalog/<?= $uuid ?>/edit" id="edit-<?= $uuid ?>" class="btn-icon">
+                <?php require PUBLIC_PATH . 'assets/icons/edit.php' ?>
+            </a>
+            <button aria-label="Delete <?= $title ?>" id="delete-trigger-<?= $uuid ?>" data-uuid="<?= $uuid ?>"
+                data-title="<?= $title ?>" class="catalog-delete-trigger dialog-trigger btn-icon">
+                <?php require PUBLIC_PATH . 'assets/icons/trash.php' ?>
+            </button>
+        </div>
+    <?php endif; ?>
+</div>
\ No newline at end of file
diff --git a/src/server/app/View/components/card/commentCard.php b/src/server/app/View/components/card/commentCard.php
new file mode 100644
index 0000000000000000000000000000000000000000..681789aa46222f0e7d78bb461e11d04bbe5c431e
--- /dev/null
+++ b/src/server/app/View/components/card/commentCard.php
@@ -0,0 +1,26 @@
+<div class="card card-comment">
+    <div class="card-content">
+        <img src=<?= "/assets/images/catalogs/posters/" . $user_image ?> alt=<?= $user_name . ' profile image' ?>
+            class="avatar" />
+        <div class="card-body">
+            <div class="header">
+                <h4>
+                    <?= $user_name ?>
+                </h4>
+                <p class="subtitle">
+                    <?= $created_at ?>
+                </p>
+            </div>
+            <p>
+                <?= $content ?>
+            </p>
+        </div>
+    </div>
+    <?php if ($is_user): ?>
+        <div class="card-button-container">
+            <button class="btn-icon">
+                <?php require PUBLIC_PATH . 'assets/icons/trash.php' ?>
+            </button>
+        </div>
+    <?php endif; ?>
+</div>
\ No newline at end of file
diff --git a/src/server/app/View/components/card/watchlistCard.php b/src/server/app/View/components/card/watchlistCard.php
new file mode 100644
index 0000000000000000000000000000000000000000..2d9e73b7fe38932ca9c3c9648643509b774cfb71
--- /dev/null
+++ b/src/server/app/View/components/card/watchlistCard.php
@@ -0,0 +1,127 @@
+<?php
+if (!function_exists("formatDate")) {
+    function formatDate($createdAt)
+    {
+        require __DIR__ . '/../../../../config/dateFormat.php';
+    }
+}
+
+if (!function_exists("likeAndSave")) {
+
+    function likeAndSave($class, $icon, $ariaLabel = "")
+    {
+        $triggerClasses = "btn-ghost $class";
+        $triggerText = "";
+        $triggerIcon = $icon;
+        $title = "Sign In Required";
+        $content = 'signInRequired';
+        require __DIR__ . '/../modal.php';
+    }
+}
+?>
+
+<div id="watchlist-card-<?= $uuid ?>" class="card watchlist__card">
+    <div class="card-content">
+        <div class="list__poster">
+            <?php for ($i = 0; $i < 4; $i++): ?>
+                <?php
+                if (!isset($posters[3 - $i])): ?>
+                    <div>
+                        <img loading=<?= $loading ?? 'eager' ?>
+                            src="<?= "/assets/images/catalogs/posters/" . (isset($posters[3 - $i]) ? $posters[3 - $i]["poster"] : "no-poster.webp") ?>"
+                            alt="Anime or Drama Poster" class="poster" />
+                    </div>
+                <?php else: ?>
+                    <?php
+                    if (!file_exists('assets/images/catalogs/posters/' . $posters[3 - $i]['poster'])) {
+                        $posters[3 - $i]['poster'] = 'no-poster.webp';
+                    }
+                    ?>
+                    <a href="/catalog<?= isset($posters[3 - $i]) ? "/" . $posters[3 - $i]["catalog_uuid"] : "" ?>">
+                        <img loading=<?= $loading ?? 'eager' ?>
+                            src="<?= "/assets/images/catalogs/posters/" . (isset($posters[3 - $i]) ? $posters[3 - $i]["poster"] : "no-poster.webp") ?>"
+                            alt="Anime or Drama Poster" class="poster" />
+                    </a>
+                <?php endif; ?>
+            <?php endfor; ?>
+        </div>
+        <div class="card-body">
+            <div class="watchlist__visibility-title">
+                <?php if ($visibility === "PRIVATE"): ?>
+                    <svg xmlns="http://www.w3.org/2000/svg" width="12" height="14" viewBox="0 0 12 14" fill="none">
+                        <path
+                            d="M9 7H10.05C10.2985 7 10.5 7.20145 10.5 7.45V12.55C10.5 12.7985 10.2985 13 10.05 13H1.95C1.70147 13 1.5 12.7985 1.5 12.55V7.45C1.5 7.20145 1.70147 7 1.95 7H3M9 7V4C9 3 8.4 1 6 1C3.6 1 3 3 3 4V7M9 7H3"
+                            stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+                    </svg>
+                <?php endif; ?>
+                <a href="/watchlist/<?= $uuid ?>">
+                    <h3 class="card-title">
+                        <?= $title ?>
+                    </h3>
+                </a>
+            </div>
+            <div class="watchlist__meta">
+                <div class="watchlist__wrapper-type-author">
+                    <span class="tag">
+                        <?= $category ?>
+                    </span>
+                    <?php for ($i = 0; $i < min(3, count($item["tags"])); $i++): ?>
+                        <span class="tag">
+                            <?= $item["tags"][$i]["name"] ?>
+                        </span>
+                    <?php endfor; ?>
+                    <span class="subtitle">by <span class="author-name">
+                            <?= $creator ?>
+                        </span></span>
+                </div>
+                <span class="subtitle">
+                    <?= formatDate($createdAt); ?>
+                </span>
+
+            </div>
+            <p class="watchlist__description">
+                <?= $description ?>
+            </p>
+            <span class="watchlist__item-count">
+                <?php require PUBLIC_PATH . 'assets/icons/clapperboard.php' ?>
+                <?= $itemCount ?> items
+            </span>
+        </div>
+    </div>
+    <div class="watchlist__actions">
+        <div class="watchlist__action-save">
+            <?php if (!$self): ?>
+                <?php if ($userUUID == ""): ?>
+                    <?php likeAndSave("btn__save", "bookmark", "Save " . $title); ?>
+                <?php else: ?>
+                    <button aria-label="Save <?= $title ?>" type="button" class="btn-ghost btn__save" data-id="<?= $uuid ?>"
+                        data-saved="<?= $saved ?>">
+                        <?php
+                        if (isset($saved)) {
+                            $type = $saved ? "filled" : "unfilled";
+                        }
+                        require PUBLIC_PATH . 'assets/icons/bookmark.php'
+                            ?>
+                    </button>
+                <?php endif; ?>
+            <?php endif; ?>
+        </div>
+        <div class="watchlist__action-love">
+            <?php if ($userUUID == ""): ?>
+                <?php likeAndSave("btn__like", "love", "Love " . $title); ?>
+            <?php else: ?>
+                <button aria-label="Love <?= $title ?>" type="button" class="btn-ghost btn__like" data-id="<?= $uuid ?>"
+                    data-liked="<?= $loved ?>">
+                    <?php
+                    if (isset($loved)) {
+                        $type = $loved ? "filled" : "unfilled";
+                    }
+                    require PUBLIC_PATH . 'assets/icons/love.php' ?>
+                </button>
+            <?php endif; ?>
+            <span data-id="<?= $uuid ?>">
+                <?= $loveCount ?>
+            </span>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/src/server/app/View/components/footer.php b/src/server/app/View/components/footer.php
new file mode 100644
index 0000000000000000000000000000000000000000..308b1d01b6ca1e7ab1b1fa896e6a8497bbcd1a37
--- /dev/null
+++ b/src/server/app/View/components/footer.php
@@ -0,0 +1,2 @@
+</body>
+</html>
diff --git a/src/server/app/View/components/header.php b/src/server/app/View/components/header.php
new file mode 100644
index 0000000000000000000000000000000000000000..7462e99aa2871161367df291d89f8bb259698110
--- /dev/null
+++ b/src/server/app/View/components/header.php
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="<?= $description ?? '#1 Drama and Anime Watch List Website' ?>" />
+
+    <title>
+        <?= 'Drawl | ' . $model['title'] ?? '🌸' ?>
+    </title>
+
+    <!-- CSS -->
+    <link rel="stylesheet" href="/css/global.css">
+    <!-- <link rel="stylesheet" href="/css/components/select.css">
+    <link rel="stylesheet" href="/css/components/card.css">
+    <link rel="stylesheet" href="/css/components/button.css">
+    <link rel="stylesheet" href="/css/components/pagination.css">
+    <link rel="stylesheet" href="/css/components/input.css">
+    <link rel="stylesheet" href="/css/components/form.css">
+    <link rel="stylesheet" href="/css/components/icon.css">
+    <link rel="stylesheet" href="/css/components/textarea.css">
+    <link rel="stylesheet" href="/css/components/modal.css">
+    <link rel='stylesheet' href='/css/components/alert.css'> -->
+
+    <?php foreach ($model['styles'] ?? [] as $style): ?>
+        <link rel='stylesheet' href='<?= $style ?>'>
+    <?php endforeach; ?>
+
+    <!-- JS -->
+    <script type="text/javascript" src="/js/global.js" defer></script>
+    <script type='text/javascript' src='/js/components/navbar.js' defer></script>
+    <script type="text/javascript" src="/js/components/select.js" defer></script>
+    <script type="text/javascript" src="/js/components/modal.js" defer></script>
+    <script type='text/javascript' src='/js/components/alert.js' defer></script>
+
+    <?php foreach ($model['js'] ?? [] as $js): ?>
+        <script type='text/javascript' src='<?= $js ?>' defer></script>
+    <?php endforeach; ?>
+</head>
+
+<body>
\ No newline at end of file
diff --git a/src/server/app/View/components/modal.php b/src/server/app/View/components/modal.php
new file mode 100644
index 0000000000000000000000000000000000000000..e101b95795add34c38d2af23c6e95eb5268d1d31
--- /dev/null
+++ b/src/server/app/View/components/modal.php
@@ -0,0 +1,42 @@
+<!-- 
+Attributes:
+- triggerClasses
+- triggerText
+- triggerIcon
+- ariaLabel
+- title
+- content
+- data
+-->
+
+<div class="modal">
+    <button aria-label="<?= $ariaLabel ?? "" ?>" type="button" class="modal__trigger <?= $triggerClasses ?>"
+        <?php foreach ($data ?? []
+
+        as $key => $val): ?>
+            data-<?= $key ?>>="<?= $val ?>"
+        <?php endforeach; ?>
+        >
+        <?php
+        if (isset($triggerIcon) && $triggerIcon != '') {
+            require PUBLIC_PATH . 'assets/icons/' . $triggerIcon . '.php';
+        }
+        ?>
+        <?= $triggerText ?? "" ?>
+    </button>
+
+    <div id="modal__content" class="modal__backdrop">
+        <div class="modal__content">
+            <h2><?= $title ?></h2>
+            <button class="modal__close btn-ghost">
+                <?php require PUBLIC_PATH . 'assets/icons/x.php' ?>
+            </button>
+
+            <?php
+            if (isset($content)) {
+                require __DIR__ . '/../components/modal/' . $content . '.php';
+            }
+            ?>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/src/server/app/View/components/modal/signInRequired.php b/src/server/app/View/components/modal/signInRequired.php
new file mode 100644
index 0000000000000000000000000000000000000000..38b4a4e0e8086b407c433994bf53cbdb3909a8f6
--- /dev/null
+++ b/src/server/app/View/components/modal/signInRequired.php
@@ -0,0 +1,3 @@
+<a href="/signin" class="btn btn-bold">
+    Sign In
+</a>
diff --git a/src/server/app/View/components/modal/watchlistAddItem.php b/src/server/app/View/components/modal/watchlistAddItem.php
new file mode 100644
index 0000000000000000000000000000000000000000..f5f2efa7a553e62ffdbc077cf40db178a37086cc
--- /dev/null
+++ b/src/server/app/View/components/modal/watchlistAddItem.php
@@ -0,0 +1,11 @@
+<div>
+    <form class="form-default form-search">
+        <div class="search">
+            <label>
+                <?php require PUBLIC_PATH . '/assets/icons/search.php' ?>
+            </label>
+            <input type="text" id="search" name="search" placeholder="Search by title" class="input-default search__input" />
+        </div>
+    </form>
+    <div class="search__items"></div>
+</div>
\ No newline at end of file
diff --git a/src/server/app/View/components/modal/watchlistAddSearchItem.php b/src/server/app/View/components/modal/watchlistAddSearchItem.php
new file mode 100644
index 0000000000000000000000000000000000000000..bb70c30efe22a1eea55452103b473912bed3690f
--- /dev/null
+++ b/src/server/app/View/components/modal/watchlistAddSearchItem.php
@@ -0,0 +1,10 @@
+<div class="search-item" data-page="<?= $page ?>">
+    <img src="<?= '/assets/images/catalogs/posters/' . $poster ?>" class="search-item__poster" />
+    <div class="search-item__content">
+        <h3 class="search-item__title"><?= $title ?></h3>
+        <p class="search-item__description"><?= $description ?></p>
+    </div>
+    <button type="button" class="search-item__action" data-id="<?= $uuid ?>">
+        <?php require PUBLIC_PATH . 'assets/icons/plus.php' ?>
+    </button>
+</div>
\ No newline at end of file
diff --git a/src/server/app/View/components/navbar.php b/src/server/app/View/components/navbar.php
new file mode 100644
index 0000000000000000000000000000000000000000..5238aabb8e4c4c5d20926492c95285af1a0c12cb
--- /dev/null
+++ b/src/server/app/View/components/navbar.php
@@ -0,0 +1,31 @@
+<div class="navbar">
+    <div class="navbar-content">
+        <div class="navbar-header">
+            <div>
+                <a href='/' class="brand__title">Drawl</a>
+            </div>
+            <button aria-label="Open Menu" id="navbar-toggle" class="navbar-toggle" aria-expanded="false"
+                aria-controls="navbar-menu">
+                <?php require PUBLIC_PATH . 'assets/icons/menu.php' ?>
+            </button>
+        </div>
+        <div id="navbar-menu" class="navbar-menu collapsed" aria-labelledby="navbar-toggle">
+            <a href="/" class="btn">Discover</a>
+            <a href="/catalog" class="btn">Catalog</a>
+            <?php if ($user == null): ?>
+                <a href="/signin" class="btn">Sign In</a>
+            <?php else: ?>
+                <button id="profile-menu-toggle" class="profile-icon" aria-label="Open Profile Dropdown"
+                    aria-expanded="false">
+                    <?php require PUBLIC_PATH . 'assets/icons/user.php' ?>
+                </button>
+                <div id="profile-menu" class="profile-menu collapsed" aria-labelledby="profile-menu-toggle">
+                    <a href="/profile" class="btn">Profile</a>
+                    <a href="/profile/watchlist" class="btn">My Watchlist</a>
+                    <a href="/profile/bookmark" class="btn">My Bookmark</a>
+                    <button id="logout" class="btn">Logout</button>
+                </div>
+            <?php endif; ?>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/src/server/app/View/components/pagination.php b/src/server/app/View/components/pagination.php
new file mode 100644
index 0000000000000000000000000000000000000000..4b9be9cc392e2baef308a16aeaac62cc22ce43ff
--- /dev/null
+++ b/src/server/app/View/components/pagination.php
@@ -0,0 +1,34 @@
+<?php
+function createQueryUrl(int $page): string
+{
+    $currentQueryParams = $_GET;
+    $query = array_merge($currentQueryParams, ['page' => $page]);
+    $url = '?' . http_build_query($query);
+    return $url;
+}
+
+?>
+<div class="pagination">
+    <?php if ($currentPage > 1): ?>
+        <a aria-label="Previous page" href="<?= createQueryUrl($currentPage - 1) ?>" class="pagination-item prev">
+            <?php require PUBLIC_PATH . 'assets/icons/chevron-down.php' ?>
+        </a>
+    <?php endif; ?>
+    <?php if (max(0, $currentPage - 2) > 0): ?>
+        <span class="pagination-elips">...</span>
+    <?php endif; ?>
+    <?php for ($i = max(0, $currentPage - 2); $i < min($totalPage, $currentPage + 2); $i++): ?>
+        <a href="<?= createQueryUrl($i + 1) ?>" class="pagination-item"
+           data-type="<?= $i + 1 === $currentPage ? 'active' : 'inactive' ?>">
+            <?= $i + 1 ?>
+        </a>
+    <?php endfor; ?>
+    <?php if (min($totalPage, $currentPage + 2) < $totalPage): ?>
+        <span class="pagination-elips">...</span>
+    <?php endif; ?>
+    <?php if ($currentPage < $totalPage): ?>
+        <a aria-label="Next page" href="<?= createQueryUrl($currentPage + 1) ?>" class="pagination-item next">
+            <?php require PUBLIC_PATH . 'assets/icons/chevron-down.php' ?>
+        </a>
+    <?php endif; ?>
+</div>
\ No newline at end of file
diff --git a/src/server/app/View/components/select.php b/src/server/app/View/components/select.php
new file mode 100644
index 0000000000000000000000000000000000000000..cd8bb7cd6fb51c64372160c8f358619e27d437ed
--- /dev/null
+++ b/src/server/app/View/components/select.php
@@ -0,0 +1,25 @@
+<!-- Attributes
+- id
+- placeholder
+- content
+- selected
+-->
+
+<div class="c-select-menu" data-id="<?= $id ?>">
+    <div class="c-select-btn">
+        <span class="c-select-btn-text"><?= $selected ?? $placeholder ?? 'Select' ?></span>
+        <?php require PUBLIC_PATH . 'assets/icons/chevron-down.php' ?>
+    </div>
+
+    <input type="hidden" id="<?= $id ?>" name="<?= $id ?>" value="<?= $selected ?>"/>
+
+    <?php if (isset($content)) : ?>
+        <ul class="c-select-options c-select-hide">
+            <?php foreach ($content as $item) : ?>
+                <li id="c-select-option-<?= $item ?>" class="c-select-option">
+                    <span class="c-select-option-text"><?= $item ?></span>
+                </li>
+            <?php endforeach; ?>
+        </ul>
+    <?php endif; ?>
+</div>
\ No newline at end of file
diff --git a/src/server/app/View/components/toast.php b/src/server/app/View/components/toast.php
new file mode 100644
index 0000000000000000000000000000000000000000..773d59aca1320702f7b2dd247a5644f078e10394
--- /dev/null
+++ b/src/server/app/View/components/toast.php
@@ -0,0 +1,13 @@
+<div id="toast" class="toast hidden" data-type="<?= $type ?? "error" ?>">
+    <div>
+        <h3>
+            <?= $title ?? "" ?>
+        </h3>
+        <p>
+            <?= $message ?? "" ?>
+        </p>
+    </div>
+    <button id="close" class="btn-ghost">
+        <?php require PUBLIC_PATH . "assets/icons/cancel.php" ?>
+    </button>
+</div>
\ No newline at end of file
diff --git a/src/server/app/View/components/watchlist/watchlistItem.php b/src/server/app/View/components/watchlist/watchlistItem.php
new file mode 100644
index 0000000000000000000000000000000000000000..661477a91d2265f7e8db8660cd574c9628d5bd7b
--- /dev/null
+++ b/src/server/app/View/components/watchlist/watchlistItem.php
@@ -0,0 +1,21 @@
+<!-- <div class="watchlist-item__wrapper"> -->
+<img alt="Poster of <?= $title ?>" src="<?= '/assets/images/catalogs/posters/' . ($poster ?? 'no-poster.webp') ?>"
+    class="watchlist-item__poster">
+<div class="watchlist-item__content">
+    <h3 class="watchlist-item__title">
+        <?= $title ?>
+    </h3>
+    <textarea name="<?= 'item[' . $id . '__' . $uuid . '__' . $category . ']' ?>"
+        class="input-default watchlist-item__description" placeholder="Enter description"
+        maxlength="255"><?= $description ?? "" ?></textarea>
+</div>
+<!-- </div> -->
+<div class="watchlist-item__actions">
+    <button aria-label="Delete <?= $title ?>" type="button" class="btn-ghost watchlist-item__delete"
+        data-id="<?= $uuid ?>">
+        <?php require PUBLIC_PATH . 'assets/icons/trash.php' ?>
+    </button>
+    <span class="span__icon drag-handler">
+        <?php require PUBLIC_PATH . 'assets/icons/grip-vertical.php' ?>
+    </span>
+</div>
\ No newline at end of file
diff --git a/src/server/app/View/home/index.php b/src/server/app/View/home/index.php
new file mode 100644
index 0000000000000000000000000000000000000000..cffdcb3a89d6f213740e2a96c2abdd9dcd9f6e65
--- /dev/null
+++ b/src/server/app/View/home/index.php
@@ -0,0 +1,120 @@
+<?php
+function selectCategory()
+{
+    $id = 'category';
+    $placeholder = 'Select Category';
+    $content = [
+        "MIXED",
+        "DRAMA",
+        "ANIME"
+    ];
+    $selected = validateQueryParams($id, $content);
+    require __DIR__ . '/../components/select.php';
+}
+
+function sortBy()
+{
+    $id = 'sortBy';
+    $placeholder = 'Sort By';
+    $content = [
+        "DATE",
+        "LOVE"
+    ];
+    $selected = validateQueryParams($id, $content);
+    require __DIR__ . '/../components/select.php';
+}
+
+function tags($tags)
+{
+    $id = 'tag';
+    $placeholder = 'Select Tag';
+    $content = $tags;
+    $selected = validateQueryParams($id, $content);
+
+    require __DIR__ . '/../components/select.php';
+}
+
+function vallidateOrder(): ?string
+{
+    if (!isset($_GET["order"]) || ($_GET["order"] != "asc" && $_GET["order"] != "desc"))
+        return null;
+    return $_GET["order"];
+}
+
+function fillLove($item)
+{
+    $type = $item["like_status"] == 1 ? "filled" : "unfilled";
+    require PUBLIC_PATH . 'assets/icons/love.php';
+}
+
+function watchlist($item, $userUUID)
+{
+    $uuid = $item["watchlist_uuid"];
+    $posters = $item["posters"];
+    $visibility = $item["visibility"];
+    $title = $item["title"];
+    $category = $item["category"];
+    $creator = $item["creator"];
+    $createdAt = $item["created_at"];
+    $description = $item["description"];
+    $itemCount = $item["item_count"];
+    $loveCount = $item["love_count"];
+    $loved = $item["loved"];
+    $saved = $item["saved"];
+    $self = ($userUUID == $item["creator_uuid"]);
+
+    require __DIR__ . '/../components/card/watchlistCard.php';
+}
+
+function pagination(int $currentPage, int $totalPage)
+{
+    require __DIR__ . '/../components/pagination.php';
+}
+
+?>
+
+<main>
+    <form class="form-search-filter">
+        <div class="search">
+            <?php require PUBLIC_PATH . 'assets/icons/search.php'; ?>
+            <input type="text" name="search" placeholder="Search title or creator" class="input-default input-search"
+                   value="<?= trim($_GET['search'] ?? '') ?? '' ?>"/>
+        </div>
+        <div class="filter">
+            <?php tags($model["data"]["tags"]); ?>
+            <?php selectCategory(); ?>
+            <div class="filter__sort">
+                <?php sortBy(); ?>
+                <button aria-label="Sort Category" type="button" class="btn-sort">
+                    <span class="span-icon btn-sort-asc <?= vallidateOrder() == 'desc' || !vallidateOrder() ? 'hidden' : '' ?>">
+                        <?php require PUBLIC_PATH . 'assets/icons/asc.php' ?>
+                    </span>
+                    <span class="span-icon btn-sort-desc <?= vallidateOrder() == 'asc' ? 'hidden' : '' ?>">
+                        <?php require PUBLIC_PATH . 'assets/icons/desc.php' ?>
+                    </span>
+                </button>
+                <input type="hidden" id="order" name="order" value="<?= vallidateOrder() ?? 'desc' ?>"/>
+            </div>
+        </div>
+        <button type="submit" id="btn-apply" class="btn-primary btn--apply">Apply</button>
+    </form>
+
+
+    <a class="btn btn-primary" href='/watchlist/create'>
+        <?php require PUBLIC_PATH . 'assets/icons/plus.php' ?>
+        New List
+    </a>
+
+    <div class="list__watchlist">
+        <?php if (count($model["data"]["items"]) == 0) : ?>
+            <div class="loading">No Results Found.</div>
+        <?php endif; ?>
+        <?php foreach ($model["data"]["items"] as $item) : ?>
+            <?php watchlist($item, $model["data"]["userUUID"]); ?>
+        <?php endforeach; ?>
+        <?php if (count($model["data"]["items"]) > 0) : ?>
+            <?php pagination($model["data"]["page"], $model["data"]["pageTotal"]); ?>
+        <?php endif; ?>
+    </div>
+
+</main>
\ No newline at end of file
diff --git a/src/server/app/View/profile/bookmark.php b/src/server/app/View/profile/bookmark.php
new file mode 100644
index 0000000000000000000000000000000000000000..f7fa0ffbfd61c00096eebaab47d7b9958b683893
--- /dev/null
+++ b/src/server/app/View/profile/bookmark.php
@@ -0,0 +1,47 @@
+<?php
+
+function watchlistCard(array $item, string $userUUID, string $loading = "eager")
+{
+    $uuid = $item["watchlist_uuid"];
+    $posters = $item["posters"];
+    $visibility = $item["visibility"];
+    $title = $item["title"];
+    $category = $item["category"];
+    $creator = $item["creator"];
+    $createdAt = $item["created_at"];
+    $description = $item["description"];
+    $itemCount = $item["item_count"];
+    $loveCount = $item["like_count"];
+    $loved = $item["liked"];
+    $self = ($userUUID == $item["creator_uuid"]);
+    $saved = true;
+
+    require __DIR__ . '/../components/card/watchlistCard.php';
+}
+
+function pagination(int $currentPage, int $totalPage)
+{
+    require __DIR__ . '/../components/pagination.php';
+}
+
+?>
+
+<main class="watchlist-self">
+    <section class="search-filter">
+        <h2>My Bookmark</h2>
+    </section>
+    <?php if (count($model['data']['bookmarks']['items']) == 0): ?>
+        <div class="no-item__container">
+            <h1>Oops! 😣</h1>
+            <div>
+                <h2>There's No Bookmark Yet...</h2>
+            </div>
+        </div>
+    <?php endif; ?>
+    <section class="content">
+        <?php for ($i = 0; $i < count($model['data']['bookmarks']['items']); $i++): ?>
+            <?php watchlistCard($model['data']['bookmarks']['items'][$i], $model["data"]["userUUID"], $i < 4 ? "eager" : "lazy"); ?>
+        <?php endfor; ?>
+        <?php pagination($model['data']['bookmarks']['page'], $model['data']['bookmarks']['totalPage']); ?>
+    </section>
+</main>
\ No newline at end of file
diff --git a/src/server/app/View/profile/watchlist.php b/src/server/app/View/profile/watchlist.php
new file mode 100644
index 0000000000000000000000000000000000000000..f8a6768104e58eabe0d4405890126a77720c3a2f
--- /dev/null
+++ b/src/server/app/View/profile/watchlist.php
@@ -0,0 +1,64 @@
+<?php
+
+function watchlistCard(array $item, string $userUUID, string $loading = "eager")
+{
+    $uuid = $item["watchlist_uuid"];
+    $posters = $item["posters"];
+    $visibility = $item["visibility"];
+    $title = $item["title"];
+    $category = $item["category"];
+    $creator = $item["creator"];
+    $createdAt = $item["created_at"];
+    $description = $item["description"];
+    $itemCount = $item["item_count"];
+    $loveCount = $item["like_count"];
+    $loved = $item["liked"];
+    $saved = true;
+    $self = ($userUUID == $item["creator_uuid"]);
+
+    require __DIR__ . '/../components/card/watchlistCard.php';
+}
+
+function pagination(int $currentPage, int $totalPage)
+{
+    require __DIR__ . '/../components/pagination.php';
+}
+
+?>
+
+<main class="watchlist-self">
+    <section class="search-filter">
+        <div>
+            <h2>My Watchlist</h2>
+            <div class="visibility">
+                <a id="visibility-all" href="/profile/watchlist?visibility=all"
+                    class="btn <?= $model['data']['visibility'] === "all" ? "selected" : "" ?>">All</a>
+                <a id="visibility-private" href="/profile/watchlist?visibility=private"
+                    class="btn <?= $model['data']['visibility'] === "private" ? "selected" : "" ?>">Private</a>
+                <a id="visibility-public" href="/profile/watchlist?visibility=public"
+                    class="btn <?= $model['data']['visibility'] === "public" ? "selected" : "" ?>">Public</a>
+            </div>
+        </div>
+        <a href="/watchlist/create" class="btn btn-bold">
+            <span class="icon-new">
+                <?php require PUBLIC_PATH . 'assets/icons/plus.php' ?>
+            </span>
+            Add Watchlist
+        </a>
+    </section>
+    <?php if (count($model['data']['watchlists']['items']) == 0): ?>
+        <div class="no-item__container">
+            <h1>Oops! 😣</h1>
+            <div>
+                <h2>There's No Watchlist Yet...</h2>
+                <p>...Go to <a href="/">Home</a> or <a href="/watchlist/create">create some watchlist!</a></p>
+            </div>
+        </div>
+    <?php endif; ?>
+    <section class="content">
+        <?php for ($i = 0; $i < count($model['data']['watchlists']['items']); $i++): ?>
+            <?php watchlistCard($model['data']['watchlists']['items'][$i], $model["data"]["userUUID"], $i < 4 ? "eager" : "lazy", ); ?>
+        <?php endfor; ?>
+        <?php pagination($model['data']['watchlists']['page'], $model['data']['watchlists']['totalPage']); ?>
+    </section>
+</main>
\ No newline at end of file
diff --git a/src/server/app/View/user/editProfile.php b/src/server/app/View/user/editProfile.php
new file mode 100644
index 0000000000000000000000000000000000000000..300694a9dd25b2cc26d93a670656aa5da0c34199
--- /dev/null
+++ b/src/server/app/View/user/editProfile.php
@@ -0,0 +1,82 @@
+<?php
+function alert($title, $message, $type = 'error')
+{
+    require __DIR__ . '/../components/alert.php';
+}
+
+?>
+
+
+<main>
+    <div class="edit-parameters">
+        <div class="my-profile-container">
+            <h2>
+                <?= $model['data']['name'] ?> - Profile
+            </h2>
+        </div>
+        <?php if (isset($model['error'])): ?>
+            <?php alert('Failed to Update', $model['error']); ?>
+        <?php endif; ?>
+        <?php if (isset($model['success'])): ?>
+            <?php alert('Success', $model['success'], 'success'); ?>
+        <?php endif; ?>
+        <form id="profile-edit-form" class="form-default">
+            <div class="display-name">
+                <h3>Name</h3>
+                <p id="name">
+                    <?= $model['data']['name'] ?>
+                </p>
+            </div>
+            <p>Change name</p>
+            <div class="input-container">
+                <div class="input-box">
+                    <input class="input" name="name" placeholder="Enter new name"
+                        value="<?= $model['data']['name'] ?>" />
+                </div>
+            </div>
+            <div class="display-name">
+                <h3>Email</h3>
+                <p>
+                    <?= $model['data']['email'] ?>
+                </p>
+            </div>
+            <div class="password display-name">
+                <h3>Change Password</h3>
+                <div class="password-button-container">
+                    <div class="password-container">
+                        <div class="password-title">
+                            <div class="password-texts">
+                                <p>Old Password</p>
+                                <div class="red-star">*</div>
+                            </div>
+                            <div class="input-container">
+                                <div class="input-box">
+                                    <input class="input" type="password" name="oldPassword"
+                                        placeholder="Enter old password" />
+                                </div>
+                            </div>
+                        </div>
+                        <div class="password-title">
+                            <div class="password-texts">
+                                <p>New Password</p>
+                                <div class="red-star">*</div>
+                            </div>
+                            <div class="input-container">
+                                <div class="input-box">
+                                    <input class="input" type="password" name="newPassword"
+                                        placeholder="Enter new password" />
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <button id="update-account" class="btn-primary save" name="update_button" type="submit">
+                    save
+                </button>
+                <button id="delete-account" class="btn-bold" type="button">
+                    Delete Account
+                </button>
+        </form>
+    </div>
+    </div>
+</main>
\ No newline at end of file
diff --git a/src/server/app/View/user/signIn.php b/src/server/app/View/user/signIn.php
new file mode 100644
index 0000000000000000000000000000000000000000..e633706dac1291e10d9613bcb16dd42a40cf8078
--- /dev/null
+++ b/src/server/app/View/user/signIn.php
@@ -0,0 +1,41 @@
+<?php
+function alert($title, $message)
+{
+    $type = 'error';
+    require __DIR__ . '/../components/alert.php';
+}
+
+?>
+
+
+<div class="signin-container row">
+    <img src="/assets/images/Suzume.webp" alt="Sign In Image" class="signin-poster" />
+    <div class="right-side">
+        <div class="main-container">
+            <div class="welcome-text">
+                <h2 class="welcome-text__h2">Hello Again!</h2>
+                <p class="welcome-text__h1">Welcome back! Please provide your details</p>
+            </div>
+            <?php if (isset($model['error'])): ?>
+                <?php alert('Failed to Sign in', $model['error']); ?>
+            <?php endif; ?>
+
+            <form class="inputs" action="/signin" method="post">
+                <div class="parameter">
+                    <label for="email" class="input-required">Email</label>
+                    <input type="email" name="email" id="email" class="input-default" placeholder="Enter email"
+                        value="<?= $model['data']['email'] ?? "" ?>">
+                </div>
+                <div class="parameter">
+                    <label for="password" class="input-required">Password</label>
+                    <input type="password" name="password" id="password" class="input-default"
+                        placeholder="Enter password">
+                </div>
+                <button class="btn-bold" type="submit">
+                    Sign In
+                </button>
+                <p>Don't have an account? <a href="/signup" class="signup-link">Sign up</a></p>
+            </form>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/src/server/app/View/user/signUp.php b/src/server/app/View/user/signUp.php
new file mode 100644
index 0000000000000000000000000000000000000000..a9e507ea5650688a549ca6ccd404e140312a2e45
--- /dev/null
+++ b/src/server/app/View/user/signUp.php
@@ -0,0 +1,46 @@
+<?php
+function alert($title, $message)
+{
+    $type = 'error';
+    require __DIR__ . '/../components/alert.php';
+}
+
+?>
+
+<div class="signup-container row">
+    <img src="/assets/images/Tomorrow.webp" alt="Sign Up Image" class="signup-poster" />
+    <div class="right-side">
+        <div class="main-container">
+            <div class="welcome-text">
+                <h2 class="welcome-text__h2">Let's Get Started!</h2>
+                <p class="welcome-text__h1">Glad to see you joining us! Please provide your details</p>
+            </div>
+
+            <?php if (isset($model['error'])): ?>
+                <?php alert('Failed to Sign up', $model['error']); ?>
+            <?php endif; ?>
+
+            <form class="inputs" action="/signup" method="post">
+                <div class="parameter">
+                    <label for="email" class="input-required">Email</label>
+                    <input type="email" name="email" id="email" class="input-default" required
+                        placeholder="Enter email">
+                </div>
+                <div class="parameter">
+                    <label for="password" class="input-required">Password</label>
+                    <input type="password" name="password" id="password" class="input-default" required
+                        placeholder="Enter password" />
+                </div>
+                <div class="parameter">
+                    <label for="passwordConfirm" class="input-required">Confirm Password</label>
+                    <input type="password" name="passwordConfirm" id="passwordConfirm" class="input-default" required
+                        placeholder="Enter confirm password" />
+                </div>
+                <button class="btn-bold" type="submit">
+                    Sign Up
+                </button>
+                <p>Already have an account? <a href="/signin" class="signin-link">Sign in</a></p>
+            </form>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/src/server/app/View/watchlist/createUpdate.php b/src/server/app/View/watchlist/createUpdate.php
new file mode 100644
index 0000000000000000000000000000000000000000..36c9264baacf277326135611bc1bdd8a044f5d5a
--- /dev/null
+++ b/src/server/app/View/watchlist/createUpdate.php
@@ -0,0 +1,111 @@
+<?php
+function visibility($visibility)
+{
+    $id = 'visibility';
+    $content = [
+        "PUBLIC",
+        "PRIVATE"
+    ];
+    $selected = $visibility ?? 'PUBLIC';
+    require __DIR__ . '/../components/select.php';
+}
+
+function addItem()
+{
+    $triggerClasses = "btn-outline btn__add-item";
+    $triggerText = 'Add Item';
+    $triggerIcon = 'plus';
+    $title = 'Search';
+    $content = 'watchlistAddItem';
+    require __DIR__ . '/../components/modal.php';
+}
+
+function getItem()
+{
+    require __DIR__ . '/../components/watchlist/watchlistItem.php';
+}
+
+function alert($title, $message)
+{
+    $type = 'error';
+    require __DIR__ . '/../components/alert.php';
+}
+
+function watchlistItem($poster, $title, $id, $uuid, $category, $description)
+{
+    require __DIR__ . '/../components/watchlist/watchlistItem.php';
+}
+
+?>
+
+<main>
+    <?php if (isset($error))
+        echo $error; ?>
+    <h2 class="title-h2"><?= $model["title"] ?></h2>
+    <?php if (isset($model['error'])) {
+        alert('Failed to ' . $model['title'], $model['error']);
+    } ?>
+    <div class="container__create-watchlist">
+        <div class="container__form">
+            <form id="update-watchlist" class="form-default form__create-watchlist">
+                <div class="form-input-default">
+                    <label for="title" class="input-required">Title</label>
+                    <input type="text" name="title" id="title" class="input-default" placeholder="Best Anime and Drama"
+                           value="<?= $model["data"]["title"] ?? '' ?>" required/>
+                </div>
+
+                <div class="form-input-default">
+                    <label for="description">Description</label>
+                    <textarea name="description" id="description" class="input-default" maxlength="255"
+                              placeholder="Enter your watchlist description"><?= $model["data"]["description"] ?? '' ?></textarea>
+                </div>
+
+                <div class="form-input-default">
+                    <label for="visibility" class="input-required">Visibility</label>
+                    <?php visibility(isset($model["data"]["visibility"]) ? $model["data"]["visibility"] : null); ?>
+                </div>
+
+                <div class="form-input-default">
+                    <label for="tags">Tags</label>
+                    <div class="tags">
+                        <?php foreach ($model["data"]["tags"] ?? [] as $tag): ?>
+                            <?php $selected = false ?>
+                            <?php foreach ($model["data"]["tagsSelected"] ?? [] as $ts): ?>
+                                <?php if ($tag->id == $ts["id"]) $selected = true; ?>
+                            <?php endforeach; ?>
+                            <div class="input-tag">
+                                <label for="tag_<?= $tag->name ?>"><?= $tag->name ?></label>
+                                <input aria-label="tag_<?= $tag->name ?>" type="checkbox" id="tag_<?= $tag->name ?>"
+                                       name="<?= $tag->name ?>"
+                                       value="<?= $tag->id ?>" class="checkbox watchlist-tag"
+                                    <?= $selected ? "checked" : "" ?>/>
+                            </div>
+                        <?php endforeach; ?>
+                    </div>
+                </div>
+
+                <h3 class="watchlist-items__title">Items</h3>
+                <div class="watchlist-items">
+                    <?php foreach ((isset($model["data"]["catalogs"]["items"]) ? $model["data"]["catalogs"]["items"] : []) as $item): ?>
+                        <div class="watchlist-item" draggable="true" data-id="<?= $item["catalog_uuid"] ?>">
+                            <?php watchlistItem($item["poster"], $item["title"], $item["catalog_id"], $item["catalog_uuid"], $item["category"], $item["description"]); ?>
+                        </div>
+                    <?php endforeach; ?>
+                    <?php if (!isset($model["data"]["catalogs"]["items"])): ?>
+                        <p class="items-placeholder">No items selected.</p>
+                    <?php endif; ?>
+                </div>
+
+                <input id="input-submit" type="submit" class="hidden"/>
+            </form>
+
+
+        </div>
+        <div class="actions">
+            <?php addItem(); ?>
+            <label for="input-submit" class="btn btn-bold btn__save">
+                Save
+            </label>
+        </div>
+    </div>
+</main>
\ No newline at end of file
diff --git a/src/server/app/View/watchlist/detail.php b/src/server/app/View/watchlist/detail.php
new file mode 100644
index 0000000000000000000000000000000000000000..c115e58f4a9922ff4fe4413d18bf2ccbba7b73f6
--- /dev/null
+++ b/src/server/app/View/watchlist/detail.php
@@ -0,0 +1,123 @@
+<?php
+
+if (!function_exists("formatDate")) {
+    function formatDate($createdAt)
+    {
+        require __DIR__ . '/../../../config/dateFormat.php';
+    }
+}
+
+function catalogCard($catalog)
+{
+    $title = $catalog['title'];
+    $poster = $catalog['poster'];
+    $category = $catalog['category'];
+    $description = $catalog['description'];
+    $uuid = $catalog['catalog_uuid'];
+    require __DIR__ . '/../components/card/catalogCard.php';
+}
+
+function pagination($currentPage, $totalPage)
+{
+    require __DIR__ . '/../components/pagination.php';
+}
+
+if (!function_exists("likeAndSave")) {
+
+    function likeAndSave($class, $icon, $ariaLabel = "")
+    {
+        $triggerClasses = "btn-ghost $class";
+        $triggerText = "";
+        $triggerIcon = $icon;
+        $title = "Sign In Required";
+        $content = 'signInRequired';
+        require __DIR__ . '/../components/modal.php';
+    }
+}
+
+?>
+
+<main>
+    <article class="header">
+        <div class="detail">
+            <h2>
+                <?php if ($model['data']['item']['visibility'] === "PRIVATE"): ?>
+                    <svg xmlns="http://www.w3.org/2000/svg" width="12" height="14" viewBox="0 0 12 14" fill="none">
+                        <path
+                            d="M9 7H10.05C10.2985 7 10.5 7.20145 10.5 7.45V12.55C10.5 12.7985 10.2985 13 10.05 13H1.95C1.70147 13 1.5 12.7985 1.5 12.55V7.45C1.5 7.20145 1.70147 7 1.95 7H3M9 7V4C9 3 8.4 1 6 1C3.6 1 3 3 3 4V7M9 7H3"
+                            stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+                    </svg>
+                <?php endif; ?>
+                <?= $model['data']['item']['title'] ?>
+            </h2>
+            <div class="container-subtitle">
+                <div class="tag">
+                    <?= $model['data']['item']['category'] ?>
+                </div>
+                <p class="subtitle">
+                    <?= $model['data']['item']['creator'] ?> |
+                    <?= formatDate($model['data']['item']['created_at']) ?>
+                </p>
+            </div>
+            <div class="watchlist__wrapper-type-author">
+                <?php foreach ($model["data"]["item"]["tags"] as $tag): ?>
+                    <span class="tag">
+                        <?= $tag["name"] ?>
+                    </span>
+                <?php endforeach; ?>
+            </div>
+            <p>
+                <?= $model['data']['item']['description'] ?>
+            </p>
+        </div>
+        <div class="container-button">
+            <div class="container-btn-love">
+                <?php if ($model['data']['userUUID'] == ""): ?>
+                    <?php likeAndSave("btn__like", "love", "Love " . $model['data']['item']['title']); ?>
+                <?php else: ?>
+                    <button class="btn-ghost btn__like" data-id="<?= $model["data"]["item"]["watchlist_uuid"] ?>"
+                        data-liked="<?= $model["data"]["item"]["liked"] ?>">
+                        <?php
+                        $type = (isset($model['data']["item"]['liked']) && $model['data']["item"]['liked']) ? "filled" : "unfilled";
+                        require PUBLIC_PATH . 'assets/icons/love.php' ?>
+                    </button>
+                <?php endif; ?>
+                <span data-id="<?= $model["data"]["item"]["watchlist_uuid"] ?>">
+                    <?= $model['data']['item']['like_count'] ?>
+                </span>
+            </div>
+            <div class="container-btn-love">
+                <?php if (!isset($model['data']['userUUID']) || $model['data']['userUUID'] != $model['data']['item']['creator_uuid']): ?>
+                    <?php if ($model['data']['userUUID'] == ""): ?>
+                        <?php likeAndSave("btn__save", "bookmark", "Save " . $model['data']['item']['title']); ?>
+                    <?php else: ?>
+                        <button class="btn-ghost btn__save" data-id="<?= $model["data"]["item"]["watchlist_uuid"] ?>"
+                            data-saved="<?= $model["data"]["item"]["saved"] ?>">
+                            <?php
+                            $type = (isset($model['data']['item']['saved']) && $model['data']['item']['saved']) ? "filled" : "unfilled";
+                            require PUBLIC_PATH . 'assets/icons/bookmark.php' ?>
+                        </button>
+                    <?php endif; ?>
+                <?php endif; ?>
+            </div>
+        </div>
+    </article>
+    <article id="catalogs" class="content">
+        <?php foreach ($model['data']['item']['catalogs']['items'] ?? [] as $catalog): ?>
+            <?php catalogCard($catalog); ?>
+        <?php endforeach; ?>
+        <?php pagination($model['data']['item']['catalogs']['page'], $model['data']['item']['catalogs']['totalPage']); ?>
+    </article>
+    <?php if (isset($model['data']['userUUID']) && $model['data']['userUUID'] === $model['data']['item']['creator_uuid']): ?>
+        <div class="watchlist-detail__button-container">
+            <a href="<?= "/watchlist/" . $model['data']['item']['watchlist_uuid'] . "/edit" ?>" id="edit"
+                aria-label="Edit <?= $model['data']['item']['title'] ?>" class="btn-icon">
+                <?php require PUBLIC_PATH . 'assets/icons/edit.php' ?>
+            </a>
+            <button type="submit" aria-label="Delete <?= $model['data']['item']['title'] ?>"
+                class="dialog-trigger btn-icon btn__delete" data-id="<?= $model["data"]["item"]["watchlist_uuid"] ?>">
+                <?php require PUBLIC_PATH . 'assets/icons/trash.php' ?>
+            </button>
+        </div>
+    <?php endif; ?>
+</main>
\ No newline at end of file
diff --git a/src/server/config/bootstrap.php b/src/server/config/bootstrap.php
new file mode 100644
index 0000000000000000000000000000000000000000..0639fbabe591cf61358741d4fe88b89a0beafeca
--- /dev/null
+++ b/src/server/config/bootstrap.php
@@ -0,0 +1,17 @@
+<?php
+define('PUBLIC_PATH', '/var/www/html/');
+require_once __DIR__ . '/../app/Utils/EnvLoader.php';
+
+// Load env
+(new EnvLoader(__DIR__ . '/../.env'))->load();
+
+if (!function_exists('validateQueryParams')) {
+    function validateQueryParams($id, $content): ?string
+    {
+        if (!isset($content)) return null;
+        if (isset($_GET[$id]) && in_array($_GET[$id], $content, TRUE)) {
+            return $_GET[$id];
+        }
+        return null;
+    }
+}
diff --git a/src/server/config/database.php b/src/server/config/database.php
new file mode 100644
index 0000000000000000000000000000000000000000..b0482b033cdd7ba5fadcc817ba5ac812be11a1d8
--- /dev/null
+++ b/src/server/config/database.php
@@ -0,0 +1,18 @@
+<?php
+
+function getDatabaseConfig(): array
+{
+    return [
+        "database" => [
+            "dev" => [
+                "url" => 'pgsql:host=' . getenv('DB_HOST') . ';port=' . getenv('DB_PORT') . ';dbname=' . getenv('DB_NAME') . ';',
+                'host_url' => 'pgsql:host=' . getenv('DB_HOST') . ';port=' . getenv('DB_PORT') . ';',
+                'host' => getenv('DB_HOST'),
+                'port' => getenv('DB_PORT'),
+                'name' => getenv('DB_NAME'),
+                'username' => getenv('DB_USER'),
+                'password' => getenv('DB_PASSWORD'),
+            ],
+        ],
+    ];
+}
diff --git a/src/server/config/dateFormat.php b/src/server/config/dateFormat.php
new file mode 100644
index 0000000000000000000000000000000000000000..283c188259483e6698c6d298103c1bd4ec9f36fd
--- /dev/null
+++ b/src/server/config/dateFormat.php
@@ -0,0 +1,32 @@
+<?php
+if (!function_exists("timeAgo")) {
+    function timeAgo($timestamp): string
+    {
+        $timestamp = strtotime($timestamp);
+        $currentTimestamp = strtotime(gmdate(DATE_RFC3339));
+        $timeDifference = $currentTimestamp - $timestamp;
+
+        $intervals = array(
+            31536000 => 'year',
+            2592000 => 'month',
+            604800 => 'week',
+            86400 => 'day',
+            3600 => 'hour',
+            60 => 'minute',
+            1 => 'second',
+        );
+
+        foreach ($intervals as $seconds => $label) {
+            $quotient = $timeDifference / $seconds;
+            if ($quotient >= 1) {
+                $rounded = round($quotient);
+                $plural = ($rounded > 1) ? 's' : '';
+                return $rounded . ' ' . $label . $plural . ' ago';
+            }
+        }
+
+        return 'just now';
+    }
+}
+
+echo timeAgo($createdAt);
\ No newline at end of file
diff --git a/src/server/routes/view.php b/src/server/routes/view.php
new file mode 100644
index 0000000000000000000000000000000000000000..850d114035dbdc88c0174227ae6e11552fc51d30
--- /dev/null
+++ b/src/server/routes/view.php
@@ -0,0 +1,63 @@
+<?php
+require_once __DIR__ . "/../app/App/Router.php";
+
+require_once __DIR__ . "/../app/Controller/HomeController.php";
+require_once __DIR__ . "/../app/Controller/UserController.php";
+require_once __DIR__ . "/../app/Controller/CatalogController.php";
+require_once __DIR__ . '/../app/Controller/WatchlistController.php';
+require_once __DIR__ . '/../app/Controller/ErrorPageController.php';
+require_once __DIR__ . '/../app/Controller/BookmarkController.php';
+
+require_once __DIR__ . '/../app/Middleware/UserAuthMiddleware.php';
+require_once __DIR__ . '/../app/Middleware/AdminAuthMiddleware.php';
+require_once __DIR__ . '/../app/Middleware/UserAuthApiMiddleware.php';
+require_once __DIR__ . '/../app/Middleware/AdminAuthApiMiddleware.php';
+
+
+// Register routes
+// Home controllers
+Router::add('GET', '/', HomeController::class, 'index', []);
+Router::add("GET", "/api/watchlists", HomeController::class, 'watchlists', []);
+
+// User controllers
+Router::add('GET', '/signup', UserController::class, 'signUp', []);
+Router::add('POST', '/signup', UserController::class, 'postSignUp', []);
+Router::add('GET', '/signin', UserController::class, 'signIn', []);
+Router::add('POST', '/signin', UserController::class, 'postSignIn', []);
+Router::add('POST', '/api/auth/logout', UserController::class, 'logout', [UserAuthMiddleware::class]);
+Router::add('DELETE', '/api/auth/delete', UserController::class, 'delete', [UserAuthMiddleware::class]);
+Router::add('PUT', '/api/auth/update', UserController::class, 'update', [UserAuthMiddleware::class]);
+
+// Catalog controllers
+Router::add('GET', '/catalog', CatalogController::class, 'index', []);
+Router::add('GET', '/catalog/create', CatalogController::class, 'create', [AdminAuthMiddleware::class]);
+Router::add('GET', '/catalog/([A-Za-z0-9\-]*)', CatalogController::class, 'detail', []);
+Router::add('GET', '/catalog/([A-Za-z0-9\-]*)/edit', CatalogController::class, 'edit', [AdminAuthMiddleware::class]);
+Router::add('POST', '/api/catalog/create', CatalogController::class, 'postCreate', [AdminAuthMiddleware::class]);
+Router::add('GET', '/api/catalog', CatalogController::class, "search", [UserAuthApiMiddleware::class]);
+Router::add("DELETE", "/api/catalog/([A-Za-z0-9\-]*)/delete", CatalogController::class, "delete", [AdminAuthMiddleware::class]);
+Router::add("POST", '/api/catalog/([A-Za-z0-9\-]*)/update', CatalogController::class, 'update', [AdminAuthMiddleware::class]);
+
+// Watchlist controllers
+Router::add("GET", "/watchlist/create", WatchlistController::class, 'create', [UserAuthMiddleware::class]);
+Router::add("GET", "/watchlist/([A-Za-z0-9]*)/edit", WatchlistController::class, "edit", [UserAuthMiddleware::class]);
+Router::add("GET", "/watchlist/([A-Za-z0-9]*)", WatchlistController::class, 'detail', []);
+
+Router::add("POST", "/api/watchlist", WatchlistController::class, "postCreate", [UserAuthApiMiddleware::class]);
+Router::add("PUT", "/api/watchlist", WatchlistController::class, "putEdit", [UserAuthApiMiddleware::class]);
+Router::add("DELETE", "/api/watchlist", WatchlistController::class, "delete", [UserAuthApiMiddleware::class]);
+Router::add("GET", "/api/watchlist/item", WatchlistController::class, 'item', [UserAuthApiMiddleware::class]);
+Router::add("POST", "/api/watchlist/like", WatchlistController::class, "like", [UserAuthApiMiddleware::class]);
+Router::add("POST", "/api/watchlist/save", WatchlistController::class, "bookmark", [UserAuthApiMiddleware::class]);
+
+
+Router::add('GET', '/profile', UserController::class, 'showEditProfile', [UserAuthMiddleware::class]);
+Router::add('GET', '/profile/bookmark', BookmarkController::class, 'self', [UserAuthMiddleware::class]);
+Router::add('GET', '/profile/watchlist', WatchlistController::class, 'self', [UserAuthMiddleware::class]);
+
+// Error page
+Router::add('GET', '/404', ErrorPageController::class, 'fourohfour', []);
+Router::add('GET', '/500', ErrorPageController::class, 'fivehundred', []);
+
+// Execute
+Router::run();
\ No newline at end of file