Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Created June 11, 2025 12:40
Show Gist options
  • Save sunmeat/b10add8c204b4ca33abe5f3643013106 to your computer and use it in GitHub Desktop.
Save sunmeat/b10add8c204b4ca33abe5f3643013106 to your computer and use it in GitHub Desktop.
reactagram v.0.1 - useParams, useNavigate, useLocation, запросы
App.jsx:
import {
BrowserRouter,
Routes,
Route,
NavLink,
Link,
Outlet,
Navigate,
useParams,
useNavigate,
useLocation
} from 'react-router';
import './App.css';
import {useState, useEffect, createContext, useContext, useMemo, useRef} from "react";
const UsersContext = createContext();
export function UsersProvider({children}) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const cachedUsersRef = useRef(null);
const [, forceUpdate] = useState(0);
useEffect(() => {
async function fetchUsers() {
try {
setLoading(true);
setError(null);
const response = await fetch('https://randomuser.me/api/?results=1000');
// загрузка данных через API с useEffect
if (!response.ok) {
throw new Error('Ошибка загрузки пользователей');
}
const data = await response.json();
if (!cachedUsersRef.current) {
cachedUsersRef.current = data.results;
forceUpdate(n => n + 1);
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchUsers();
}, []);
return (
<UsersContext.Provider
value={{
users: cachedUsersRef.current || [],
loading,
error,
}}
>
{children}
</UsersContext.Provider>
);
}
export function useUsers() {
return useContext(UsersContext);
}
function Home() {
const {users, loading: usersLoading, error: usersError} = useUsers();
const [cachedUsers, setCachedUsers] = useState(null);
useEffect(() => {
if (!usersLoading && users?.length && !cachedUsers) {
setCachedUsers(users);
}
// загрузка данных через API с useEffect
}, [usersLoading, users, cachedUsers]);
if (!cachedUsers) {
if (usersError) {
return (
<div className="page-content">
<h1 className="page-title">Ошибка</h1>
<p className="about-text">{usersError}</p>
</div>
);
}
return (
<div className="page-content">
<h1 className="page-title">Загрузка...</h1>
</div>
);
}
const getUserById = (id) => {
const index = id - 1;
return cachedUsers[index] ?? null;
};
const user1 = getUserById(1);
const user2 = getUserById(2);
// url /users/1 показывает профиль конкретного пользователя
return (
<div className="page-content">
<h1 className="page-title home-title">Лента</h1>
<div className="post">
<div className="post-header">
<div className="post-author-wrapper">
<span className="post-avatar"></span>
<Link to="/users/1" className="post-author">
{user1?.login.username ?? "Неизвестный"}
</Link>
</div>
<span className="post-time">1м назад</span>
</div>
<div className="post-content">Красивый закат в Одессе! #природа #море</div>
<div className="post-actions">
<button className="action-button like">Лайк</button>
<Link to="/messages" className="action-button comment">Комментировать</Link>
</div>
</div>
<div className="post">
<div className="post-header">
<div className="post-author-wrapper">
<span className="post-avatar"></span>
<Link to="/users/2" className="post-author">
{user2?.login.username ?? "Неизвестный"}
</Link>
</div>
<span className="post-time">2м назад</span>
</div>
<div className="post-content">Вылазка на Говерлу! #путешествия #горы</div>
<div className="post-actions">
<button className="action-button like">Лайк</button>
<Link to="/messages" className="action-button comment">Комментировать</Link>
</div>
</div>
<div className="link-group">
<Link to="/profile" className="page-link">Перейти к профилю</Link>
<Link to="/search" className="page-link">Найти друзей</Link>
</div>
</div>
);
}
function UserProfile() {
const {id} = useParams(); // хук useParams получает параметры из URL
const {users, loading: usersLoading, error: usersError} = useUsers();
const userId = parseInt(id, 10);
const user = useMemo(() => {
if (!Array.isArray(users) || users.length === 0) return null;
if (isNaN(userId) || userId < 1 || userId > users.length) return null;
return users[userId - 1];
}, [userId, users]);
if (usersLoading) {
return (
<div className="page-content">
<h1 className="page-title">Загрузка...</h1>
</div>
);
}
if (usersError) {
return (
<div className="page-content">
<h1 className="page-title">Ошибка</h1>
<p className="about-text">{usersError}</p>
<div className="link-group">
<Link to="/" className="page-link">Вернуться в ленту</Link>
</div>
</div>
);
}
if (!user) {
return (
<div className="page-content">
<h1 className="page-title">Пользователь не найден</h1>
<div className="link-group">
<Link to="/" className="page-link">Вернуться в ленту</Link>
</div>
</div>
);
}
// {'url /users/:id показывает профиль конкретного пользователя'}
return (
<div className="page-content">
<h1 className="page-title profile-title">Профиль пользователя #{id}</h1>
<div className="profile-info">
<div className="profile-header">
<div className="profile-avatar">
<img
src={user.picture.large}
alt="Avatar"
style={{width: "100%", height: "100%", borderRadius: "50%"}}
/>
</div>
<div className="profile-details">
<span className="profile-username">{user.login.username}</span>
<p>{user.name.first} {user.name.last}</p>
<p>{user.email}</p>
<p>{user.location.city}, {user.location.country}</p>
</div>
</div>
<div className="profile-stats">
<span>{Math.floor(Math.random() * 100)} постов</span>
<span>{Math.floor(Math.random() * 1000)} подписчиков</span>
<span>{Math.floor(Math.random() * 500)} подписок</span>
</div>
</div>
<div className="link-group">
<Link to="/" className="page-link">Вернуться в ленту</Link>
<Link to="/search" className="page-link">Поиск</Link>
</div>
</div>
);
}
function ProfilePosts() {
const [activePost, setActivePost] = useState(0);
const posts = [
"Поехать согласилась только крыша…",
"Пойди приляг. Желательно на рельсы.",
"Контрольный выстрел мало что исправил…",
"Бежать за пивом помешали ноги.",
"Кругом такое!.. Хоть иди участвуй.",
"Сегодня дел полно! Во-первых, завтрак…"
];
return (
<div className="profile-content">
<h2>Мои посты</h2>
{posts.map((post, index) => (
<div
key={index}
className={`post ${activePost === index ? 'active' : ''}`}
onClick={() => setActivePost(index)}
>
{post}
</div>
))}
</div>
);
}
function ProfileSettings() {
return (
<div className="profile-content">
<h2>Настройки профиля</h2>
<h4>Тут какие-нибудь настройки профиля</h4>
</div>
);
}
function Profile() {
return (
<div className="page-content">
<h1 className="page-title profile-title">Мой профиль</h1>
<div className="profile-info">
<div className="profile-header">
<div className="profile-avatar"></div>
<div className="profile-details">
<span className="profile-username">sunmeat</span>
<button className="profile-edit">Редактировать профиль</button>
</div>
</div>
<div className="profile-stats">
<span>6 постов</span>
<span>1.2K подписчиков</span>
<span>350 подписок</span>
</div>
</div>
<div className="profile-nav">
<NavLink to="posts"
className={({isActive}) => isActive ? "nav-link active" : "nav-link"}>Посты</NavLink>
<NavLink to="settings"
className={({isActive}) => isActive ? "nav-link active" : "nav-link"}>Настройки</NavLink>
</div>
<Outlet/>
<div className="link-group">
<Link to="/" className="page-link">Вернуться в ленту</Link>
<Link to="/messages" className="page-link">Сообщения</Link>
<Link to="/search" className="page-link">Поиск</Link>
</div>
</div>
);
}
function MessagesInbox() {
// url /users/793 показывает профиль конкретного пользователя
return (
<div className="messages-content">
<h2>Входящие</h2>
<div className="message">
<div className="message-sender-wrapper">
<span className="message-avatar"></span>
<Link to="/users/215" className="message-sender">Вася</Link>
</div>
<span className="message-preview">Шо ты там?</span>
</div>
<div className="message">
<div className="message-sender-wrapper">
<span className="message-avatar"></span>
<Link to="/users/793" className="message-sender">Лена</Link>
</div>
<span className="message-preview">Ты помнишь, что у меня завтра важное?</span>
</div>
</div>
);
}
function MessagesSent() {
return (
<div className="messages-content">
<h2>Отправленные</h2>
<div className="message">
<div className="message-sender-wrapper">
<span className="message-avatar"></span>
<Link to="/profile" className="message-sender">sunmeat</Link>
</div>
<span className="message-preview">Да, помню, удачи!</span>
</div>
</div>
);
}
function Messages() {
return (
<div className="page-content">
<h1 className="page-title messages-title">Сообщения</h1>
<div className="messages-nav">
<NavLink to="inbox"
className={({isActive}) => isActive ? "nav-link active" : "nav-link"}>Входящие</NavLink>
<NavLink to="sent"
className={({isActive}) => isActive ? "nav-link active" : "nav-link"}>Отправленные</NavLink>
</div>
<Outlet/>
<div className="link-group">
<Link to="/" className="page-link">Вернуться в ленту</Link>
<Link to="/profile" className="page-link">Профиль</Link>
</div>
</div>
);
}
function Search() {
const [searchTerm, setSearchTerm] = useState('');
const [filteredUsers, setFilteredUsers] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const navigate = useNavigate(); // useNavigate позволяет программно переходить между страницами
const location = useLocation(); // useLocation предоставляет доступ к текущему URL и его частям
const {users, loading: usersLoading, error: usersError} = useUsers();
const searchParams = new URLSearchParams(location.search);
// запросы позволяют обрабатывать дополнительные параметры в URL
const query = searchParams.get('q') || '';
useEffect(() => {
if (query) {
setSearchTerm(query);
handleSearch(query);
}
}, [query, users]);
const handleSearch = (term) => {
if (!term.trim()) {
setFilteredUsers([]);
return;
}
setSearchLoading(true);
setTimeout(() => {
const results = users.filter(user => {
const firstName = user.name.first.toLowerCase();
const lastName = user.name.last.toLowerCase();
const username = user.login.username.toLowerCase();
const searchTerm = term.toLowerCase();
return firstName.includes(searchTerm) ||
lastName.includes(searchTerm) ||
username.includes(searchTerm);
});
setFilteredUsers(results);
setSearchLoading(false);
}, 300);
};
const handleInputChange = (e) => {
const value = e.target.value;
setSearchTerm(value);
const params = new URLSearchParams();
if (value.trim())
params.set('q', value);
const queryString = params.toString();
navigate(`/search${queryString ? `?${queryString}` : ''}`, {replace: true});
// useNavigate позволяет программно переходить между страницами
};
if (usersLoading) {
return (
<div className="page-content">
<h1 className="page-title search-title">Поиск</h1>
<p>Загрузка базы пользователей...</p>
</div>
);
}
if (usersError) {
return (
<div className="page-content">
<h1 className="page-title search-title">Поиск</h1>
<p>Ошибка загрузки пользователей: {usersError}</p>
<div className="link-group">
<Link to="/" className="page-link">Вернуться в ленту</Link>
</div>
</div>
);
}
return (
<div className="page-content">
<h1 className="page-title search-title">Поиск</h1>
<div className="search-bar">
<input
type="text"
placeholder="Поиск пользователей..."
value={searchTerm}
onChange={handleInputChange}
/>
</div>
{searchLoading && <p>Поиск...</p>}
{filteredUsers.length > 0 && (
<div className="search-results">
{filteredUsers.map((user, index) => {
const originalIndex = users.findIndex(u => u.login.uuid === user.login.uuid) + 1;
return (
<div key={user.login.uuid} className="search-result">
<div className="result-wrapper">
<span className="result-avatar">
<img src={user.picture.thumbnail} alt="Avatar"
style={{width: '40px', height: '40px', borderRadius: '50%'}}/>
</span>
<Link to={`/users/${originalIndex}`}
className="result-item">{user.login.username}</Link>
</div>
<span
className="result-bio">{user.name.first} {user.name.last} - {user.location.city}</span>
</div>
);
})}
</div>
)}
{searchTerm && !searchLoading && filteredUsers.length === 0 && users.length > 0 && (
<p>Пользователи не найдены</p>
)}
<div className="link-group">
<Link to="/" className="page-link">Вернуться в ленту</Link>
<Link to="/profile" className="page-link">Профиль</Link>
</div>
</div>
);
}
function About() {
return (
<div className="page-content">
<h1 className="page-title about-title">Reactagram</h1>
<p className="about-text">Добро пожаловать в наше приложение! Делись моментами, находи друзей и
вдохновляйся!</p>
<div className="link-group">
<Link to="/" className="page-link">Вернуться в ленту</Link>
<Link to="/profile" className="page-link">Профиль</Link>
<Link to="/search" className="page-link">Поиск</Link>
</div>
</div>
);
}
function NotFound() {
return (
<div className="page-content">
<h1 className="page-title">404 - Страница не найдена</h1>
<p className="about-text">Извините, запрашиваемая страница не существует.</p>
<div className="link-group">
<Link to="/" className="page-link">Вернуться в ленту</Link>
<Link to="/profile" className="page-link">Профиль</Link>
<Link to="/search" className="page-link">Поиск</Link>
</div>
</div>
);
}
function App() {
return (
<UsersProvider>
<BrowserRouter>
<nav className="nav">
<ul className="nav-list">
<li className="nav-item">
<NavLink to="/" className={({isActive}) => isActive ? "nav-link active" : "nav-link"}>
<span className="nav-icon home-icon"></span>
<span className="nav-text">Лента</span>
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/profile"
className={({isActive}) => isActive ? "nav-link active" : "nav-link"}>
<span className="nav-icon profile-icon"></span>
<span className="nav-text">Профиль</span>
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/messages"
className={({isActive}) => isActive ? "nav-link active" : "nav-link"}>
<span className="nav-icon messages-icon"></span>
<span className="nav-text">Сообщения</span>
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/search" className={({isActive}) => isActive ? "nav-link active" : "nav-link"}>
<span className="nav-icon search-icon"></span>
<span className="nav-text">Поиск</span>
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/about" className={({isActive}) => isActive ? "nav-link active" : "nav-link"}>
<span className="nav-icon about-icon"></span>
<span className="nav-text">О нас</span>
</NavLink>
</li>
</ul>
</nav>
<main className="content">
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/users/:id" element={<UserProfile/>}/>
{'параметры делают маршруты динамическими (например, /users/:id'}
<Route path="/profile" element={<Profile/>}>
<Route path="posts" element={<ProfilePosts/>}/>
<Route path="settings" element={<ProfileSettings/>}/>
<Route index element={<Navigate to="posts" replace/>}/>
</Route>
<Route path="/messages" element={<Messages/>}>
<Route path="inbox" element={<MessagesInbox/>}/>
<Route path="sent" element={<MessagesSent/>}/>
<Route index element={<Navigate to="inbox" replace/>}/>
</Route>
<Route path="/search" element={<Search/>}/>
<Route path="/about" element={<About/>}/>
<Route path="*" element={<NotFound/>}/>
</Routes>
</main>
</BrowserRouter>
</UsersProvider>
);
}
export default App;
==========================================================================================================================
App.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: #fafafa;
min-height: 100vh;
color: #262626;
}
.nav {
background: #ffffff;
border-bottom: 1px solid #dbdbdb;
padding: 0.75rem 0;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
.nav-list {
list-style: none;
display: flex;
justify-content: center;
align-items: center;
gap: 2rem;
max-width: 935px;
margin: 0 auto;
}
.nav-item {
position: relative;
}
.nav-link {
display: flex;
align-items: center;
gap: 0.5rem;
color: #262626;
text-decoration: none;
font-size: 0.9rem;
font-weight: 500;
padding: 0.5rem 0.75rem;
border-radius: 8px;
transition: background-color 0.2s ease, color 0.2s ease;
}
.nav-link:hover,
.nav-link.active {
background-color: rgba(0, 149, 246, 0.1);
color: #0095f6;
}
.nav-icon {
display: inline-block;
width: 24px;
height: 24px;
background-size: cover;
background-position: center;
}
.home-icon {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>');
}
.profile-icon {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>');
}
.messages-icon {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>');
}
.search-icon {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>');
}
.about-icon {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v.01"></path><path d="M12 8v4"></path></svg>');
}
.nav-text {
display: inline;
}
.content {
max-width: 935px;
margin: 4rem auto 2rem;
padding: 1rem;
min-height: calc(100vh - 4rem);
}
.page-content {
display: flex;
flex-direction: column;
gap: 1.5rem;
padding: 1.5rem;
background: #ffffff;
border-radius: 8px;
border: 1px solid #dbdbdb;
}
.page-title {
font-size: 1.5rem;
font-weight: 600;
text-align: left;
color: #262626;
}
.home-title {
background: linear-gradient(45deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.profile-title {
background: linear-gradient(45deg, #405de6, #5851db, #833ab4, #c13584, #e1306c, #fd1d1d);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.messages-title {
background: linear-gradient(45deg, #ffdc80, #fcaf45, #f77737);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.search-title {
background: linear-gradient(45deg, #833ab4, #fd1d1d, #fcb045);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.about-title {
background: linear-gradient(45deg, #00ddeb, #26a0da, #0095f6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.post {
padding: 10px;
margin: 5px 0;
cursor: pointer;
background-color: #f9f9f9;
}
.post.active {
background-color: #e0f7fa;
border-left: 4px solid #007bff;
}
.post-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.post-author-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
}
.post-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(45deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888);
border: 2px solid #ffffff;
}
.post-author {
font-weight: 600;
color: #262626;
text-decoration: none;
}
.post-author:hover {
color: #0095f6;
}
.post-time {
color: #8e8e8e;
font-size: 0.8rem;
}
.post-content {
font-size: 0.9rem;
line-height: 1.4;
margin-bottom: 0.75rem;
}
.post-actions {
display: flex;
gap: 0.5rem;
}
.action-button {
padding: 0.5rem;
background: none;
border: none;
font-size: 0.85rem;
cursor: pointer;
color: #262626;
transition: color 0.2s ease;
}
.action-button:hover {
color: #0095f6;
}
.like::before {
content: '❤️';
margin-right: 0.25rem;
}
.comment::before {
content: '💬';
margin-right: 0.25rem;
}
.profile-info {
display: flex;
flex-direction: column;
gap: 1rem;
}
.profile-header {
display: flex;
align-items: center;
gap: 1rem;
}
.profile-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(45deg, #405de6, #5851db, #833ab4, #c13584, #e1306c, #fd1d1d);
border: 2px solid #ffffff;
}
.profile-details {
flex: 1;
}
.profile-username {
font-weight: 600;
font-size: 1.2rem;
}
.profile-edit {
padding: 0.5rem 1rem;
background: linear-gradient(45deg, #0095f6, #00ddeb);
color: #ffffff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.85rem;
font-weight: 500;
transition: opacity 0.2s ease;
margin-left: 10px;
}
.profile-edit:hover {
opacity: 0.9;
}
.profile-stats {
display: flex;
gap: 1.5rem;
font-size: 0.9rem;
font-weight: 500;
}
.message {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border-bottom: 1px solid #dbdbdb;
}
.message-sender-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
}
.message-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(45deg, #f77737, #fcaf45, #ffdc80);
border: 2px solid #ffffff;
}
.message-sender {
font-weight: 600;
color: #262626;
text-decoration: none;
}
.message-sender:hover {
color: #0095f6;
}
.message-preview {
color: #8e8e8e;
font-size: 0.85rem;
}
.search-bar {
margin-bottom: 1rem;
}
.search-bar input {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid #dbdbdb;
border-radius: 12px;
font-size: 0.9rem;
background: #fafafa;
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.search-bar input:focus {
border-color: #0095f6;
box-shadow: 0 0 0 3px rgba(0, 149, 246, 0.1);
}
.search-bar input::placeholder {
color: #8e8e8e;
}
.search-results {
display: flex;
flex-direction: column;
gap: 0.75rem;
animation: fadeInUp 0.4s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.search-result {
padding: 1rem;
border: 1px solid #dbdbdb;
border-radius: 12px;
display: flex;
flex-direction: column;
gap: 0.5rem;
background: #ffffff;
transition: all 0.3s ease;
animation: slideInLeft 0.5s ease-out;
animation-fill-mode: both;
transform: translateX(-20px);
opacity: 0;
}
.search-result:nth-child(1) {
animation-delay: 0.1s;
}
.search-result:nth-child(2) {
animation-delay: 0.15s;
}
.search-result:nth-child(3) {
animation-delay: 0.2s;
}
.search-result:nth-child(4) {
animation-delay: 0.25s;
}
.search-result:nth-child(5) {
animation-delay: 0.3s;
}
.search-result:nth-child(n+6) {
animation-delay: 0.35s;
}
@keyframes slideInLeft {
to {
opacity: 1;
transform: translateX(0);
}
}
.search-result:hover {
border-color: #0095f6;
box-shadow: 0 4px 12px rgba(0, 149, 246, 0.15);
transform: translateY(-2px);
}
.result-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
}
.result-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(45deg, #833ab4, #fd1d1d, #fcb045);
border: 2px solid #ffffff;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.result-item {
font-weight: 600;
font-size: 1rem;
color: #262626;
text-decoration: none;
transition: color 0.2s ease;
}
.result-item:hover {
color: #0095f6;
}
.result-bio {
color: #8e8e8e;
font-size: 0.9rem;
margin-left: 60px;
padding-top: 0.25rem;
}
.about-text {
font-size: 0.9rem;
line-height: 1.5;
text-align: center;
color: #262626;
}
.link-group {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
justify-content: center;
}
.page-link {
display: inline-block;
padding: 0.6rem 1.2rem;
background: linear-gradient(45deg, #0095f6, #00ddeb);
color: #ffffff;
text-decoration: none;
font-size: 0.85rem;
font-weight: 500;
border-radius: 8px;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.page-link:hover {
opacity: 0.9;
transform: translateY(-2px);
}
.search-loading {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1rem;
color: #8e8e8e;
font-style: italic;
}
.search-loading::before {
content: '';
width: 16px;
height: 16px;
border: 2px solid #dbdbdb;
border-top: 2px solid #0095f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.no-results {
text-align: center;
padding: 2rem;
color: #8e8e8e;
font-style: italic;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (max-width: 768px) {
.nav-list {
gap: 0.5rem;
flex-wrap: wrap;
padding: 0 0.5rem;
}
.nav-link {
flex-direction: column;
gap: 0.25rem;
padding: 0.5rem;
}
.nav-text {
font-size: 0.75rem;
}
.page-content {
padding: 1rem;
}
.page-title {
font-size: 1.25rem;
}
.profile-header {
flex-direction: column;
align-items: flex-start;
}
.profile-avatar {
width: 60px;
height: 60px;
}
.link-group {
flex-direction: column;
align-items: center;
}
.search-result {
padding: 0.75rem;
}
.result-avatar {
width: 40px;
height: 40px;
}
.result-bio {
margin-left: 52px;
}
}
@media (max-width: 480px) {
.nav-icon {
width: 20px;
height: 20px;
}
.nav-text {
display: none;
}
.post-avatar, .message-avatar {
width: 28px;
height: 28px;
}
.result-avatar {
width: 36px;
height: 36px;
}
.result-bio {
margin-left: 48px;
}
.post-content, .about-text {
font-size: 0.85rem;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment