From 0ed810ac5011a4552780018014db6c9ea2b9a7ec Mon Sep 17 00:00:00 2001 From: Nicolas Jegat Date: Thu, 8 Oct 2026 17:50:21 +0200 Subject: [PATCH 01/12] Create not liked profile table (DB) and add security to like --- .../migrations/016_create_table_passes.sql | 11 ++++++++++ api/src/Controllers/LikeController.php | 20 ++++++++----------- 2 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 api/database/migrations/016_create_table_passes.sql diff --git a/api/database/migrations/016_create_table_passes.sql b/api/database/migrations/016_create_table_passes.sql new file mode 100644 index 00000000..74ea779a --- /dev/null +++ b/api/database/migrations/016_create_table_passes.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS passes ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + target_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + PRIMARY KEY (user_id, target_id), + CONSTRAINT passes_no_self_pass CHECK (user_id <> target_id) +); + +CREATE INDEX IF NOT EXISTS passes_target_id_idx ON passes (target_id); +CREATE INDEX IF NOT EXISTS passes_created_at_idx ON passes (created_at); diff --git a/api/src/Controllers/LikeController.php b/api/src/Controllers/LikeController.php index f91c5b7e..40065b1d 100644 --- a/api/src/Controllers/LikeController.php +++ b/api/src/Controllers/LikeController.php @@ -9,26 +9,22 @@ class LikeController { public function store(string $username) { - $user = User::find([ - 'username' => $username, - ]); - - if (is_null($user)) { - Flight::json([ - 'message' => 'User not found', - ], 404); + $target = User::find(['username' => $username]); + $me = Flight::user(); + if (is_null($target) || $target->isBlocking($me)) { + Flight::json(['message' => 'User not found'], 404) return; } - Flight::user()->like($user); + Flight::user()->like($target); - Flight::notifier()->publish("user/" . $user->id . "/notifications", [ + Flight::notifier()->publish("user/" . $target->id . "/notifications", [ 'type' => 'like', - 'message' => Flight::user()->username . " a aimé ton profil", + 'message' => Flight::user()->username . " has liked your profile", ], private: false); - Flight::json([], 203); + Flight::json([], 201); } public function destroy(string $username) From 7763c13353b4d8693bb4fe80444dd8e55ed1bb08 Mon Sep 17 00:00:00 2001 From: Nicolas Jegat Date: Fri, 9 Oct 2026 14:38:25 +0200 Subject: [PATCH 02/12] Create Pass controller/DB to users skip other one --- .../migrations/016_create_table_passes.sql | 18 ++++++++----- .../migrations/017_alter_preferences.sql | 3 +++ api/src/Controllers/PassController.php | 23 ++++++++++++++++ api/src/Exceptions/AutoPassException.php | 15 +++++++++++ api/src/Model/Pass.php | 26 +++++++++++++++++++ api/src/Model/User.php | 22 +++++++++++++++- api/src/routes.php | 3 +++ 7 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 api/database/migrations/017_alter_preferences.sql create mode 100644 api/src/Controllers/PassController.php create mode 100644 api/src/Exceptions/AutoPassException.php create mode 100644 api/src/Model/Pass.php diff --git a/api/database/migrations/016_create_table_passes.sql b/api/database/migrations/016_create_table_passes.sql index 74ea779a..607bd87d 100644 --- a/api/database/migrations/016_create_table_passes.sql +++ b/api/database/migrations/016_create_table_passes.sql @@ -1,11 +1,15 @@ -CREATE TABLE IF NOT EXISTS passes ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - target_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), +CREATE TABLE passes ( + user_id INT NOT NULL, + target_id INT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, target_id), + INDEX passes_target_id_idx (target_id), + INDEX passes_created_at_idx (created_at), + + CONSTRAINT passes_user_fk + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT passes_target_fk + FOREIGN KEY (target_id) REFERENCES users(id) ON DELETE CASCADE, CONSTRAINT passes_no_self_pass CHECK (user_id <> target_id) ); - -CREATE INDEX IF NOT EXISTS passes_target_id_idx ON passes (target_id); -CREATE INDEX IF NOT EXISTS passes_created_at_idx ON passes (created_at); diff --git a/api/database/migrations/017_alter_preferences.sql b/api/database/migrations/017_alter_preferences.sql new file mode 100644 index 00000000..6d1ac6df --- /dev/null +++ b/api/database/migrations/017_alter_preferences.sql @@ -0,0 +1,3 @@ +ALTER TABLE `preferences` + ADD COLUMN `fame_gap` INTEGER DEFAULT 0, + DROP COLUMN `is_custom_loc`; diff --git a/api/src/Controllers/PassController.php b/api/src/Controllers/PassController.php new file mode 100644 index 00000000..73685f7f --- /dev/null +++ b/api/src/Controllers/PassController.php @@ -0,0 +1,23 @@ + $username]); + + if (is_null($target)) { + Flight::json(['message' => 'User not found'], 404); + return; + } + + Flight::user()->pass($target); + + Flight::json([], 204); + } +} diff --git a/api/src/Exceptions/AutoPassException.php b/api/src/Exceptions/AutoPassException.php new file mode 100644 index 00000000..b161eb72 --- /dev/null +++ b/api/src/Exceptions/AutoPassException.php @@ -0,0 +1,15 @@ + $this->$target_id]); + } + + public function user(): User + { + return User::find(['id' => $this->user_id]); + } +} diff --git a/api/src/Model/User.php b/api/src/Model/User.php index 5b0ec6f7..c492e6ea 100644 --- a/api/src/Model/User.php +++ b/api/src/Model/User.php @@ -6,6 +6,7 @@ use Flight; use Matcha\Api\Builder\JoinBuilder; use Matcha\Api\Exceptions\AutoLikeException; +use Matcha\Api\Exceptions\AutoPassException; use Matcha\Api\Validator\Asserts\Email; use Matcha\Api\Validator\Asserts\Minimum; use Matcha\Api\Validator\Asserts\NotBlank; @@ -105,6 +106,25 @@ public function getAvatar(): ?string return "https://" . trim(getenv('APP_HOST') ?? 'localhost', '/') . "/api/medias/p/" . $photo->name; } + /** + * Create a pass from the user to the user passed as a parameter + * + * @param User $user + * @return void + */ + public function pass(User $user): void + { + if ($this->id === $user->id) { + throw new AutoPassException(); + } + + $pass = new Pass(); + $pass->user_id = $this->id; + $pass->target_id = $user->id; + + $pass->save(); + } + /** * Create a like from the user to the user passed as a parameter * @@ -277,7 +297,7 @@ public function removeTag(string $name): void { $stmt = Flight::db()->prepare(" DELETE FROM user_tags - WHERE `user_id` = :user_id + WHERE `user_id` = :user_id AND `tag_id` = (SELECT tags.id FROM tags WHERE tags.name = :name); "); diff --git a/api/src/routes.php b/api/src/routes.php index be78dbca..492d6420 100644 --- a/api/src/routes.php +++ b/api/src/routes.php @@ -7,6 +7,7 @@ use Matcha\Api\Controllers\ForgotController; use Matcha\Api\Controllers\HistoryController; use Matcha\Api\Controllers\LikeController; +use Matcha\Api\Controllers\PassController; use Matcha\Api\Controllers\LocalisationController; use Matcha\Api\Controllers\Notifications\NotificationsController; use Matcha\Api\Controllers\Profile\PreferencesController; @@ -56,6 +57,8 @@ Flight::route('POST /like', [LikeController::class, 'store']); Flight::route('DELETE /unlike', [LikeController::class, 'destroy']); + Flight::route('POST /pass', [PassController::class, 'store']); + Flight::route('POST /block', [UserBlockController::class, 'store']); Flight::route('DELETE /unblock', [UserBlockController::class, 'destroy']); From 152838f0df36c6b2ac65e1aa95370243e540ee7d Mon Sep 17 00:00:00 2001 From: Nicolas Jegat Date: Fri, 9 Oct 2026 16:29:11 +0200 Subject: [PATCH 03/12] Create suggestion profiles (backend) with filters and sorts --- api/src/Controllers/SuggestionController.php | 45 +++++++ api/src/Model/Preference.php | 2 +- api/src/Model/User.php | 121 +++++++++++++++++++ api/src/routes.php | 6 +- 4 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 api/src/Controllers/SuggestionController.php diff --git a/api/src/Controllers/SuggestionController.php b/api/src/Controllers/SuggestionController.php new file mode 100644 index 00000000..20c692d0 --- /dev/null +++ b/api/src/Controllers/SuggestionController.php @@ -0,0 +1,45 @@ +query; + + $sort = $query['sort'] ?? null; + $order = strtolower($query['order'] ?? 'desc'); + $offset = (int) ($query['offset'] ?? 0); + $limit = (int) ($query['limit'] ?? 20); + + if ($sort !== null && !in_array($sort, self::SORTS, true)) { + Flight::json(['message' => 'Invalid sort'], 422); + return; + } + + if (!in_array($order, ['asc', 'desc'], true)) { + Flight::json(['message' => 'Invalid order'], 422); + return; + } + + if ($offset < 0 || $limit < 1 || $limit > self::MAX_LIMIT) { + Flight::json(['message' => 'Invalid pagination'], 422); + return; + } + + $result = Flight::user()->suggestions($sort, $order, $offset, $limit); + + Flight::json([ + 'count' => count($result['profiles']), + 'has_more' => $result['has_more'], + 'profiles' => $result['profiles'], + ]); + } +} diff --git a/api/src/Model/Preference.php b/api/src/Model/Preference.php index b5fd8aa1..88e26fcd 100644 --- a/api/src/Model/Preference.php +++ b/api/src/Model/Preference.php @@ -32,7 +32,7 @@ class Preference extends Model public float $lon = 0; - public bool $is_custom_loc = false; + public int $fame_gap = 10; public function user(): User { diff --git a/api/src/Model/User.php b/api/src/Model/User.php index c492e6ea..82cee90c 100644 --- a/api/src/Model/User.php +++ b/api/src/Model/User.php @@ -7,11 +7,13 @@ use Matcha\Api\Builder\JoinBuilder; use Matcha\Api\Exceptions\AutoLikeException; use Matcha\Api\Exceptions\AutoPassException; +use Matcha\Api\Resources\ProfileResource; use Matcha\Api\Validator\Asserts\Email; use Matcha\Api\Validator\Asserts\Minimum; use Matcha\Api\Validator\Asserts\NotBlank; use Matcha\Api\Validator\Asserts\Regex; use PDO; +use DateTimeImmutable; /** * @method static User find(array $data) @@ -385,4 +387,123 @@ public static function authenticate(string $username, string $password): User|fa return false; } + /** + * Suggested profiles for this user, based on their stored preferences. + * + * @param string|null $sort distance|fame|common_tags|age, null for the default order + * @param string $order asc|desc (ignored when $sort is null) + * @return array + */ + public function suggestions(?string $sort, string $order, int $offset, int $limit): array + { + $prefs = Preference::find(['user_id' => $this->id]); + + // --- Paramètres calculés une seule fois --- + $distMax = (int) $prefs->distance_maximum; + $lat = (float) $prefs->lat; + $lon = (float) $prefs->lon; + + $today = new DateTimeImmutable('today'); + $ageMin = (int) ($prefs->age_minimum ?? 18); + $ageMax = (int) ($prefs->age_maximum ?? 100); + $birthMax = $today->modify("-{$ageMin} years")->format('Y-m-d'); + $birthMin = $today->modify('-' . ($ageMax + 1) . ' years')->format('Y-m-d'); + + $deltaLat = $distMax / 111.2; + $deltaLon = $distMax / (111.2 * max(cos(deg2rad($lat)), 0.01)); + + // fame_gap = 0 signifie "désactivé" + $fameGap = (int) $prefs->fame_gap > 0 ? (int) $prefs->fame_gap : 1000000; + + $fetch = $limit + 1; + + $dir = $order === 'asc' ? 'ASC' : 'DESC'; + $orderBy = match ($sort) { + 'distance' => "distance_km $dir, id ASC", + 'fame' => "fame_rating $dir, id ASC", + 'common_tags' => "common_tags $dir, id ASC", + 'age' => 'birthday ' . ($order === 'asc' ? 'DESC' : 'ASC') . ', id ASC', + default => $prefs->by_tags + ? 'tier ASC, common_tags DESC, fame_rating DESC, id ASC' + : 'tier ASC, fame_rating DESC, id ASC', + }; + + $sql = " + WITH candidates AS ( + SELECT + u.*, + ST_Distance_Sphere(POINT(p.lon, p.lat), POINT(:me_lon, :me_lat)) / 1000 AS distance_km, + (SELECT COUNT(*) FROM user_tags ut + JOIN user_tags mine ON mine.tag_id = ut.tag_id AND mine.user_id = :me_id_tags + WHERE ut.user_id = u.id) AS common_tags + FROM users u + JOIN preferences p ON p.user_id = u.id + WHERE u.id <> :me_id + AND u.email_verified = 1 + AND (:me_pref = 'A' OR :me_pref2 = u.gender) + AND (p.sexual_preferences = 'A' OR p.sexual_preferences = :me_gender) + AND u.birthday <= :birth_max + AND u.birthday > :birth_min + AND p.lat BETWEEN :lat_min AND :lat_max + AND p.lon BETWEEN :lon_min AND :lon_max + AND ABS(u.fame_rating - :me_fame) <= :fame_gap + AND NOT EXISTS (SELECT 1 FROM likes l + WHERE l.user_id = :me_id_l AND l.liked_id = u.id) + AND NOT EXISTS (SELECT 1 FROM passes ps + WHERE ps.user_id = :me_id_p AND ps.target_id = u.id + AND ps.created_at > NOW() - INTERVAL 7 DAY) + AND NOT EXISTS (SELECT 1 FROM user_blocked b + WHERE (b.user_id = :me_id_b1 AND b.blocked_id = u.id) + OR (b.user_id = u.id AND b.blocked_id = :me_id_b2)) + ) + SELECT + *, + ROUND(distance_km) AS distance_km, + CASE WHEN distance_km < 10 THEN 0 + WHEN distance_km < 50 THEN 1 + ELSE 2 END AS tier + FROM candidates + WHERE distance_km <= :dist_max + ORDER BY $orderBy + LIMIT $fetch OFFSET $offset + "; + + $stmt = Flight::db()->prepare($sql); + $stmt->execute([ + 'me_lon' => $lon, + 'me_lat' => $lat, + 'me_id_tags' => $this->id, + 'me_id' => $this->id, + 'me_pref' => $prefs->sexual_preferences, + 'me_pref2' => $prefs->sexual_preferences, + 'me_gender' => $this->gender, + 'birth_max' => $birthMax, + 'birth_min' => $birthMin, + 'lat_min' => $lat - $deltaLat, + 'lat_max' => $lat + $deltaLat, + 'lon_min' => $lon - $deltaLon, + 'lon_max' => $lon + $deltaLon, + 'me_fame' => $this->fame_rating, + 'fame_gap' => $fameGap, + 'me_id_l' => $this->id, + 'me_id_p' => $this->id, + 'me_id_b1' => $this->id, + 'me_id_b2' => $this->id, + 'dist_max' => $distMax, + ]); + + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + + if (empty($rows)) { + return []; + } + + $hasMore = count($rows) > $limit; + $rows = array_slice($rows, 0, $limit); + + $rows = array_map(fn ($r) => User::morph($r), $rows); + + return ['profiles' => ProfileResource::collection($rows), 'has_more' => $hasMore]; + } } diff --git a/api/src/routes.php b/api/src/routes.php index 492d6420..96602687 100644 --- a/api/src/routes.php +++ b/api/src/routes.php @@ -12,7 +12,8 @@ use Matcha\Api\Controllers\Notifications\NotificationsController; use Matcha\Api\Controllers\Profile\PreferencesController; use Matcha\Api\Controllers\Profile\ProfileController; -use Matcha\Api\Controllers\Profile\ProfileSuggestionController; +// use Matcha\Api\Controllers\Profile\ProfileSuggestionController; +use Matcha\Api\Controllers\SuggestionController; use Matcha\Api\Controllers\Profile\ReportController; use Matcha\Api\Controllers\Profile\UserBlockController; use Matcha\Api\Controllers\RefreshTokenController; @@ -82,7 +83,8 @@ Flight::route('GET /preferences', [PreferencesController::class, 'index']); Flight::route('GET /history', [HistoryController::class, 'index']); - Flight::route('GET /suggestions', [ProfileSuggestionController::class, 'index']); + // Flight::route('GET /suggestions', [ProfileSuggestionController::class, 'index']); + Flight::route('GET /suggestions', [SuggestionController::class, 'index']); Flight::route('GET /blocks', [UserBlockController::class, 'index']); Flight::route('POST /notifications/@id/view', [NotificationsController::class, 'viewed']); From ea00b0fd7288d1248a843542802fbd110749996e Mon Sep 17 00:00:00 2001 From: Nicolas Jegat Date: Fri, 9 Oct 2026 17:00:37 +0200 Subject: [PATCH 04/12] Fix bug with empty suggestion profiles array --- api/src/Model/User.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/Model/User.php b/api/src/Model/User.php index 82cee90c..d3523dbb 100644 --- a/api/src/Model/User.php +++ b/api/src/Model/User.php @@ -496,7 +496,7 @@ public function suggestions(?string $sort, string $order, int $offset, int $limi if (empty($rows)) { - return []; + return ['profiles' => [], 'has_more' => false]; } $hasMore = count($rows) > $limit; From c11779fc5a583f0da47e8aa6c78493e58b493108 Mon Sep 17 00:00:00 2001 From: Nicolas Jegat Date: Fri, 9 Oct 2026 20:06:02 +0200 Subject: [PATCH 05/12] create frontend function to suggestion call --- frontend/src/api/users.ts | 16 ++++++++++++++++ frontend/src/composable/useUsers.ts | 18 ++++++++++++++++++ frontend/src/views/HomeView.vue | 11 +++++++++-- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index 0a39bfec..84717383 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -22,6 +22,16 @@ export interface Profile { email?: string } +export interface SuggestionParams { + sort?: string +} + +export interface SuggestionProfile { + profiles: Profile[] + count: number + has_more: boolean +} + export const usersApi = { // --- View --- async getProfile(username: string) { @@ -61,4 +71,10 @@ export const usersApi = { res.json(), ) }, + + async suggestion(params: SuggestionParams) { + return ApiClient.get(`/users/me/suggestions`, params).then(res => + res.json(), + ) + }, } diff --git a/frontend/src/composable/useUsers.ts b/frontend/src/composable/useUsers.ts index b9fe64af..1842aab0 100644 --- a/frontend/src/composable/useUsers.ts +++ b/frontend/src/composable/useUsers.ts @@ -1,5 +1,6 @@ import { ref } from 'vue' import { usersApi } from '@/api/users' +import type { Profile, SuggestionParams } from '@/api/users' export function useUsers() { const isLoading = ref(false) @@ -7,6 +8,7 @@ export function useUsers() { const userProfile = ref(null) const blockedUsers = ref([]) + const suggestedProfiles = ref([]) // --- Consultation --- const fetchProfile = async (username: string) => { @@ -120,12 +122,27 @@ export function useUsers() { } } + const suggestion = async (params: SuggestionParams) => { + isLoading.value = true + error.value = null + try { + const data = await usersApi.suggestion(params); + suggestedProfiles.value = data.profiles; + } catch (err: any) { + error.value = + err.response?.data?.message || 'suggestion error' + } finally { + isLoading.value = false + } + } + return { // États réactifs isLoading, error, userProfile, blockedUsers, + suggestedProfiles, // Méthodes fetchProfile, @@ -136,5 +153,6 @@ export function useUsers() { block, unblock, report, + suggestion, } } diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 3def97db..b2fbf6b3 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -8,6 +8,9 @@ import Filter from '@/components/main/Filter.vue' import { EffectCreative } from 'swiper/modules' import type { Swiper as SwiperType } from 'swiper/types' import type { Profile } from '@/api/users' +import { useUsers } from '@/composable/useUsers' + +const users = useUsers() const swiperInstance = ref(null) const sections = ref([ @@ -77,9 +80,13 @@ const goToNextSlide = () => { // } // }) -onMounted(() => { +onMounted(async () => { // getSocket().emit('browsing') // getSocket().emit('browsing') + + await users.suggestion({}) + sections.value = users.suggestedProfiles.value; + console.log(users.suggestedProfiles.value) }) @@ -108,7 +115,7 @@ onMounted(() => { @slideChange="onSlideChange" > From 11d791dd5d9fc9e7cfcba8add751ead2b9a133c1 Mon Sep 17 00:00:00 2001 From: Nicolas Jegat Date: Mon, 12 Oct 2026 19:47:48 +0200 Subject: [PATCH 06/12] Fix bug, add sort dropdown (front) and add values in preferences interface --- Makefile | 4 +- .../Profile/ProfileSuggestionController.php | 74 ---------- api/src/Model/User.php | 41 +++++- api/src/Resources/PreferencesResource.php | 2 +- frontend/src/api/preferences.ts | 2 +- frontend/src/api/users.ts | 3 + frontend/src/components/SortDropdown.vue | 130 ++++++++++++++++++ .../sections/MatchingPreferencesSection.vue | 9 +- .../ui/toggle-group/ToggleGroup.vue | 49 +++++++ .../ui/toggle-group/ToggleGroupItem.vue | 45 ++++++ .../src/components/ui/toggle-group/index.ts | 2 + frontend/src/components/ui/toggle/Toggle.vue | 35 +++++ frontend/src/components/ui/toggle/index.ts | 27 ++++ frontend/src/views/HomeView.vue | 49 +------ 14 files changed, 341 insertions(+), 131 deletions(-) delete mode 100644 api/src/Controllers/Profile/ProfileSuggestionController.php create mode 100644 frontend/src/components/SortDropdown.vue create mode 100644 frontend/src/components/ui/toggle-group/ToggleGroup.vue create mode 100644 frontend/src/components/ui/toggle-group/ToggleGroupItem.vue create mode 100644 frontend/src/components/ui/toggle-group/index.ts create mode 100644 frontend/src/components/ui/toggle/Toggle.vue create mode 100644 frontend/src/components/ui/toggle/index.ts diff --git a/Makefile b/Makefile index 66a47df3..9ed971ad 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ DC := docker compose -all: set_app_host up +all: up up: $(DC) up --build @@ -26,4 +26,4 @@ set_app_host: echo "\nAPP_HOST=$(shell hostname -i)" >> .env; \ fi -.PHONY: all re clean fclean up down \ No newline at end of file +.PHONY: all re clean fclean up down diff --git a/api/src/Controllers/Profile/ProfileSuggestionController.php b/api/src/Controllers/Profile/ProfileSuggestionController.php deleted file mode 100644 index 4fda2521..00000000 --- a/api/src/Controllers/Profile/ProfileSuggestionController.php +++ /dev/null @@ -1,74 +0,0 @@ -getSuggestions($user); - - $preferences = $user->getPreferences(); - - - $users = array_filter($users, function ($value) use ($user, $preferences) { - $userPreferences = $value->getPreferences(); - return $this->inLocation($preferences->lat, $preferences->lon, $userPreferences->lat, $userPreferences->lon, $preferences->distance_maximum) - && !$user->isBlocking($value); - }); - - $tags = $user->getTags(); - - usort($users, function (User $a, User $b) use ($tags) { - return count(array_diff($tags, $b->getTags())) - count(array_diff($tags, $a->getTags())); - }); - - Flight::json( - ProfileResource::collection($users) - ); - } - - /** - * @return User[] - * @throws ReflectionException - */ - private function getSuggestions(User $user): array - { - $preferences = $user->getPreferences(); - - $sexualPreference = $preferences->sexual_preferences == 'A' ? - "users.gender IN ('M', 'F', 'O')" : - "users.gender = '$preferences->sexual_preferences'"; - - $users = User::where([ - ['users.gender', 'IN', "('M', 'F', 'O')"] - ]) - ->join('preferences', function (JoinBuilder $builder) use ($user) { - $builder - ->and('preferences.user_id', '=', 'users.id') - ->and('preferences.sexual_preferences', 'IN', "('A', '$user->gender')"); - }) - ->join('likes', function (JoinBuilder $builder) { - $builder->and('likes.user_id', '=', 'users.id'); - }) - ->groupBy('users.username'); - - $users = $users->get(true); - - return $users; - } - - private function inLocation($lat1, $lon1, $lat2, $lon2, $rayon): bool - { - return haversine($lat1, $lon1, $lat2, $lon2) <= $rayon; - } -} diff --git a/api/src/Model/User.php b/api/src/Model/User.php index 1df08e8e..e3c1ee8e 100644 --- a/api/src/Model/User.php +++ b/api/src/Model/User.php @@ -105,7 +105,7 @@ public function getAvatar(): ?string return null; } - return "https://" . trim(getenv('APP_HOST') ?? 'localhost', '/') . "/api/medias/p/" . $photo->name; + return "http://" . trim(getenv('APP_HOST') ?? 'localhost', '/') . ":8011/api/medias/p/" . $photo->name; } /** @@ -268,16 +268,45 @@ public function getPhotosUrl(): array $host = getenv('APP_HOST') ?: "localhost"; - return array_map(fn (Photo $photo) => "https://" . trim($host, '/') . "/api/medias/p/" . $photo->name, $photos); + return array_map(fn (Photo $photo) => "http://" . trim($host, '/') . ":8011/api/medias/p/" . $photo->name, $photos); } + public function getAge(): ?int { - $birthDate = explode("-", $this->birthday); + if (empty($this->birthday)) { + return null; + } - return (date("md", date("U", mktime(0, 0, 0, $birthDate[2], $birthDate[1], $birthDate[0]))) > date("md") - ? ((date("Y") - $birthDate[0]) - 1) - : (date("Y") - $birthDate[0])); + try { + $birthDate = DateTimeImmutable::createFromFormat( + '!Y-m-d', + $this->birthday + ); + + $errors = DateTimeImmutable::getLastErrors(); + + if ( + $birthDate === false + || ($errors !== false && ( + $errors['warning_count'] > 0 + || $errors['error_count'] > 0 + )) + || $birthDate->format('Y-m-d') !== $this->birthday + ) { + return null; + } + + $today = new DateTimeImmutable('today'); + + if ($birthDate > $today) { + return null; + } + + return $birthDate->diff($today)->y; + } catch (Throwable) { + return null; + } } /** diff --git a/api/src/Resources/PreferencesResource.php b/api/src/Resources/PreferencesResource.php index d667fc2e..9e9cab01 100644 --- a/api/src/Resources/PreferencesResource.php +++ b/api/src/Resources/PreferencesResource.php @@ -25,7 +25,7 @@ public function jsonSerialize(): array 'sexual_preferences' => $this->model->sexual_preferences, 'lat' => $this->model->lat, 'lon' => $this->model->lon, - 'is_custom_loc' => $this->model->is_custom_loc, + 'fame_gap' => $this->model->fame_gap, ]; } } diff --git a/frontend/src/api/preferences.ts b/frontend/src/api/preferences.ts index 09c67676..03e3f169 100644 --- a/frontend/src/api/preferences.ts +++ b/frontend/src/api/preferences.ts @@ -10,7 +10,7 @@ export interface UserPreferences { sexual_preferences: SexualPreference lat: number lon: number - is_custom_loc: boolean + fame_gap: number } export const apiPreferences = { diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index 84717383..60c221f5 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -24,6 +24,9 @@ export interface Profile { export interface SuggestionParams { sort?: string + order?: string + offset?: number + limit?: number } export interface SuggestionProfile { diff --git a/frontend/src/components/SortDropdown.vue b/frontend/src/components/SortDropdown.vue new file mode 100644 index 00000000..dfd862ef --- /dev/null +++ b/frontend/src/components/SortDropdown.vue @@ -0,0 +1,130 @@ + + + diff --git a/frontend/src/components/settings/sections/MatchingPreferencesSection.vue b/frontend/src/components/settings/sections/MatchingPreferencesSection.vue index c924e6a9..ce1f6f16 100644 --- a/frontend/src/components/settings/sections/MatchingPreferencesSection.vue +++ b/frontend/src/components/settings/sections/MatchingPreferencesSection.vue @@ -38,7 +38,7 @@ const pref = ref({ sexual_preferences: 'A', lat: 50, lon: 11, - is_custom_loc: false, + fame_gap: 10, }) const distanceRange = computed({ @@ -71,7 +71,10 @@ const locations = computed({ }, }) const city = ref('') -const fameRatingRange = ref([100]) +const fameRatingRange = computed({ + get: () => [pref.value.fame_gap], + set: ([v]) => (pref.value.fame_gap = v), +}) const byTags = computed({ get: () => pref.value.by_tags, set: v => { @@ -170,7 +173,7 @@ async function save() {
- +
diff --git a/frontend/src/components/ui/toggle-group/ToggleGroup.vue b/frontend/src/components/ui/toggle-group/ToggleGroup.vue new file mode 100644 index 00000000..b2e3a3d0 --- /dev/null +++ b/frontend/src/components/ui/toggle-group/ToggleGroup.vue @@ -0,0 +1,49 @@ + + + diff --git a/frontend/src/components/ui/toggle-group/ToggleGroupItem.vue b/frontend/src/components/ui/toggle-group/ToggleGroupItem.vue new file mode 100644 index 00000000..e94ed782 --- /dev/null +++ b/frontend/src/components/ui/toggle-group/ToggleGroupItem.vue @@ -0,0 +1,45 @@ + + + diff --git a/frontend/src/components/ui/toggle-group/index.ts b/frontend/src/components/ui/toggle-group/index.ts new file mode 100644 index 00000000..9f89ad77 --- /dev/null +++ b/frontend/src/components/ui/toggle-group/index.ts @@ -0,0 +1,2 @@ +export { default as ToggleGroup } from './ToggleGroup.vue' +export { default as ToggleGroupItem } from './ToggleGroupItem.vue' diff --git a/frontend/src/components/ui/toggle/Toggle.vue b/frontend/src/components/ui/toggle/Toggle.vue new file mode 100644 index 00000000..84ef3c5e --- /dev/null +++ b/frontend/src/components/ui/toggle/Toggle.vue @@ -0,0 +1,35 @@ + + + diff --git a/frontend/src/components/ui/toggle/index.ts b/frontend/src/components/ui/toggle/index.ts new file mode 100644 index 00000000..714d2e09 --- /dev/null +++ b/frontend/src/components/ui/toggle/index.ts @@ -0,0 +1,27 @@ +import type { VariantProps } from 'class-variance-authority' +import { cva } from 'class-variance-authority' + +export { default as Toggle } from './Toggle.vue' + +export const toggleVariants = cva( + 'hover:text-foreground aria-pressed:bg-muted focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive gap-1 rounded-md text-sm font-medium transition-[color,box-shadow] [&_svg:not([class*=size-])]:size-4 group/toggle hover:bg-muted inline-flex items-center justify-center whitespace-nowrap outline-none focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0', + { + variants: { + variant: { + default: 'bg-transparent', + outline: 'border-input hover:bg-muted border bg-transparent shadow-xs', + }, + size: { + default: 'h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', + sm: 'h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5', + lg: 'h-10 min-w-10 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +) + +export type ToggleVariants = VariantProps diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index b2fbf6b3..fb8ce2dc 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -9,51 +9,12 @@ import { EffectCreative } from 'swiper/modules' import type { Swiper as SwiperType } from 'swiper/types' import type { Profile } from '@/api/users' import { useUsers } from '@/composable/useUsers' +import SortDropdown from '@/components/SortDropdown.vue' const users = useUsers() const swiperInstance = ref(null) -const sections = ref([ - { - username: 'test', - first_name: 'Pablo', - last_name: 'test_name', - biography: 'dsfsdfsdfsd', - age: 22, - avatar: - 'https://images.unsplash.com/photo-1643023234393-776f2624061b?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxjb2xsZWN0aW9uLXBhZ2V8OHwxMTU0MDcyNnx8ZW58MHx8fHx8', - photos: [ - 'https://images.unsplash.com/photo-1643023234393-776f2624061b?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxjb2xsZWN0aW9uLXBhZ2V8OHwxMTU0MDcyNnx8ZW58MHx8fHx8', - 'https://images.unsplash.com/photo-1552134378-71b4a8333430?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxjb2xsZWN0aW9uLXBhZ2V8MTR8MTE1NDA3MjZ8fGVufDB8fHx8fA%3D%3D', - ], - tags: ['tags', 'tegs'], - common_tags: [], - fame_rating: 1400, - distance: 100, - me: false, - gender: 'M', - }, - { - username: 'test2', - first_name: 'John', - last_name: 'Doe', - biography: 'Unsiojghoihfoighjofighjoifhg', - age: 23, - avatar: - 'https://images.unsplash.com/photo-1718749861351-918524751a6b?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', - photos: [ - 'https://images.unsplash.com/photo-1718749861351-918524751a6b?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', - 'https://images.unsplash.com/photo-1701163802834-46d90f8b452a?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxjb2xsZWN0aW9uLXBhZ2V8M3wxMTU0MDcyNnx8ZW58MHx8fHx8', - 'https://images.unsplash.com/photo-1645441261871-e468b99a8d7c?q=80&w=1169&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', - ], - tags: ['tagsss', 'tegs'], - common_tags: [], - fame_rating: 1560, - distance: 2000, - me: false, - gender: 'M', - }, -]) + const skeleton = ref(true) const modules = ref([EffectCreative]) @@ -84,15 +45,15 @@ onMounted(async () => { // getSocket().emit('browsing') // getSocket().emit('browsing') - await users.suggestion({}) - sections.value = users.suggestedProfiles.value; - console.log(users.suggestedProfiles.value) + await users.suggestion({limit: 5, order: 'desc', sort: "age"}) })