Skip to content
Merged
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
df20351
feat: add responsive layout with left and right sidebars
Hussein-Mohamed1 Oct 16, 2025
038e587
refactor: improve left sidebar with typed tabs and active state logic
Hussein-Mohamed1 Oct 17, 2025
4587167
test: add comprehensive test coverage for pages, components, and layouts
Hussein-Mohamed1 Oct 17, 2025
7aa022a
refactor: standardize theming and i18n configuration
Hussein-Mohamed1 Oct 17, 2025
f1b6e3c
fix(ui): improve sidebar styling and icon consistency
Hussein-Mohamed1 Oct 18, 2025
7a43041
feat: implement user profile page with cover, avatar, and details
Hussein-Mohamed1 Oct 19, 2025
eb62d45
feat: add responsive layout with left and right sidebars
Hussein-Mohamed1 Oct 16, 2025
bbed732
refactor: improve left sidebar with typed tabs and active state logic
Hussein-Mohamed1 Oct 17, 2025
bd9817a
test: add comprehensive test coverage for pages, components, and layouts
Hussein-Mohamed1 Oct 17, 2025
e0241e0
feat: implement user profile page with cover, avatar, and details
Hussein-Mohamed1 Oct 19, 2025
599c1f1
chore: resolve merge conflicts after rebase
Hussein-Mohamed1 Oct 21, 2025
710b90e
feat: create mock data and integrate /profile/:username API into the …
Hussein-Mohamed1 Oct 22, 2025
d113c5c
chore: resolve conflicts
Hussein-Mohamed1 Oct 23, 2025
bd1ed12
feat: add responsive layout with left and right sidebars
Hussein-Mohamed1 Oct 16, 2025
23a1b68
refactor: improve left sidebar with typed tabs and active state logic
Hussein-Mohamed1 Oct 17, 2025
a5d8026
test: add comprehensive test coverage for pages, components, and layouts
Hussein-Mohamed1 Oct 17, 2025
dd7929f
refactor: standardize theming and i18n configuration
Hussein-Mohamed1 Oct 17, 2025
5e89ceb
feat: implement user profile page with cover, avatar, and details
Hussein-Mohamed1 Oct 19, 2025
f548d58
feat(generic-ui): styled spinner - [CU-869aw1b8u] (#26)
habibayman Oct 19, 2025
6139b8b
feat: profile layout - [CU-869aw1cfx] (#27)
habibayman Oct 19, 2025
993b729
refactor: improve left sidebar with typed tabs and active state logic
Hussein-Mohamed1 Oct 17, 2025
7a41465
test: add comprehensive test coverage for pages, components, and layouts
Hussein-Mohamed1 Oct 17, 2025
64525bf
feat: implement user profile page with cover, avatar, and details
Hussein-Mohamed1 Oct 19, 2025
7d751aa
chore: resolve merge conflicts after rebase
Hussein-Mohamed1 Oct 21, 2025
9a42935
feat: create mock data and integrate /profile/:username API into the …
Hussein-Mohamed1 Oct 22, 2025
781b8cb
fix: resolve PR review comments and apply suggested improvements
Hussein-Mohamed1 Oct 25, 2025
5b64787
chore: merge with dev
Hussein-Mohamed1 Oct 25, 2025
1678cda
fix: likes tab appear again in profile tabs
Hussein-Mohamed1 Oct 30, 2025
c0c46fe
feat: add responsive layout with left and right sidebars
Hussein-Mohamed1 Oct 16, 2025
9a02cb4
refactor: improve left sidebar with typed tabs and active state logic
Hussein-Mohamed1 Oct 17, 2025
2192fc8
test: add comprehensive test coverage for pages, components, and layouts
Hussein-Mohamed1 Oct 17, 2025
d5ea848
refactor: standardize theming and i18n configuration
Hussein-Mohamed1 Oct 17, 2025
edfd1b4
fix(ui): improve sidebar styling and icon consistency
Hussein-Mohamed1 Oct 18, 2025
6f5f87d
feat: implement user profile page with cover, avatar, and details
Hussein-Mohamed1 Oct 19, 2025
ad34d1e
feat: add responsive layout with left and right sidebars
Hussein-Mohamed1 Oct 16, 2025
9d113db
refactor: improve left sidebar with typed tabs and active state logic
Hussein-Mohamed1 Oct 17, 2025
504ad3c
test: add comprehensive test coverage for pages, components, and layouts
Hussein-Mohamed1 Oct 17, 2025
c319507
feat: implement user profile page with cover, avatar, and details
Hussein-Mohamed1 Oct 19, 2025
18aacdd
chore: rebase with dev
Hussein-Mohamed1 Oct 30, 2025
5e2e2f1
feat: create mock data and integrate /profile/:username API into the …
Hussein-Mohamed1 Oct 22, 2025
6150901
chore: resolve conflicts
Hussein-Mohamed1 Oct 23, 2025
dd13b3c
feat: add responsive layout with left and right sidebars
Hussein-Mohamed1 Oct 16, 2025
011b6e2
refactor: improve left sidebar with typed tabs and active state logic
Hussein-Mohamed1 Oct 17, 2025
e950337
test: add comprehensive test coverage for pages, components, and layouts
Hussein-Mohamed1 Oct 17, 2025
4bfe417
refactor: standardize theming and i18n configuration
Hussein-Mohamed1 Oct 17, 2025
d03c821
feat: implement user profile page with cover, avatar, and details
Hussein-Mohamed1 Oct 19, 2025
9bd8658
feat(generic-ui): styled spinner - [CU-869aw1b8u] (#26)
habibayman Oct 19, 2025
371d586
feat: profile layout - [CU-869aw1cfx] (#27)
habibayman Oct 19, 2025
c7cd750
refactor: improve left sidebar with typed tabs and active state logic
Hussein-Mohamed1 Oct 17, 2025
983a8dc
test: add comprehensive test coverage for pages, components, and layouts
Hussein-Mohamed1 Oct 17, 2025
ac67f42
feat: implement user profile page with cover, avatar, and details
Hussein-Mohamed1 Oct 19, 2025
fb34577
chore: resolve merge conflicts after rebase
Hussein-Mohamed1 Oct 21, 2025
1e51cf8
feat: create mock data and integrate /profile/:username API into the …
Hussein-Mohamed1 Oct 22, 2025
b04a710
fix: resolve PR review comments and apply suggested improvements
Hussein-Mohamed1 Oct 25, 2025
c8d81e8
chore: merge with dev
Hussein-Mohamed1 Oct 25, 2025
7b38d6a
fix: likes tab appear again in profile tabs
Hussein-Mohamed1 Oct 30, 2025
df72053
chore: solve merge conflicts
Hussein-Mohamed1 Oct 30, 2025
cbe7da0
chore: solve merge conflicts
Hussein-Mohamed1 Oct 30, 2025
4fcb302
chore: address issues mentioned in PR comments
Hussein-Mohamed1 Oct 30, 2025
157c4c4
test: increase test cov
Hussein-Mohamed1 Oct 30, 2025
86b2db0
chore: merge with dev
Hussein-Mohamed1 Oct 30, 2025
053d3f2
feat: create not logged in user profile
habibayman Oct 30, 2025
8420265
feat: add dynamic username route in left side bar
habibayman Oct 30, 2025
667f4d8
fix: show tweets back in user profile
habibayman Oct 30, 2025
bf36c4f
test: not logged in profile page
habibayman Oct 30, 2025
3e05629
test: fix profile posts page
habibayman Oct 30, 2025
63f21b8
test: remove duplicate & wrong test
habibayman Oct 30, 2025
b852cd1
fix: add back default user data
habibayman Oct 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions app/components/SideBar/Left/Tab.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<script lang="ts" setup>
import type { LeftSidebarTab } from '~~/shared/types/leftsidebar';

const props = defineProps<{
tab: LeftSidebarTab;
}>();
Expand Down
23 changes: 20 additions & 3 deletions app/components/SideBar/Left/index.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
<script lang="ts" setup>
import { SIDEBAR_TABS } from '~/constants/leftsidebar';
const userStore = useUserStore();
</script>
<template>
<div class="flex h-screen flex-col items-center xl:items-start">
<div class="hover:bg-accent my-2 w-min p-2 hover:rounded-full">
<div class="hover:bg-foreground/10 my-2 w-min p-2 hover:rounded-full">
<NuxtLink to="/">
<LogoRaven class="h-8 w-8" />
</NuxtLink>
</div>
<div class="mt-2 flex flex-col items-center space-y-3 xl:items-start">
<SideBarLeftTab v-for="tab in SIDEBAR_TABS" :key="tab.label" :tab />
<SideBarLeftTab :tab="{ label: 'home', icon: 'home', route: '/home' }"></SideBarLeftTab>
<SideBarLeftTab
:tab="{ label: 'explore', icon: 'search', route: '/explore' }"
></SideBarLeftTab>
<SideBarLeftTab
:tab="{ label: 'notifications', icon: 'notifications', route: '/notifications' }"
></SideBarLeftTab>
<SideBarLeftTab
:tab="{ label: 'messages', icon: 'chat', route: '/messages' }"
></SideBarLeftTab>
<SideBarLeftTab
:tab="{
label: 'profile',
icon: 'person',
route: `/profile/${userStore.user?.username || ''}`,
}"
></SideBarLeftTab>
<SideBarLeftTab :tab="{ label: 'more', icon: 'more-horiz', route: '#' }"></SideBarLeftTab>
</div>
</div>
</template>
2 changes: 1 addition & 1 deletion app/components/SideBar/Right/PreviewCard/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const props = defineProps({
</h1>
<slot />

<div class="text-primary hover:bg-background cursor-pointer p-3 text-sm">
<div class="text-primary hover:bg-foreground/10 cursor-pointer p-3 text-sm">
{{ $t('rightsidebar.show-more') }}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/components/SideBar/Right/PreviewCard/item.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts" setup></script>

<template>
<div class="border-border hover:bg-background cursor-pointer border-b p-3">
<div class="border-border hover:bg-foreground/10 cursor-pointer border-b p-3">
<slot />
</div>
</template>
2 changes: 1 addition & 1 deletion app/components/SideBar/Right/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const whoToFollowItems = [
</script>

<template>
<div>
<div class="ms-4">
<!-- preview Card -->
<SideBarRightPreviewCard :title="$t('rightsidebar.whats-happening.title')">
<div>
Expand Down
18 changes: 18 additions & 0 deletions app/components/profile/ProfileAvatarSection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script lang="ts" setup>
defineProps<{
profileImg: string;
}>();
</script>
<template>
<div class="mx-4 flex flex-wrap items-center justify-between gap-4">
<div>
<NuxtImg
:src="profileImg"
alt="Profile picture"
class="-mt-16 size-34 rounded-full border-4 object-contain"
loading="eager"
/>
</div>
<UiButton variant="outline">{{ $t('profile-info.edit-profile') }}</UiButton>
</div>
</template>
8 changes: 8 additions & 0 deletions app/components/profile/ProfileCover.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts" setup>
defineProps<{
coverImg: string;
}>();
</script>
<template>
<NuxtImg :src="coverImg" alt="Profile Cover" class="h-48 w-full object-cover" loading="eager" />
</template>
13 changes: 11 additions & 2 deletions app/components/profile/ProfileDetails.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
<script setup lang="ts"></script>
<script lang="ts" setup>
import type { UserProfile } from '~~/shared/types/user';

defineProps<{
userProfile: UserProfile;
}>();
</script>
<template>
<div class="bg-accent/10 h-[460px]"></div>
<div>
<ProfileCover :cover-img="userProfile.bannerUrl" />
<ProfileAvatarSection :profile-img="userProfile.avatarUrl" />
<ProfileInfo :user-profile="userProfile" />
</div>
</template>
50 changes: 50 additions & 0 deletions app/components/profile/ProfileInfo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script lang="ts" setup>
import type { UserProfile } from '~~/shared/types/user';
import { formatMonthYear } from '~/utils/date';
const props = defineProps<{
userProfile: UserProfile;
}>();
const displayUrl = computed(() => {
if (props.userProfile.websiteUrl) {
const cleanedUrl = cleanUrl(props.userProfile.websiteUrl);
return cleanedUrl.length > 30 ? cleanedUrl.slice(0, 29) + '...' : cleanedUrl;
}
return '';
});
</script>
<template>
<div class="mt-2 flex flex-col">
<div class="px-4">
<h2 class="text-foreground text-2xl font-bold">{{ userProfile.displayName }}</h2>
<p class="text-muted-foreground">{{ userProfile.username }}</p>
<p class="mt-2">{{ userProfile.bio }}</p>
<div class="mt-2 flex flex-wrap">
<a
:href="userProfile.websiteUrl"
target="_blank"
rel="noopener noreferrer"
class="text-brand-blue me-2 flex items-center gap-1 hover:underline"
>
<Icon class="text-muted-foreground" name="ic:sharp-link" size="18" />
{{ displayUrl }}
</a>

<p class="text-muted-foreground flex items-center gap-1">
<Icon name="ic:sharp-calendar-month" />
{{ $t('profile-info.joined') }}
{{ formatMonthYear(userProfile.joinedAt) }}
</p>
</div>
<div class="mt-4 flex space-x-4">
<span
><strong>{{ userProfile.followingCount }}</strong>
<span class="text-muted-foreground ms-1"> {{ $t('profile-info.following') }} </span>
</span>
<span
><strong>{{ userProfile.followersCount }}</strong>
<span class="text-muted-foreground ms-1"> {{ $t('profile-info.followers') }} </span>
</span>
</div>
</div>
</div>
</template>
16 changes: 16 additions & 0 deletions app/composables/useUserProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { UserProfile } from '~~/shared/types/user';
Comment thread
Hussein-Mohamed1 marked this conversation as resolved.

export const useUserProfile = (username: string) => {
const {
data: userProfile,
error,
pending,
} = useFetch<UserProfile>(`/api/users/${username}/profile`, {
key: `user-profile-${username}`,
});
return {
userProfile,
error,
loading: pending,
};
};
35 changes: 0 additions & 35 deletions app/constants/leftsidebar.ts

This file was deleted.

4 changes: 2 additions & 2 deletions app/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
<div class="flex min-h-screen justify-center">
<div class="flex w-full max-w-7xl sm:justify-center">
<!-- Left sidebar -->
<div class="w-16 flex-shrink-0 sm:w-20 md:w-24 xl:w-64">
<div class="w-16 flex-shrink-0 sm:w-16 md:w-24 xl:w-64">
<div class="sticky top-0">
<SideBarLeft />
</div>
</div>

<!-- Main content -->
<main class="border-border flex-1 border-x sm:w-[590px] sm:flex-none md:w-[600px]">
<main class="border-border flex-1 border-x sm:w-[560px] sm:flex-none md:w-[600px]">
<slot />
</main>

Expand Down
71 changes: 46 additions & 25 deletions app/layouts/profile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,57 @@
import ProfileDetails from '~/components/profile/ProfileDetails.vue';
import Tabs from '@/components/ui/Tabs.vue';
import Tab from '@/components/ui/Tab.vue';
import Spinner from '~/components/ui/Spinner.vue';

const route = useRoute();
const username = computed(() => (route.params.username as string) || 'hussein');
const profilePath = computed(() => `/profile/${username.value}`);
const userStore = useUserStore();

onMounted(() => {
userStore.fetchUserProfile(username.value);
});
</script>

<template>
<NuxtLayout name="default">
<!-- Profile header and tabs (sticky across all profile pages) -->
<ProfileDetails />
<Tabs>
<Tab
:label="$t('profile.tabs.posts')"
route="/profile"
:is-active="$route.path === '/profile'"
/>
<Tab
:label="$t('profile.tabs.replies')"
route="/profile/replies"
:is-active="$route.path === '/profile/replies'"
/>
<Tab
:label="$t('profile.tabs.media')"
route="/profile/media"
:is-active="$route.path === '/profile/media'"
/>
<Tab
:label="$t('profile.tabs.likes')"
route="/profile/likes"
:is-active="$route.path === '/profile/likes'"
/>
</Tabs>
<div v-if="userStore.loading" class="flex justify-center p-4">
<Spinner />
</div>

<div v-else-if="userStore.error" class="text-destructive p-4">
{{ userStore.error }}
</div>

<div v-else-if="userStore.user">
<ProfileDetails :user-profile="userStore.user" />

<Tabs>
Comment thread
Hussein-Mohamed1 marked this conversation as resolved.
<Tab
:label="$t('profile.tabs.posts')"
:route="`${profilePath}`"
:is-active="$route.path === `${profilePath}`"
/>
<Tab
:label="$t('profile.tabs.replies')"
:route="`${profilePath}/replies`"
:is-active="$route.path === `${profilePath}/replies`"
/>
<Tab
:label="$t('profile.tabs.media')"
:route="`${profilePath}/media`"
:is-active="$route.path === `${profilePath}/media`"
/>
<Tab
:label="$t('profile.tabs.likes')"
:route="`${profilePath}/likes`"
:is-active="$route.path === `${profilePath}/likes`"
/>
</Tabs>

<!-- Dynamic content from child tab pages -->
<slot />
<!-- Dynamic content from child tab pages -->
<slot />
</div>
</NuxtLayout>
</template>
27 changes: 27 additions & 0 deletions app/pages/profile/[username]/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup lang="ts">
import type { Tweet } from '~~/shared/types/tweets';
import TweetDefaultCard from '@/components/tweet/TweetDefaultCard.vue';
definePageMeta({
layout: 'profile',
});

const tweets = ref<Tweet[]>([]);

const { data: tweetsData, error } = await useFetch<{ data: Tweet[] }>('/api/tweets');
tweets.value = tweetsData.value?.data || [];

if (error.value) console.error(error.value);
</script>

<template>
<div class="p-2">
<div v-if="tweets.length > 0" class="mb-4">
<div class="mt-4 flex w-full max-w-[700px] flex-col gap-4">
<TweetDefaultCard v-for="tweet in tweets" :key="tweet.id" :tweet="tweet" />
</div>
</div>
<div v-else data-testid="empty-state" class="text-muted-foreground mt-10 text-center">
<h1 class="text-xl font-semibold">{{ $t('testing.tweets.tweet-not-found') }}</h1>
</div>
</div>
</template>
File renamed without changes.
File renamed without changes.
36 changes: 19 additions & 17 deletions app/pages/profile/index.vue
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
<script setup lang="ts">
import type { Tweet } from '~~/shared/types/tweets';
import TweetDefaultCard from '../../components/tweet/TweetDefaultCard.vue';
const { t } = useI18n();

definePageMeta({
layout: 'profile',
layout: 'default',
});

const tweets = ref<Tweet[]>([]);

const { data: tweetsData, error } = await useFetch<{ data: Tweet[] }>('/api/tweets');
tweets.value = tweetsData.value?.data || [];

if (error.value) console.error(error.value);
</script>

<template>
<div class="p-2">
<div v-if="tweets.length > 0" class="mb-4">
<div class="mt-4 flex w-full max-w-[700px] flex-col gap-4">
<TweetDefaultCard v-for="tweet in tweets" :key="tweet.id" :tweet="tweet" />
<div class="flex min-h-screen items-center justify-center p-6">
<div class="text-center">
<div class="mb-6">
<LogoRaven class="text-primary mx-auto h-16 w-16" />
</div>
<h1 class="mb-4 text-3xl font-bold">{{ t('profile.not-logged-in.title') }}</h1>
<p class="text-muted-foreground mb-8 text-lg">
{{ t('profile.not-logged-in.description') }}
</p>
<div class="space-y-3">
<NuxtLink
to="/"
class="bg-primary hover:bg-primary/90 inline-block rounded-full px-8 py-3 font-semibold text-white transition-colors"
>
{{ t('profile.not-logged-in.take-me-home') }}
</NuxtLink>
</div>
</div>
<div v-else data-testid="empty-state" class="text-muted-foreground mt-10 text-center">
<h1 class="text-xl font-semibold">{{ $t('testing.tweets.tweet-not-found') }}</h1>
</div>
</div>
</template>
Loading
Loading