Skip to content

Commit

Permalink
Merge pull request #7 from BeatBuzzer/feat/friend-system
Browse files Browse the repository at this point in the history
Feat/friend system
  • Loading branch information
synan798 authored Dec 5, 2024
2 parents 6a2bb13 + b4b439a commit 07b5924
Show file tree
Hide file tree
Showing 21 changed files with 725 additions and 11 deletions.
197 changes: 197 additions & 0 deletions DB/friendships.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
DO
$$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'friendship_status') THEN
CREATE TYPE friendship_status AS ENUM ('pending', 'accepted', 'declined');
END IF;
END
$$;

CREATE TABLE IF NOT EXISTS friendships
(
friendship_id SERIAL PRIMARY KEY,
user1_id UUID NOT NULL,
user2_id UUID NOT NULL,
status friendship_status NOT NULL,
action_user_id UUID NOT NULL, -- The user who performed the last action
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),

-- Ensure user1_id is always less than user2_id to prevent duplicate friendships
CONSTRAINT ensure_user_order CHECK (user1_id < user2_id),
CONSTRAINT unique_friendship UNIQUE (user1_id, user2_id),

-- Foreign keys
CONSTRAINT fk_user1 FOREIGN KEY (user1_id) REFERENCES users (id) ON DELETE CASCADE,
CONSTRAINT fk_user2 FOREIGN KEY (user2_id) REFERENCES users (id) ON DELETE CASCADE,
CONSTRAINT fk_action_user FOREIGN KEY (action_user_id) REFERENCES users (id)
);

CREATE INDEX IF NOT EXISTS idx_friendship_user1 ON friendships (user1_id, status);
CREATE INDEX IF NOT EXISTS idx_friendship_user2 ON friendships (user2_id, status);

-- Functions

-- automatically update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS
$$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE TRIGGER update_friendships_timestamp
BEFORE UPDATE
ON friendships
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

-- Send a friend request
CREATE OR REPLACE FUNCTION send_friend_request(sender_id UUID, receiver_id UUID) RETURNS void AS
$$
DECLARE
smaller_id UUID;
larger_id UUID;
BEGIN
-- Determine order of IDs
IF sender_id < receiver_id THEN
smaller_id := sender_id;
larger_id := receiver_id;
ELSE
smaller_id := receiver_id;
larger_id := sender_id;
END IF;

-- Insert friendship record
INSERT INTO friendships (user1_id, user2_id, status, action_user_id)
VALUES (smaller_id, larger_id, 'pending', sender_id)
ON CONFLICT (user1_id, user2_id) DO UPDATE
SET status = CASE
WHEN friendships.status = 'declined' THEN 'pending'::friendship_status
ELSE friendships.status
END,
action_user_id = sender_id;
END;
$$ LANGUAGE plpgsql;

-- accept friendship request
CREATE OR REPLACE FUNCTION accept_friend_request_by_id(friendship_id_param INT)
RETURNS void AS
$$
BEGIN
UPDATE friendships
SET status = 'accepted'
WHERE friendship_id = friendship_id_param
AND status = 'pending';

IF NOT FOUND THEN
RAISE EXCEPTION 'No pending friend request found with this ID';
END IF;
END;
$$ LANGUAGE plpgsql;


-- decline friendship request
CREATE OR REPLACE FUNCTION decline_friend_request_by_id(friendship_id_param INT)
RETURNS void AS
$$
BEGIN
UPDATE friendships
SET status = 'declined'
WHERE friendship_id = friendship_id_param
AND status = 'pending';

IF NOT FOUND THEN
RAISE EXCEPTION 'No pending friend request found with this ID';
END IF;
END;
$$ LANGUAGE plpgsql;

-- delete friendship
CREATE OR REPLACE FUNCTION remove_friend(user_id UUID, friend_id UUID) RETURNS void AS
$$
DECLARE
smaller_id UUID;
larger_id UUID;
BEGIN
-- Determine order of IDs
IF friend_id < user_id THEN
smaller_id := friend_id;
larger_id := user_id;
ELSE
smaller_id := user_id;
larger_id := friend_id;
END IF;

DELETE
FROM friendships
WHERE user1_id = smaller_id
AND user2_id = larger_id
AND status = 'accepted';

IF NOT FOUND THEN
RAISE EXCEPTION 'No active friendship found between these users';
END IF;
END;
$$ LANGUAGE plpgsql;

-- 2nd version with id
CREATE OR REPLACE FUNCTION remove_friend_by_id(friendship_id_param INT) RETURNS void AS
$$
BEGIN
DELETE
FROM friendships
WHERE friendship_id = friendship_id_param
AND status = 'accepted';

IF NOT FOUND THEN
RAISE EXCEPTION 'No active friendship found with this ID';
END IF;
END;
$$ LANGUAGE plpgsql;

-- retrieve all friends, incoming and outgoing friend requests
CREATE OR REPLACE FUNCTION get_friends(user_id UUID)
RETURNS TABLE
(
friendship_id INT,
friend_id UUID,
friend_username TEXT,
friend_avatar TEXT,
friend_spotify_id TEXT,
friend_spotify_visibility BOOLEAN,
status friendship_status,
action_user_id UUID,
created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE,
request_type TEXT
)
AS
$$
BEGIN
RETURN QUERY
SELECT f.friendship_id,
CASE WHEN f.user1_id = user_id THEN f.user2_id ELSE f.user1_id END AS friend_id,
u.username AS friend_username,
u.avatar_url AS friend_avatar,
u.spotify_id AS friend_spotify_id,
u.spotify_visibility AS friend_spotify_visibility,
f.status,
f.action_user_id,
f.created_at,
f.updated_at,
CASE WHEN f.action_user_id = user_id THEN 'outgoing' ELSE 'incoming' END AS request_type
FROM friendships f, users u
WHERE (f.user1_id = user_id OR f.user2_id = user_id)
AND (f.status != 'declined')
AND (CASE WHEN f.user1_id = user_id THEN f.user2_id ELSE f.user1_id END = u.id);
END;
$$ LANGUAGE plpgsql;
-- examples
-- SELECT send_friend_request('sender', 'receiver');
-- SELECT accept_friend_request_by_id(4);
-- SELECT decline_friend_request_by_id(4);
-- SELECT remove_friend('friend_user');
-- SELECT * FROM get_friends('user_id');
15 changes: 15 additions & 0 deletions DB/users.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE users
(
id uuid not null references auth.users on delete cascade,
avatar_url text,
username text not null,
spotify_id text not null,
spotify_visibility boolean not null default false,
primary key (id)
);

ALTER TABLE users
ADD CONSTRAINT unique_username UNIQUE (username),
ADD CONSTRAINT valid_username check (username <> '' AND length(trim(username)) >= 4 AND username ~ '^[a-zA-Z0-9_]+$');

CREATE INDEX idx_username ON users(username);
15 changes: 15 additions & 0 deletions components/home/Controls/GameButtons.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
</script>

<template>
<div class="flex space-x-3 w-full">
<button class="bg-[#22a9fb] hover:bg-blue-700 text-white font-bold py-20 rounded-3xl text-lg w-full">
Start Game
</button>
<button class="bg-[#22a9fb] hover:bg-blue-700 text-white font-bold py-20 rounded-3xl text-lg w-full">
Start Round
</button>
</div>
</template>

14 changes: 14 additions & 0 deletions components/home/Turns/Opponent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
</script>

<template>
<div class="w-full bg-gray-200 p-3 mt-auto rounded-3xl my-3">
<p class="my-1 ">Opponentes Turn</p>
<div class="flex space-x-3">
<HomeUsersUserBox name="test1" :user-turn="false"/>
<HomeUsersUserBox name="test2" :user-turn="false"/>
<HomeUsersUserBox name="test3" :user-turn="false"/>
</div>
</div>
</template>
14 changes: 14 additions & 0 deletions components/home/Turns/User.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
</script>

<template>
<div class="w-full bg-gray-200 p-3 mt-auto rounded-3xl my-3 h-full">
<p class="my-1">Your Turn</p>
<div class="flex-col space-y-3">
<HomeUsersUserBox name="test1" :user-turn="true"/>
<HomeUsersUserBox name="test2" :user-turn="true"/>
<HomeUsersUserBox name="test3" :user-turn="true"/>
</div>
</div>
</template>
26 changes: 26 additions & 0 deletions components/home/Users/UserBox.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script setup lang="ts">
const props = defineProps({
profilePicture: {
type: String,
default: 'https://i.scdn.co/image/ab6775700000ee855d4c281804e8773208248312'
},
name: {
type: String,
default: 'Opponent'
},
userTurn: {
type: Boolean,
default: false
}
});
</script>

<template>
<div :class="['bg-blue-600', props.userTurn ? 'flex items-center h-30 w-full rounded-3xl p-3' : 'flex flex-col items-center justify-center h-30 w-full rounded-3xl p-3']">
<img :class="['rounded-full w-12 h-12 sm:w-16 sm:h-16 md:w-16 md:h-16 lg:w-16 lg:h-16', props.userTurn ? 'mr-3' : 'mb-2']" :src="props.profilePicture" :alt="name">
<p class="text-white">{{ props.name }}</p>
<button v-if="props.userTurn" class="ml-auto p-2 sm:p-3 md:p-4 lg:p-5" @click="console.log(props.name)">
<NuxtImg src="icons/playButton.svg" alt="Play" class="w-12 h-12 sm:w-16 sm:h-16 md:w-16 md:h-16 lg:w-16 lg:h-16 hover:bg-black"/>
</button>
</div>
</template>
41 changes: 41 additions & 0 deletions components/profile/Friendlist.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script setup lang="ts">
import {
FriendshipStatus,
FriendshipType,
type GetFriendsResponse
} from "@/types/api/user.friends";
const requests: Ref<GetFriendsResponse[]> = useState("incoming_friendships", () => [])
const friends: Ref<GetFriendsResponse[]> = useState("accepted_friendships", () => [])
const session = useSupabaseSession()
onMounted(async () => {
if (session.value) {
await getFriendships()
friends.value.push(friends.value[0])
}
})
async function getFriendships() {
$fetch<GetFriendsResponse[]>('http://localhost:3000/api/v1/user/friends')
.then((data) => {
requests.value = data.filter((item) => item.request_type == FriendshipType.INCOMING && item.status === FriendshipStatus.PENDING)
friends.value = data.filter((item) => item.status === FriendshipStatus.ACCEPTED)
});
}
</script>

<template>
<div class="w-full bg-gray-200 p-3 mt-auto rounded-3xl my-3">
<p class="my-1 ">Friends</p>
<div class="flex space-x-3 overflow-x-auto">
<HomeUsersUserBox
v-for="item in friends" :key="item.friend_id" :name="item.friend_id" :user-turn="false"
class="shrink-0 w-[calc(33.33%-1rem)]" />
</div>


</div>
</template>
21 changes: 21 additions & 0 deletions components/profile/ProfileInformation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script setup lang="ts">
</script>

<template>
<div class="flex flex-col">
<div class="flex flex-col items-center justify-center mb-3">
<img
src="https://i.scdn.co/image/ab6775700000ee855d4c281804e8773208248312"
class="w-16 h-16 rounded-full" >
<p>Nickname</p>
</div>

<div class="grid grid-cols-2 gap-4">
<div class="bg-gray-200 h-8 rounded-3xl px-3">Streak</div>
<div class="bg-gray-200 h-8 rounded-3xl px-3">Games played</div>
<div class="bg-gray-200 h-8 rounded-3xl px-3">Placeholder</div>
<div class="bg-gray-200 h-8 rounded-3xl px-3">Date joined</div>
</div>
</div>
</template>
14 changes: 14 additions & 0 deletions components/profile/ProfileOverview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
import Friendlist from './Friendlist.vue';
</script>

<template>
<div>
<div class="p-3">
<ProfileInformation class="py-8"/>
<Friendlist/>
</div>
</div>
</template>
27 changes: 27 additions & 0 deletions layouts/FooterView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup lang="ts">
</script>

<template>
<div class="flex flex-col h-screen max-h-screen items-center">
<!-- Wrapper div für die Breiten-Kontrolle -->
<div class="w-full xl:w-1/2 2xl:w-1/4 flex flex-col h-full">
<!-- Content -->
<div class="flex-1 border rounded-3xl">
<slot name="content"/>
</div>

<!-- Footer -->
<div class="basis-24 border rounded-t-3xl">
<div class="flex justify-around items-center h-full">
<slot name="footer"/>
</div>
</div>

</div>
</div>
</template>

<style scoped>
</style>
Loading

0 comments on commit 07b5924

Please sign in to comment.