Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea6df1d6be |
@ -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)) } },
|
||||
@ -900,4 +900,3 @@ module.exports = class PostsDBApi {
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
75
frontend/src/components/Socio/CreatePost.tsx
Normal file
75
frontend/src/components/Socio/CreatePost.tsx
Normal file
@ -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 (
|
||||
<div className="bg-white dark:bg-dark-800 rounded-xl shadow-sm p-4 mb-6 border border-gray-100 dark:border-dark-700">
|
||||
<div className="flex items-start space-x-3">
|
||||
<UserAvatarCurrentUser className="w-10 h-10 mt-1" />
|
||||
<div className="flex-1">
|
||||
<textarea
|
||||
className="w-full bg-gray-50 dark:bg-dark-900 border-none rounded-lg p-3 text-sm focus:ring-2 focus:ring-indigo-500 transition-shadow resize-none min-h-[100px]"
|
||||
placeholder="What's on your mind?"
|
||||
value={caption}
|
||||
onChange={(e) => setCaption(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<div className="flex items-center space-x-4">
|
||||
<button className="text-gray-500 hover:text-indigo-500 transition-colors p-1 rounded-full hover:bg-gray-100 dark:hover:bg-dark-700">
|
||||
<BaseIcon path={mdiImageOutline} size="22" />
|
||||
</button>
|
||||
<button className="text-gray-500 hover:text-rose-500 transition-colors p-1 rounded-full hover:bg-gray-100 dark:hover:bg-dark-700">
|
||||
<BaseIcon path={mdiVideoOutline} size="22" />
|
||||
</button>
|
||||
<button className="text-gray-500 hover:text-yellow-500 transition-colors p-1 rounded-full hover:bg-gray-100 dark:hover:bg-dark-700">
|
||||
<BaseIcon path={mdiEmoticonOutline} size="22" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
label="Post"
|
||||
color="info"
|
||||
icon={mdiSend}
|
||||
small
|
||||
disabled={!caption.trim() || loading}
|
||||
onClick={handleSubmit}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePost;
|
||||
119
frontend/src/components/Socio/PostCard.tsx
Normal file
119
frontend/src/components/Socio/PostCard.tsx
Normal file
@ -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 (
|
||||
<div className="bg-white dark:bg-dark-800 rounded-xl shadow-sm mb-6 border border-gray-100 dark:border-dark-700 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center p-4">
|
||||
<UserAvatar
|
||||
username={post.author?.firstName || 'User'}
|
||||
image={post.author?.avatar}
|
||||
className="w-10 h-10 mr-3"
|
||||
/>
|
||||
<div>
|
||||
<h4 className="font-bold text-sm dark:text-white hover:underline cursor-pointer">
|
||||
{post.author?.firstName} {post.author?.lastName}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-dark-600">
|
||||
{dayjs(post.createdAt).fromNow()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-4 pb-3 text-sm dark:text-gray-200">
|
||||
<p>{post.caption}</p>
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
{post.media_images && post.media_images.length > 0 && (
|
||||
<div className="w-full">
|
||||
<ImageField
|
||||
name="post-image"
|
||||
image={post.media_images}
|
||||
className="w-full h-auto max-h-[500px] overflow-hidden bg-gray-100 dark:bg-dark-900"
|
||||
imageClassName="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="p-4 flex items-center justify-between border-t border-gray-50 dark:border-dark-700">
|
||||
<div className="flex items-center space-x-6">
|
||||
<button
|
||||
onClick={handleLike}
|
||||
className={`flex items-center space-x-2 transition-colors ${isLiked ? 'text-rose-500' : 'text-gray-500 dark:text-dark-600 hover:text-rose-500'}`}
|
||||
>
|
||||
<BaseIcon path={isLiked ? mdiHeart : mdiHeartOutline} size="20" />
|
||||
<span className="text-xs font-semibold">{likeCount}</span>
|
||||
</button>
|
||||
|
||||
<button className="flex items-center space-x-2 text-gray-500 dark:text-dark-600 hover:text-indigo-500 transition-colors">
|
||||
<BaseIcon path={mdiCommentOutline} size="20" />
|
||||
<span className="text-xs font-semibold">{post.comment_count || 0}</span>
|
||||
</button>
|
||||
|
||||
<button className="flex items-center space-x-2 text-gray-500 dark:text-dark-600 hover:text-green-500 transition-colors">
|
||||
<BaseIcon path={mdiShareVariantOutline} size="20" />
|
||||
<span className="text-xs font-semibold">{post.share_count || 0}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-400">
|
||||
{post.view_count || 0} views
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostCard;
|
||||
92
frontend/src/components/Socio/TrendingBar.tsx
Normal file
92
frontend/src/components/Socio/TrendingBar.tsx
Normal file
@ -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 (
|
||||
<div className="hidden lg:block w-80 space-y-6 shrink-0 h-screen sticky top-16 overflow-y-auto aside-scrollbars-[slate] dark:aside-scrollbars-dark">
|
||||
{/* Trending Section */}
|
||||
<div className="bg-white dark:bg-dark-800 rounded-xl shadow-sm border border-gray-100 dark:border-dark-700 p-5">
|
||||
<h3 className="font-bold text-lg mb-4 dark:text-white">Trending for you</h3>
|
||||
<div className="space-y-4">
|
||||
{trending.map((item) => (
|
||||
<div key={item.tag} className="flex items-center justify-between group cursor-pointer">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-gray-100 dark:bg-dark-900 p-2 rounded-lg text-gray-500 group-hover:bg-indigo-50 group-hover:text-indigo-600 transition-colors">
|
||||
<BaseIcon path={mdiPound} size="18" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold dark:text-gray-200 group-hover:text-indigo-600 transition-colors">
|
||||
#{item.tag}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-dark-600">
|
||||
{item.count} posts
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button className="w-full mt-6 py-2 text-sm font-semibold text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded-lg transition-colors">
|
||||
Show more
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Suggested Users */}
|
||||
<div className="bg-white dark:bg-dark-800 rounded-xl shadow-sm border border-gray-100 dark:border-dark-700 p-5">
|
||||
<h3 className="font-bold text-lg mb-4 dark:text-white">Who to follow</h3>
|
||||
<div className="space-y-4">
|
||||
{suggestions.map((user) => (
|
||||
<div key={user.handle} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 rounded-full bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 font-bold text-sm">
|
||||
{user.avatar}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold dark:text-gray-200 leading-tight">
|
||||
{user.name}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-dark-600 leading-tight">
|
||||
{user.handle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button className="text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 p-2 rounded-full transition-colors">
|
||||
<BaseIcon path={mdiAccountPlusOutline} size="20" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button className="w-full mt-6 py-2 text-sm font-semibold text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded-lg transition-colors">
|
||||
Show more
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Footer Links */}
|
||||
<div className="px-5 text-xs text-gray-500 dark:text-dark-600 flex flex-wrap gap-x-4 gap-y-2">
|
||||
<span>Terms of Service</span>
|
||||
<span>Privacy Policy</span>
|
||||
<span>Cookie Policy</span>
|
||||
<span>Accessibility</span>
|
||||
<span>Ads info</span>
|
||||
<span>© 2026 Socio</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrendingBar;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
99
frontend/src/pages/feed.tsx
Normal file
99
frontend/src/pages/feed.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Home Feed')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionMain>
|
||||
<div className="flex flex-col lg:flex-row gap-6 max-w-6xl mx-auto">
|
||||
{/* Main Feed Content */}
|
||||
<div className="flex-1 max-w-2xl mx-auto w-full">
|
||||
|
||||
{/* Stories Bar */}
|
||||
<div className="flex items-center space-x-4 mb-8 overflow-x-auto pb-4 no-scrollbar">
|
||||
{stories.map((story) => (
|
||||
<div key={story.id} className="flex flex-col items-center flex-shrink-0 cursor-pointer group">
|
||||
<div className={`w-16 h-16 rounded-full p-[2px] mb-1 ring-2 ${story.isMe ? 'ring-indigo-200 dark:ring-indigo-900' : 'ring-indigo-500'}`}>
|
||||
<div className="w-full h-full rounded-full border-2 border-white dark:border-dark-800 overflow-hidden bg-gray-100 dark:bg-dark-900 flex items-center justify-center font-bold text-indigo-600">
|
||||
{story.isMe && currentUser ? (
|
||||
<UserAvatar username={currentUser.firstName} className="w-full h-full" />
|
||||
) : (
|
||||
story.avatar
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[10px] font-semibold text-gray-600 dark:text-dark-600 group-hover:text-indigo-600 transition-colors">
|
||||
{story.name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Create Post Widget */}
|
||||
<CreatePost onPostCreated={() => setRefetch(prev => prev + 1)} />
|
||||
|
||||
{/* Posts List */}
|
||||
{loading && !posts.length ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{posts.map((post: any) => (
|
||||
<PostCard key={post.id} post={post} />
|
||||
))}
|
||||
|
||||
{!loading && posts.length === 0 && (
|
||||
<div className="text-center py-20 bg-white dark:bg-dark-800 rounded-xl shadow-sm border border-gray-100 dark:border-dark-700">
|
||||
<p className="text-gray-500">No posts yet. Be the first to post something!</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Sidebar - Trending & Suggestions */}
|
||||
<TrendingBar />
|
||||
</div>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
FeedPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||
};
|
||||
|
||||
export default FeedPage;
|
||||
@ -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 Social App'
|
||||
|
||||
// Fetch Pexels image/video
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const image = await getPexelsImage();
|
||||
const video = await getPexelsVideo();
|
||||
setIllustrationImage(image);
|
||||
setIllustrationVideo(video);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const imageBlock = (image) => (
|
||||
<div
|
||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
||||
style={{
|
||||
backgroundImage: `${
|
||||
image
|
||||
? `url(${image?.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
>
|
||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={image?.photographer_url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Photo by {image?.photographer} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const videoBlock = (video) => {
|
||||
if (video?.video_files?.length > 0) {
|
||||
return (
|
||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
||||
<video
|
||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
>
|
||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={video?.user?.url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Video by {video.user.name} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
};
|
||||
const title = 'Socio';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
contentPosition === 'background'
|
||||
? {
|
||||
backgroundImage: `${
|
||||
illustrationImage
|
||||
? `url(${illustrationImage.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<div className="min-h-screen bg-slate-50 dark:bg-dark-900 overflow-hidden">
|
||||
<Head>
|
||||
<title>{getPageTitle('Starter Page')}</title>
|
||||
<title>{getPageTitle('Welcome to Socio')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div
|
||||
className={`flex ${
|
||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||
} min-h-screen w-full`}
|
||||
>
|
||||
{contentType === 'image' && contentPosition !== 'background'
|
||||
? imageBlock(illustrationImage)
|
||||
: null}
|
||||
{contentType === 'video' && contentPosition !== 'background'
|
||||
? videoBlock(illustrationVideo)
|
||||
: null}
|
||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||
<CardBoxComponentTitle title="Welcome to your Socio Social App app!"/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
||||
<p className='text-center '>For guides and documentation please check
|
||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
||||
{/* Navigation */}
|
||||
<nav className="flex items-center justify-between px-6 py-4 max-w-7xl mx-auto">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-10 h-10 bg-indigo-600 rounded-xl flex items-center justify-center text-white font-black text-2xl italic shadow-lg shadow-indigo-500/30">
|
||||
S
|
||||
</div>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
href='/login'
|
||||
label='Login'
|
||||
color='info'
|
||||
className='w-full'
|
||||
/>
|
||||
|
||||
</BaseButtons>
|
||||
</CardBox>
|
||||
<span className="text-2xl font-black tracking-tight dark:text-white uppercase">ocio</span>
|
||||
</div>
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
<div className="flex items-center space-x-6">
|
||||
<Link href="/login" className="text-sm font-bold text-gray-600 dark:text-gray-300 hover:text-indigo-600 transition-colors">
|
||||
Sign in
|
||||
</Link>
|
||||
<BaseButton
|
||||
href="/register"
|
||||
label="Join Socio"
|
||||
color="info"
|
||||
className="rounded-xl font-bold shadow-md shadow-indigo-500/20"
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero Section */}
|
||||
<main className="max-w-7xl mx-auto px-6 pt-12 lg:pt-24 flex flex-col lg:flex-row items-center">
|
||||
<div className="lg:w-1/2 space-y-8 text-center lg:text-left z-10">
|
||||
<div className="inline-flex items-center px-4 py-2 bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 rounded-full text-xs font-bold tracking-widest uppercase">
|
||||
✨ Connect, Share, and Discover
|
||||
</div>
|
||||
<h1 className="text-5xl lg:text-7xl font-black text-slate-900 dark:text-white leading-[1.1]">
|
||||
The Social Network for <span className="text-transparent bg-clip-text bg-gradient-to-r from-indigo-600 to-violet-600 italic">Tomorrow.</span>
|
||||
</h1>
|
||||
<p className="text-lg text-slate-600 dark:text-slate-400 max-w-xl mx-auto lg:mx-0">
|
||||
Socio brings your friends, interests, and world together in one place. Experience the next generation of social interaction.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center lg:justify-start space-y-4 sm:space-y-0 sm:space-x-4">
|
||||
<BaseButton
|
||||
href="/register"
|
||||
label="Start Creating"
|
||||
color="info"
|
||||
icon={mdiArrowRight}
|
||||
className="w-full sm:w-auto px-8 py-4 rounded-2xl text-lg font-bold shadow-xl shadow-indigo-500/30"
|
||||
/>
|
||||
<button className="flex items-center space-x-2 px-8 py-4 text-slate-600 dark:text-slate-300 font-bold hover:text-indigo-600 transition-colors group">
|
||||
<BaseIcon path={mdiPlayCircleOutline} size="24" className="group-hover:scale-110 transition-transform" />
|
||||
<span>Watch Demo</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center lg:justify-start space-x-8 pt-8 opacity-60 grayscale hover:grayscale-0 transition-all">
|
||||
<div className="text-2xl font-black italic">FACEBOOK</div>
|
||||
<div className="text-2xl font-black italic">INSTAGRAM</div>
|
||||
<div className="text-2xl font-black italic">TIKTOK</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative Visuals */}
|
||||
<div className="lg:w-1/2 mt-12 lg:mt-0 relative">
|
||||
<div className="relative z-10 transform lg:rotate-2 lg:translate-x-12 hover:rotate-0 transition-transform duration-700">
|
||||
<div className="bg-white dark:bg-dark-800 p-4 rounded-[2.5rem] shadow-2xl shadow-indigo-500/20 border border-slate-100 dark:border-dark-700 w-full max-w-[400px] mx-auto overflow-hidden">
|
||||
<div className="h-64 bg-slate-100 dark:bg-dark-900 rounded-[2rem] mb-4 overflow-hidden relative group">
|
||||
<img
|
||||
src="https://images.pexels.com/photos/3194521/pexels-photo-3194521.jpeg?auto=compress&cs=tinysrgb&w=800"
|
||||
alt="Sample content"
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent flex items-end p-6">
|
||||
<p className="text-white font-bold text-lg">Experience more together.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-2 mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 rounded-full bg-indigo-100 dark:bg-indigo-900/50 flex items-center justify-center text-indigo-600 font-bold">A</div>
|
||||
<div>
|
||||
<div className="h-2 w-24 bg-slate-100 dark:bg-dark-900 rounded mb-2"></div>
|
||||
<div className="h-2 w-16 bg-slate-100 dark:bg-dark-900 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-8 h-8 rounded-full bg-rose-50 dark:bg-rose-900/20 flex items-center justify-center text-rose-500">❤️</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background Blobs */}
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[120%] h-[120%] bg-indigo-500/10 dark:bg-indigo-500/5 blur-[100px] rounded-full z-0"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="mt-24 border-t border-slate-100 dark:border-dark-800 py-12 px-6">
|
||||
<div className="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between space-y-6 md:space-y-0">
|
||||
<p className="text-slate-500 text-sm">© 2026 Socio App. All rights reserved.</p>
|
||||
<div className="flex items-center space-x-8 text-sm font-semibold text-slate-500">
|
||||
<Link href="/privacy-policy" className="hover:text-indigo-600 transition-colors">Privacy</Link>
|
||||
<Link href="/terms" className="hover:text-indigo-600 transition-colors">Terms</Link>
|
||||
<Link href="/login" className="hover:text-indigo-600 transition-colors">Login to Admin</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||
SocioLanding.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user