diff --git a/backend/src/db/api/posts.js b/backend/src/db/api/posts.js
index f4ef2d6..a66bd49 100644
--- a/backend/src/db/api/posts.js
+++ b/backend/src/db/api/posts.js
@@ -1,4 +1,3 @@
-
const db = require('../models');
const FileDBApi = require('./file');
const crypto = require('crypto');
@@ -487,6 +486,7 @@ module.exports = class PostsDBApi {
output.author = await posts.getAuthor({
+ include: [{ model: db.file, as: 'avatar' }],
transaction
});
@@ -529,7 +529,7 @@ module.exports = class PostsDBApi {
{
model: db.users,
as: 'author',
-
+ include: [{ model: db.file, as: 'avatar' }],
where: filter.author ? {
[Op.or]: [
{ id: { [Op.in]: filter.author.split('|').map(term => Utils.uuid(term)) } },
@@ -899,5 +899,4 @@ module.exports = class PostsDBApi {
}
-};
-
+};
\ No newline at end of file
diff --git a/frontend/src/components/Socio/CreatePost.tsx b/frontend/src/components/Socio/CreatePost.tsx
new file mode 100644
index 0000000..1d7ee8b
--- /dev/null
+++ b/frontend/src/components/Socio/CreatePost.tsx
@@ -0,0 +1,75 @@
+import React, { useState } from 'react';
+import { mdiImageOutline, mdiVideoOutline, mdiEmoticonOutline, mdiSend } from '@mdi/js';
+import BaseIcon from '../BaseIcon';
+import UserAvatarCurrentUser from '../UserAvatarCurrentUser';
+import { useAppDispatch } from '../../stores/hooks';
+import { create as createPost } from '../../stores/posts/postsSlice';
+import BaseButton from '../BaseButton';
+
+const CreatePost = ({ onPostCreated }: { onPostCreated: () => void }) => {
+ const dispatch = useAppDispatch();
+ const [caption, setCaption] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async () => {
+ if (!caption.trim()) return;
+
+ setLoading(true);
+ try {
+ await dispatch(createPost({
+ caption,
+ post_type: 'text',
+ visibility: 'public',
+ published_at: new Date().toISOString()
+ }));
+ setCaption('');
+ onPostCreated();
+ } catch (error) {
+ console.error('Failed to create post', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default CreatePost;
diff --git a/frontend/src/components/Socio/PostCard.tsx b/frontend/src/components/Socio/PostCard.tsx
new file mode 100644
index 0000000..fe83fe1
--- /dev/null
+++ b/frontend/src/components/Socio/PostCard.tsx
@@ -0,0 +1,119 @@
+import React, { useState } from 'react';
+import { mdiHeart, mdiHeartOutline, mdiCommentOutline, mdiShareVariantOutline } from '@mdi/js';
+import BaseIcon from '../BaseIcon';
+import UserAvatar from '../UserAvatar';
+import ImageField from '../ImageField';
+import { useAppDispatch, useAppSelector } from '../../stores/hooks';
+import { create as createReaction } from '../../stores/reactions/reactionsSlice';
+import { update as updatePost } from '../../stores/posts/postsSlice';
+import dayjs from 'dayjs';
+import relativeTime from 'dayjs/plugin/relativeTime';
+
+dayjs.extend(relativeTime);
+
+type Props = {
+ post: any;
+};
+
+const PostCard = ({ post }: Props) => {
+ const dispatch = useAppDispatch();
+ const { currentUser } = useAppSelector((state) => state.auth);
+ const [isLiked, setIsLiked] = useState(false); // Simple local state for demo
+ const [likeCount, setLikeCount] = useState(post.like_count || 0);
+
+ const handleLike = async () => {
+ if (isLiked) return;
+
+ setIsLiked(true);
+ setLikeCount(likeCount + 1);
+
+ try {
+ await dispatch(createReaction({
+ target_type: 'post',
+ reaction_type: 'like',
+ postId: post.id,
+ userId: currentUser?.id,
+ reacted_at: new Date().toISOString()
+ }));
+
+ // Update post like count in DB
+ await dispatch(updatePost({
+ id: post.id,
+ data: {
+ like_count: likeCount + 1
+ }
+ }));
+ } catch (error) {
+ console.error('Failed to like post', error);
+ setIsLiked(false);
+ setLikeCount(likeCount);
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ {post.author?.firstName} {post.author?.lastName}
+
+
+ {dayjs(post.createdAt).fromNow()}
+
+
+
+
+ {/* Content */}
+
+
+ {/* Image */}
+ {post.media_images && post.media_images.length > 0 && (
+
+
+
+ )}
+
+ {/* Actions */}
+
+
+
+
+
+
+
+
+
+
+ {post.view_count || 0} views
+
+
+
+ );
+};
+
+export default PostCard;
\ No newline at end of file
diff --git a/frontend/src/components/Socio/TrendingBar.tsx b/frontend/src/components/Socio/TrendingBar.tsx
new file mode 100644
index 0000000..0c1a93a
--- /dev/null
+++ b/frontend/src/components/Socio/TrendingBar.tsx
@@ -0,0 +1,92 @@
+import React from 'react';
+import { mdiPound, mdiAccountPlusOutline } from '@mdi/js';
+import BaseIcon from '../BaseIcon';
+
+const TrendingBar = () => {
+ const trending = [
+ { tag: 'reactjs', count: '12.4k' },
+ { tag: 'tailwindcss', count: '8.2k' },
+ { tag: 'socialmedia', count: '5.6k' },
+ { tag: 'nextjs', count: '4.1k' },
+ { tag: 'webdev', count: '3.9k' },
+ ];
+
+ const suggestions = [
+ { name: 'John Doe', handle: '@johndoe', avatar: 'JD' },
+ { name: 'Jane Smith', handle: '@janesmith', avatar: 'JS' },
+ { name: 'Alex Rivera', handle: '@arivera', avatar: 'AR' },
+ ];
+
+ return (
+
+ {/* Trending Section */}
+
+
Trending for you
+
+ {trending.map((item) => (
+
+
+
+
+
+
+
+ #{item.tag}
+
+
+ {item.count} posts
+
+
+
+
+ ))}
+
+
+
+
+ {/* Suggested Users */}
+
+
Who to follow
+
+ {suggestions.map((user) => (
+
+
+
+ {user.avatar}
+
+
+
+ {user.name}
+
+
+ {user.handle}
+
+
+
+
+
+ ))}
+
+
+
+
+ {/* Footer Links */}
+
+ Terms of Service
+ Privacy Policy
+ Cookie Policy
+ Accessibility
+ Ads info
+ © 2026 Socio
+
+
+ );
+};
+
+export default TrendingBar;
diff --git a/frontend/src/css/main.css b/frontend/src/css/main.css
index a2d6067..cbbf1b6 100644
--- a/frontend/src/css/main.css
+++ b/frontend/src/css/main.css
@@ -58,7 +58,18 @@ body {
}
.introjs-bullets ul li a.active {
@apply bg-midnightBlueTheme-iconsColor !important;
+ text-shadow: none !important;
}
.introjs-prevbutton{
@apply bg-transparent border border-midnightBlueTheme-buttonColor text-midnightBlueTheme-buttonColor !important;
}
+
+@layer utilities {
+ .no-scrollbar::-webkit-scrollbar {
+ display: none;
+ }
+ .no-scrollbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts
index 527c02b..35f3af1 100644
--- a/frontend/src/menuAside.ts
+++ b/frontend/src/menuAside.ts
@@ -2,6 +2,11 @@ import * as icon from '@mdi/js';
import { MenuAsideItem } from './interfaces'
const menuAside: MenuAsideItem[] = [
+ {
+ href: '/feed',
+ icon: icon.mdiHomeOutline,
+ label: 'Feed',
+ },
{
href: '/dashboard',
icon: icon.mdiViewDashboardOutline,
@@ -216,4 +221,4 @@ const menuAside: MenuAsideItem[] = [
},
]
-export default menuAside
+export default menuAside
\ No newline at end of file
diff --git a/frontend/src/pages/feed.tsx b/frontend/src/pages/feed.tsx
new file mode 100644
index 0000000..6a55e1f
--- /dev/null
+++ b/frontend/src/pages/feed.tsx
@@ -0,0 +1,99 @@
+import React, { useEffect, useState } from 'react';
+import type { ReactElement } from 'react';
+import Head from 'next/head';
+import LayoutAuthenticated from '../layouts/Authenticated';
+import SectionMain from '../components/SectionMain';
+import { getPageTitle } from '../config';
+import { useAppDispatch, useAppSelector } from '../stores/hooks';
+import { fetch as fetchPosts } from '../stores/posts/postsSlice';
+import PostCard from '../components/Socio/PostCard';
+import CreatePost from '../components/Socio/CreatePost';
+import TrendingBar from '../components/Socio/TrendingBar';
+import LoadingSpinner from '../components/LoadingSpinner';
+import UserAvatar from '../components/UserAvatar';
+
+const FeedPage = () => {
+ const dispatch = useAppDispatch();
+ const { posts, loading } = useAppSelector((state) => state.posts);
+ const { currentUser } = useAppSelector((state) => state.auth);
+
+ const [refetch, setRefetch] = useState(0);
+
+ useEffect(() => {
+ dispatch(fetchPosts({ query: '?limit=10&page=0' }));
+ }, [dispatch, refetch]);
+
+ const stories = [
+ { id: 1, name: 'Your Story', avatar: 'Me', isMe: true },
+ { id: 2, name: 'Alice', avatar: 'AL' },
+ { id: 3, name: 'Bob', avatar: 'BO' },
+ { id: 4, name: 'Charlie', avatar: 'CH' },
+ { id: 5, name: 'David', avatar: 'DA' },
+ { id: 6, name: 'Eve', avatar: 'EV' },
+ ];
+
+ return (
+ <>
+
+ {getPageTitle('Home Feed')}
+
+
+
+
+ {/* Main Feed Content */}
+
+
+ {/* Stories Bar */}
+
+ {stories.map((story) => (
+
+
+
+ {story.isMe && currentUser ? (
+
+ ) : (
+ story.avatar
+ )}
+
+
+
+ {story.name}
+
+
+ ))}
+
+
+ {/* Create Post Widget */}
+
setRefetch(prev => prev + 1)} />
+
+ {/* Posts List */}
+ {loading && !posts.length ? (
+
+ ) : (
+
+ {posts.map((post: any) => (
+
+ ))}
+
+ {!loading && posts.length === 0 && (
+
+
No posts yet. Be the first to post something!
+
+ )}
+
+ )}
+
+
+ {/* Right Sidebar - Trending & Suggestions */}
+
+
+
+ >
+ );
+};
+
+FeedPage.getLayout = function getLayout(page: ReactElement) {
+ return {page};
+};
+
+export default FeedPage;
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index 1ce6cef..744543b 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -1,166 +1,125 @@
-
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BaseButton from '../components/BaseButton';
-import CardBox from '../components/CardBox';
-import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
-import BaseDivider from '../components/BaseDivider';
-import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
-import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
-import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
+import { mdiArrowRight, mdiPlayCircleOutline } from '@mdi/js';
+import BaseIcon from '../components/BaseIcon';
-
-export default function Starter() {
- const [illustrationImage, setIllustrationImage] = useState({
- src: undefined,
- photographer: undefined,
- photographer_url: undefined,
- })
- const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
- const [contentType, setContentType] = useState('image');
- const [contentPosition, setContentPosition] = useState('left');
+export default function SocioLanding() {
const textColor = useAppSelector((state) => state.style.linkColor);
+ const title = 'Socio';
- const title = 'Socio Social App'
+ return (
+
+
+
{getPageTitle('Welcome to Socio')}
+
- // Fetch Pexels image/video
- useEffect(() => {
- async function fetchData() {
- const image = await getPexelsImage();
- const video = await getPexelsVideo();
- setIllustrationImage(image);
- setIllustrationVideo(video);
- }
- fetchData();
- }, []);
+ {/* Navigation */}
+
- const imageBlock = (image) => (
-
-
+ {/* Hero Section */}
+
+
+
+ ✨ Connect, Share, and Discover
+
+
+ The Social Network for Tomorrow.
+
+
+ Socio brings your friends, interests, and world together in one place. Experience the next generation of social interaction.
+
+
+
+
+
+
+
+
FACEBOOK
+
INSTAGRAM
+
TIKTOK
+
+
+
+ {/* Decorative Visuals */}
+
+
+
+
+

+
+
Experience more together.
+
+
+
+
+
+
+ {/* Background Blobs */}
+
+
+
+
+ {/* Footer */}
+
);
-
- const videoBlock = (video) => {
- if (video?.video_files?.length > 0) {
- return (
-
-
-
-
)
- }
- };
-
- return (
-
-
-
{getPageTitle('Starter Page')}
-
-
-
-
- {contentType === 'image' && contentPosition !== 'background'
- ? imageBlock(illustrationImage)
- : null}
- {contentType === 'video' && contentPosition !== 'background'
- ? videoBlock(illustrationVideo)
- : null}
-
-
-
-
-
© 2026 {title}. All rights reserved
-
- Privacy Policy
-
-
-
-
- );
}
-Starter.getLayout = function getLayout(page: ReactElement) {
- return
{page};
-};
-
+SocioLanding.getLayout = function getLayout(page: ReactElement) {
+ return
{page};
+};
\ No newline at end of file