Skip to content

Commit

Permalink
Merge pull request #16 from BeatBuzzer/feat/home-view
Browse files Browse the repository at this point in the history
Feat/home view
  • Loading branch information
Likqez authored Dec 10, 2024
2 parents 0d80041 + 949c17b commit 4ce2318
Show file tree
Hide file tree
Showing 23 changed files with 923 additions and 542 deletions.
4 changes: 2 additions & 2 deletions DB/users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ 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);
CREATE INDEX idx_username ON users(username);
104 changes: 104 additions & 0 deletions components/UsersView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<script setup lang="ts">
import type {GetFriendsResponse} from "@/types/api/user.friends";
import {UserViewType} from "@/types/components/users.view";
import type {GetUserResponse} from "@/types/api/users";
// Props
const props = defineProps({
viewType: {
type: Number,
required: true,
},
users: {
type: Array as PropType<GetFriendsResponse[] | GetUserResponse[]>,
required: true,
}
});
const default_avatar = ref('https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg');
// Computed Classes
const containerClasses = computed(() => {
const baseClasses = 'w-full bg-gray-200 px-3 pb-1 mt-auto rounded-3xl mb-3';
if (props.viewType === UserViewType.USERTURN) return `${baseClasses} h-full overflow-y-hidden overflow-x-auto flex-grow-0`;
if (props.viewType === UserViewType.OPPONENTTURN || props.viewType === UserViewType.FRIENDS) return `${baseClasses} overflow-y-hidden`;
return '';
});
const userBoxContainerClasses = computed(() =>
props.viewType === UserViewType.USERTURN
? 'flex flex-col space-y-1 md:space-y-3 h-full overflow-y-auto'
: 'flex gap-1 md:gap-3 mt-6 md:mt-9'
);
function isGetFriendsResponse(user: GetFriendsResponse | GetUserResponse): user is GetFriendsResponse {
return 'friend_id' in user;
}
function isUserInformation(user: GetFriendsResponse | GetUserResponse): user is GetUserResponse {
return 'id' in user && 'username' in user;
}
// Map users conditionally depending on their type
const mappedUsers: Array<GetUserResponse> = computed(() => {
return props.users.map(user => {
if (isGetFriendsResponse(user)) {
return user.user;
} else if (isUserInformation(user)) {
return user;
}
return {};
});
});
</script>

<template>
<div
:class="[
containerClasses,
((viewType === UserViewType.FRIENDS || viewType === UserViewType.OPPONENTTURN) && users.length > 3) ? 'pr-0' : ''
]"
>
<!-- Fixed Conditional Header -->
<div class="mb-1 text-xs md:text-base bg-gray-200 mt-2">
<p v-if="viewType === UserViewType.USERTURN">
Your Turn
</p>
<p v-else-if="viewType === UserViewType.OPPONENTTURN" class="fixed">
Opponent's Turn
</p>
<p v-else-if="viewType === UserViewType.FRIENDS" class="fixed">
Friends
</p>
</div>

<!-- Scrollable User Boxes -->
<div
v-if="users.length != 0"
:class="userBoxContainerClasses"
>
<HomeUsersUserBox
v-for="user in mappedUsers"
:key="user.id"
:profile-picture="user?.avatar_url ? user.avatar_url : default_avatar"
:name="user.username"
:user-turn="viewType === UserViewType.USERTURN"
:class="[
(viewType === UserViewType.FRIENDS || viewType === UserViewType.OPPONENTTURN)
? users.length === 3 ? 'w-1/3'
: users.length === 2 ? 'w-1/2'
: 'w-full'
: ''
]"
:style="(viewType === UserViewType.FRIENDS || viewType === UserViewType.OPPONENTTURN)
? users.length > 3
? { width: 'calc(33.33% - .98rem)', flexShrink: 0 }
: {}
: {}"
/>
<!-- Placeholder for scrolling -->
<div v-if="users.length > 3" class="px-1 py-4"/>
</div>
</div>
</template>
4 changes: 2 additions & 2 deletions components/home/Controls/GameButtons.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

<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">
<button class="bg-[#22a9fb] hover:bg-blue-700 text-white font-bold h-full 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">
<button class="bg-[#22a9fb] hover:bg-blue-700 text-white font-bold h-full rounded-3xl text-lg w-full">
Start Round
</button>
</div>
Expand Down
14 changes: 0 additions & 14 deletions components/home/Turns/Opponent.vue

This file was deleted.

14 changes: 0 additions & 14 deletions components/home/Turns/User.vue

This file was deleted.

44 changes: 35 additions & 9 deletions components/home/Users/UserBox.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<script setup lang="ts">
const props = defineProps({
profilePicture: {
type: String,
default: 'https://i.scdn.co/image/ab6775700000ee855d4c281804e8773208248312'
default: 'https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg'
},
name: {
type: String,
default: 'Opponent'
default: 'User'
},
userTurn: {
type: Boolean,
Expand All @@ -16,11 +17,36 @@
</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
:class="[
'bg-blue-600 rounded-3xl px-3 w-full',
props.userTurn
? 'flex items-center'
: 'flex flex-col items-center justify-center mb-3 py-2'
]"
>
<!-- Profile Picture -->
<NuxtImg
:class="[
'rounded-full',
'w-10 h-10 sm:w-12 sm:h-12 md:w-14 md:h-14',
props.userTurn ? 'mr-3' : 'mb-0'
]"
:src="props.profilePicture.toString()"
:alt="props.name"
/>

<!-- User Name -->
<p class="text-white text-sm sm:text-base md:text-lg" v-text="props.name"/>

<!-- Play Button -->
<button
v-if="props.userTurn"
class="ml-auto p-2 sm:p-3 md:p-4 lg:p-5"
@click="console.log(props.name)"
>
<Icon name="mdi:play" class="w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 lg:w-14 lg:h-14 text-white"/>
</button>
</div>
</template>
</template>

46 changes: 22 additions & 24 deletions components/profile/Friendlist.vue
Original file line number Diff line number Diff line change
@@ -1,41 +1,39 @@
<script setup lang="ts">
import {
FriendshipStatus,
FriendshipType,
type GetFriendsResponse
} from "@/types/api/user.friends";
import {FriendshipStatus, FriendshipType, type GetFriendsResponse} from "@/types/api/user.friends";
import { UserViewType } from "@/types/components/users.view";
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])
}
if (session.value) {
await getFriendships()
//friends.value.push(friends.value[0]) unexpected behavior on initial page load
}
})
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)
});
$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)
});
}
function addFriend() {
const newFriend = {
friend_username: "sfsdf",
}
friends.value.push(newFriend)
}
</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>

<UsersView :view-type="UserViewType.FRIENDS" :users="friends"/>

</div>
</template>
<div class="mt-3">
<button class="p-2 bg-blue-500 text-white rounded ml-2" @click="addFriend">Add Friend</button>
</div>
</template>
41 changes: 29 additions & 12 deletions components/profile/ProfileInformation.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
<script setup lang="ts">
const session = useSupabaseSession()
const {user, loading, error, fetchUser} = useUser()
onMounted(async () => {
if (session.value) {
await fetchUser()
}
})
const default_avatar = ref('https://t4.ftcdn.net/jpg/05/49/98/39/360_F_549983970_bRCkYfk0P6PP5fKbMhZMIb07mCJ6esXL.jpg');
const formatDate = (dateString : string) => {
const options = { year: "numeric", month: "short", day: "numeric" };
const formattedDate = new Intl.DateTimeFormat('en-US', options).format(new Date(dateString));
return formattedDate.replace(',', '');
};
</script>

<template>
<div>
<div v-if="error" class="text-red-500">Error occurred: {{error}}</div>
<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="flex flex-col items-center justify-center mb-3">
<NuxtImg :src="loading || !user?.avatar_url ? default_avatar : user!.avatar_url" class="w-16 h-16 rounded-full"/>
<p>{{ loading ? 'Loading.' : user?.username }}</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 class="grid grid-cols-2 gap-4">
<div class="bg-gray-200 h-8 rounded-3xl px-3 flex items-center justify-center">Games played</div>
<div class="bg-gray-200 h-8 rounded-3xl px-3 flex items-center justify-center">Streak: {{ user?.daily_streak }}</div>
<div class="bg-gray-200 h-8 rounded-3xl px-3 flex items-center justify-center">Spotify: {{user?.spotify_visibility ? 'public' : 'private'}}</div>
<div class="bg-gray-200 h-8 rounded-3xl px-3 flex items-center justify-center">Joined: {{ formatDate(session?.user.created_at) }}</div>
</div>
</div>
</div>
</template>
34 changes: 34 additions & 0 deletions composables/useUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type {GetUserResponse} from "@/types/api/users";

export const useUser = () => {
const user = useState<GetUserResponse | null>('user', () => null)
const loading = useState<boolean>('userLoading', () => false)
const error = useState<string | null>('userError', () => null)

const fetchUser = async () => {
try {
// Only set loading on initial fetch
if (!user.value) {
loading.value = true
}

user.value = await useProfileInformation()
} catch (err) {
error.value = 'Failed to fetch user'
console.error(err)
} finally {
loading.value = false
}
}

return {
user,
loading,
error,
fetchUser
}
}

async function useProfileInformation() {
return await $fetch<GetUserResponse>('http://localhost:3000/api/v1/user/')
}
2 changes: 1 addition & 1 deletion layouts/FooterView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</div>

<!-- Footer -->
<div class="basis-24 border rounded-t-3xl">
<div class="bg-yellow-500 text-red-600 rounded-t-3xl">
<div class="flex justify-around items-center h-full">
<slot name="footer"/>
</div>
Expand Down
Loading

0 comments on commit 4ce2318

Please sign in to comment.