React Social Media App 静态页面部分
React Social Media App 静态页面部分
使用技术:
React
scss
react-router
原视频地址
这个项目是一个全栈项目,也是我的第一个 react 项目,以下为项目的静态页面部分的构建流程
初始化工作
下载文件
克隆一个初始仓库 github.com/safak/youtu…
git clone --single-branch -b react-mini https://github.com/safak/youtube2022.git
指定 react-mini 分支
再下载依赖 npm i
下载 sass
命令: npm i sass
构建文件结构
在 src
下创建 pages
文件夹
然后分别创建 home login profile register
文件夹
各文件下创建 .jsx
文件 和 scss
文件
在 App.js
文件中加入 Login
组件
import Login from './pages/login/login'
function App() {
return (
<div>
<Login />
</div>
);
}
export default App;
搭建
登录界面
登录界面的最终呈现效果是这样的
HTML 结构
因为使用了 scss,它可以做到直接在父类的样式中递进地写子类的样式,所以 HTML 结构中类的命名较少
-
基础的 HTML 结构
将整个盒子称作
card
,分为left
和right
两个部分<div className='login-page'> <div className='card'> <div className='left'> <h1>Hello World.</h1> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit.Libero cum, alias totam numquam ipsa exercitationem dignissimos,error nam, consequatur. </p> <span>Don't you have an account?</span> <button>Register</button> </div> <div className="right"> <h1>Login</h1> <form> <input type="text" placeholder='Username' /> <input type="password" placeholder="Password" /> <button>Login</button> </form> </div> </div> </div>
样式
根据HTML结构直接编写样式就行了
-
样式
.login { height: 100vh; background-color: rgb(193, 190, 255); display: flex; justify-content: center; align-items: center; .card { width: 50%; min-height: 600px; display: flex; background-color: #fff; border-radius: 10px; overflow: hidden; .left { display: flex; flex: 1; flex-direction: column; gap: 30px; background: linear-gradient(rgba(39, 11, 96, .5), rgba(39, 11, 96, .5)), url("https://i.328888.xyz/2023/03/20/PsE9H.jpeg") center; background-size: cover; padding: 50px; color: white; h1 { font-size: 100px; line-height: 100px; } p {} span { font-size: 14px; } button { width: 50%; padding: 10px; border: none; border-color: #fff; color: rebeccapurple; font-weight: bold; cursor: pointer; } } .right { display: flex; justify-content: center; flex: 1; padding: 50px; flex-direction: column; gap: 50px; h1 { color: #555; } form { display: flex; flex-direction: column; gap: 30px; input { border: none; border-bottom: 1px solid lightgray; padding: 20px 10px; } button { width: 50%; padding: 10px; border: none; background-color: #938eef; color: white; font-weight: bold; cursor: pointer; } } } } }
这其中有个细节,就是给 card 设置圆角,之后又给 left 设置背景图片,图片溢出挡住了圆角
解决方案是 overflow: hidden;
注册
HTML结构就相对于登录界面修改一点
-
HTML结构
import './register.scss' export default function Register() { return ( <div className='register'> <div className='card'> <div className='left'> <h1>Hello World.</h1> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit.Libero cum, alias totam numquam ipsa exercitationem dignissimos,error nam, consequatur. </p> <span>Do you have an account?</span> <button>Login</button> </div> <div className="right"> <h1>Register</h1> <form> <input type="text" placeholder='Username' /> <input type="email" placeholder='Email' /> <input type="password" placeholder="Password" /> <input type="text" placeholder='Name' /> <button>Register</button> </form> </div> </div> </div> ) }
样式就是修改一下背景图片,以及给 card
添加 flex-direction: row-reverse;
直接将左右转换
-
样式
.register { height: 100vh; background-color: rgb(193, 190, 255); display: flex; justify-content: center; align-items: center; .card { display: flex; flex-direction: row-reverse; width: 50%; min-height: 600px; background-color: #fff; border-radius: 10px; overflow: hidden; .left { display: flex; flex: 1; flex-direction: column; gap: 30px; background: linear-gradient(rgba(39, 11, 96, .5), rgba(39, 11, 96, .5)), url("https://i.328888.xyz/2023/03/20/P9BtZ.jpeg") center; background-size: cover; padding: 50px; color: white; h1 { font-size: 100px; line-height: 100px; } p {} span { font-size: 14px; } button { width: 50%; padding: 10px; border: none; border-color: #fff; color: rebeccapurple; font-weight: bold; cursor: pointer; } } .right { display: flex; justify-content: center; flex: 1; padding: 50px; flex-direction: column; gap: 50px; h1 { color: #555; } form { display: flex; flex-direction: column; gap: 30px; input { border: none; border-bottom: 1px solid lightgray; padding: 20px 10px; } button { width: 50%; padding: 10px; border: none; background-color: #938eef; color: white; font-weight: bold; cursor: pointer; } } } } }
使用 react-router
登录页面与注册页面相互切换
npm i react-router-dom
安装依赖
在 APP.js 中配置路由
import Login from './pages/login/login'
import Register from './pages/register/register'
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
const router = createBrowserRouter([
{
path: "/login",
element: <Login />
},
{
path: "/register",
element: <Register />
}
]);
function App() {
return (
<div>
<RouterProvider router={router} />
</div>
);
}
export default App;
在 Login.jsx
and Register.jsx
中导入 Link import {Link} from 'react-router-dom’
// Login.jsx
<Link to='/register'>
<button>Register</button>
</Link>
// Register.jsx
<Link to='/login'>
<button>Login</button>
</Link>
实现首页
初始化
首先构建新的文件结构
在 src
下添加 components
接着添加 leftBar rightBar navBar
构造主页的 HTML结构
const Layout = () => {
return (
<div>
<NavBar />
<div style={{ display: 'flex' }}>
<LeftBar />
<Outlet />
<RightBar />
</div>
</div>
)
}
更改路由配置
const router = createBrowserRouter([
**{
path: "/",
element: <Layout />,
children: [
{
path: '/',
element: <Home/>
},
{
path: '/profile/:id',
element: <Profile/>
}
]
},**
{
path: "/login",
element: <Login />
},
{
path: "/register",
element: <Register />
}
]);
重定向
当未登录时,应跳转到登录页面,而非首页
当然这里的判断是否登录需要与后端合作,现阶段仅是实现静态页面,所以下面只是展示重定向策略
创建一个函数判断是否登录,已登录就返回主页,未登录就重定向到登录页面
function App() {
**const currentUser = false;**
**const ProtectRoute = ({ children }) => {
if (!currentUser) {
return <Navigate to='/login' />
}
return children
}**
const router = createBrowserRouter([
{
path: "/",
**element: <ProtectRoute>
<Layout />
</ProtectRoute>,**
//..............................
}
navBar
我使用的是阿里巴巴矢量图标库的 icon 单个使用法
故在 navBar.scss 文件中引入 icon (链接必然会失效,仅展示)
@font-face {
font-family: 'iconfont';
/* Project id 3591478 */
src: url('//at.alicdn.com/t/c/font_3591478_6mxgs3fjb3h.woff2?t=1679327226324') format('woff2'),
url('//at.alicdn.com/t/c/font_3591478_6mxgs3fjb3h.woff?t=1679327226324') format('woff'),
url('//at.alicdn.com/t/c/font_3591478_6mxgs3fjb3h.ttf?t=1679327226324') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-webkit-text-stroke-width: 0.2px;
-moz-osx-font-smoothing: grayscale;
}
-
HTML 结构
import './navBar.scss'; import {Link} from 'react-router-dom' export default function NavBar() { return ( <div className='nav-bar'> <div className="left"> <Link to='/'> <span>lamasocial</span> </Link> <i className='iconfont home'></i> <i className="iconfont moon"></i> <i className="iconfont function"></i> <div className="search"> <i className="iconfont search-icon"></i> <input type="text" placeholder='search....' /> </div> </div> <div className="right"> <i className="iconfont profile"></i> <i className="iconfont email"></i> <i className="iconfont remind"></i> <div className="user"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-g7oj73.jpg"/> <span>John Doe</span> </div> </div> </div> ) }
navBar 分为左右部分,整体都使用弹性分布,左右在主轴 space-between
分布
这里使用了 object-fit
属性(新知识)
object-fit
CSS属性指定可替换元素(例如:<img>
或 <video>
)的内容应该如何适应到其使用高度和宽度确定的框。
navBar 还需要固定在顶部
.navBar {
**background-color: white;
position: sticky;
top: 0;
}**
-
样式
/* CDN 服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */ /* CDN 服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */ /* CDN 服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */ /* CDN 服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */ @font-face { font-family: 'iconfont'; /* Project id 3591478 */ src: url('//at.alicdn.com/t/c/font_3591478_q40gy84nnm.woff2?t=1679365108671') format('woff2'), url('//at.alicdn.com/t/c/font_3591478_q40gy84nnm.woff?t=1679365108671') format('woff'), url('//at.alicdn.com/t/c/font_3591478_q40gy84nnm.ttf?t=1679365108671') format('truetype'); } .iconfont { font-family: "iconfont" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -webkit-text-stroke-width: 0.2px; -moz-osx-font-smoothing: grayscale; } .nav-bar { display: flex; align-items: center; justify-content: space-between; padding: 10px 20px; height: 50px; border-bottom: 1px solid lightgray; background-color: white; position: sticky; top: 0; .left { display: flex; align-items: center; gap: 30px; span { font-weight: bold; font-size: 20px; color: darkblue; } .search { display: flex; align-items: center; gap: 10px; border: 1px solid lightgray; border-radius: 5px; padding: 5px; input { border: none; width: 500px; } } } .right { display: flex; align-items: center; gap: 20px; .user { display: flex; align-items: center; gap: 10px; font-weight: 500; img { width: 30px; height: 30px; border-radius: 50%; object-fit: cover; } span {} } } }
leftBar
这个组件需要许多作为图标的小图片,所以在 src
下创建 assets/imgs
文件夹保存小图片资源
这个组件结构的想法是,用一个 container
包裹整个,其中有三个 menu
,每一项就是 item
当然有个特殊的就是第一个 menu
有个 user
图片路径使用的是引入保存在变量中
-
HTML
import './leftBar.scss'; import Friends from '../../assets/imgs/泡泡.png'; import Groups from '../../assets/imgs/蛋糕.png'; import Gallery from '../../assets/imgs/相机.png'; import Watch from '../../assets/imgs/眼睛.png'; import Memories from '../../assets/imgs/闹钟.png'; import Market from '../../assets/imgs/购物.png'; import Events from '../../assets/imgs/风筝.png'; import Gaming from '../../assets/imgs/纸飞机.png'; import Video from '../../assets/imgs/礼花.png'; import Message from '../../assets/imgs/对话框.png'; import Fundraiser from '../../assets/imgs/花.png'; import Tutorials from '../../assets/imgs/彩铅.png'; import Courses from '../../assets/imgs/煎蛋.png'; export default function LeftBar() { return ( <div className='left-bar'> <div className="container"> <div className="menu"> <div className="user"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-g7oj73.jpg"/> <span>John Doe</span> </div> <div className="item"> <img src={Friends}/> <span>Friends</span> </div> <div className="item"> <img src={Groups}/> <span>Groups</span> </div> <div className="item"> <img src={Market}/> <span>Market</span> </div> <div className="item"> <img src={Watch}/> <span>Watch</span> </div> <div className="item"> <img src={Memories}/> <span>Memories</span> </div> </div> <hr/> <div className="menu"> <span>Your shortcuts</span> <div className="item"> <img src={Events}/> <span>Events</span> </div> <div className="item"> <img src={Gaming}/> <span>Gaming</span> </div> <div className="item"> <img src={Gallery}/> <span>Gallery</span> </div> <div className="item"> <img src={Video}/> <span>Video</span> </div> <div className="item"> <img src={Message}/> <span>Message</span> </div> </div> <hr/> <div className="menu"> <span>Others</span> <div className="item"> <img src={Fundraiser}/> <span>Fundraiser</span> </div> <div className="item"> <img src={Tutorials}/> <span>Tutorials</span> </div> <div className="item"> <img src={Courses}/> <span>Courses</span> </div> </div> </div> </div> ) }
依然使用弹性布局,container 主轴方向改为竖轴
整个主页部分打算的也是弹性布局,而 leftBar
占 flex: 2
leftBar 依然需要固定在左侧,但是 leftBar 的内容有点多,很可能溢出,但是它被固定了,就不会随着主页的滚动而显示溢出的部分
这时就使用 overflow: scroll;
即可,取消滚动条 &::-webkit-scrollbar {display: none; }
(因为这对 rightBar
也生效,所以直接复制粘贴到 rightBar.scss
中)
-
样式
img { width: 30px; } .left-bar { **flex: 2;** position: sticky; top: 50px; height: calc(100vh - 70px); overflow: scroll; &::-webkit-scrollbar { display: none; } .container { padding: 20px; hr { margin: 20px 0; border: none; height: .5px; background-color: lightgray; } .menu { display: flex; flex-direction: column; gap: 20px; span { font-size: 12px; } .user { display: flex; align-items: center; gap: 10px; img { height: 30px; border-radius: 50%; object-fit: cover; } span { font-size: 14px; } } .item { display: flex; align-items: center; gap: 10px; img {} span { font-size: 14px; } } } } }
rightBar
rightBar
有三个 item
,item
中内容都与 user
有关
-
HTML 结构
import './rightBar.scss'; export default function RightBar() { return ( <div className='right-bar'> <div className="container"> <div className="item"> <span>Suggestions for you</span> <div className="user"> <div className="user-info"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-1k97z3.png"/> <span>Jane Doe</span> </div> <div className="buttons"> <button>follow</button> <button>dismiss</button> </div> </div> <div className="user"> <div className="user-info"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-1k97z3.png"/> <span>Jane Doe</span> </div> <div className="buttons"> <button>follow</button> <button>dismiss</button> </div> </div> </div> <div className="item"> <span>Latest Activities</span> <div className="user"> <div className="user-info"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-1k97z3.png"/> <p> <span>Jane Doe</span> changed their cover picture </p> </div> <span>1 min ago</span> </div> <div className="user"> <div className="user-info"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-1k97z3.png"/> <p> <span>Jane Doe</span> changed their cover picture </p> </div> <span>1 min ago</span> </div> <div className="user"> <div className="user-info"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-1k97z3.png"/> <p> <span>Jane Doe</span> changed their cover picture </p> </div> <span>1 min ago</span> </div> <div className="user"> <div className="user-info"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-1k97z3.png"/> <p> <span>Jane Doe</span> changed their cover picture </p> </div> <span>1 min ago</span> </div> </div> <div className="item"> <span>Online Friends</span> <div className="user"> <div className="user-info"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-1k97z3.png"/> <div className="online"></div> <span>Jane Doe</span> </div> </div> <div className="user"> <div className="user-info"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-1k97z3.png"/> <div className="online"></div> <span>Jane Doe</span> </div> </div> <div className="user"> <div className="user-info"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-1k97z3.png"/> <div className="online"></div> <span>Jane Doe</span> </div> </div> <div className="user"> <div className="user-info"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-1k97z3.png"/> <div className="online"></div> <span>Jane Doe</span> </div> </div> <div className="user"> <div className="user-info"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-1k97z3.png"/> <div className="online"></div> <span>Jane Doe</span> </div> </div> <div className="user"> <div className="user-info"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-1k97z3.png"/> <div className="online"></div> <span>Jane Doe</span> </div> </div> </div> </div> </div> ) }
盒子阴影使用的是 www.cssmatic.com/box-shadow 网站快捷生成的
需要注意的是在线好友 item 中头像上的绿点是定位实现的
-
样式
.right-bar { flex: 3; position: sticky; top: 50px; height: calc(100vh - 70px); overflow: scroll; &::-webkit-scrollbar { display: none; } .container { padding: 20px; .item { -webkit-box-shadow: -1px 5px 19px 0px rgba(194, 188, 194, 0.31); -moz-box-shadow: -1px 5px 19px 0px rgba(194, 188, 194, 0.31); box-shadow: -1px 5px 19px 0px rgba(194, 188, 194, 0.31); padding: 20px; margin-bottom: 20px; span { color: gray; } .user { display: flex; align-items: center; justify-content: space-between; margin: 20px 0; .user-info { display: flex; align-items: center; gap: 20px; position: relative; img { height: 40px; width: 40px; border-radius: 50%; object-fit: cover; } .online { height: 12px; width: 12px; border-radius: 50%; background-color: limegreen; position: absolute; top: 0; left: 30px; } p { color: gray; } span { font-weight: 500; color: #000; } } .buttons { display: flex; align-items: center; gap: 10px; button { border: none; padding: 5px; color: white; cursor: pointer; &:first-child { background-color: #5271ff; } &:last-child { background-color: #f0544f; } } } } } } }
设置光亮与夜间模式
设置光亮与夜间模式总是意味着要写两套样式,但是如果使用 scss
的函数,则只需要写两套关于颜色的变量
在 style.scss
中设置变量与函数
$themes: (
light: (textColor: #000,
bg: #fff,
logo: darkblue,
bgSoft: #f6f3f3,
textColorSoft: #555,
border: lightgray),
dark: (textColor: whitesmoke,
bg: #222,
logo: white,
bgSoft: #333,
textColorSoft: lightgray,
border: #444)
);
@mixin themify($themes) {
@each $theme,
$map in $themes {
.theme-#{$theme} & {
$theme-map: () !global;
@each $key,
$submap in $map {
$value: map-get(map-get($themes, $theme), "#{$key}");
$theme-map: map-merge($theme-map,
($key: $value,
)) !global;
}
@content;
$theme-map: null !global;
}
}
}
@function themed($key) {
@return map-get($theme-map, $key);
}
a {
text-decoration: none;
}
然后分别在 navBar leftBar rightBar
三个组件的 scss
文件中使用函数即可
以 navBar
为例,凡是需要更改颜色的地方就使用函数传入对应参数名称
@import "../../style.scss";
.nav-bar {
@include themify($themes) {
//..........
border-bottom: 1px solid themed("border");
background-color: themed("bg");
color: themed('textColor');
.left {
//......
.search {
//..............
border: 1px solid themed("border");
input {
color: themed('textColor');
}
}
}
}
}
脚本部分
但现在仅仅只是样式设置好了,脚本还没动呢
首先我们会想到,与改变模式有关的值其实就是,navBar
的一个 icon
,那么我们应该在 navBar
组件中使用 state
吗,显然不是,因为这个 state
还会影响 leftBar rightBar
以及主页的 outlet
,其实就是会影响整个主页。
那我们应该把在 App.jsx
中使用 state
吗,如果在 App.jsx
中使用 state
,而 navBar
需要有改变 state
的权力,那就会使用 props
,那如果还有其他子组件也需要呢,这会比较麻烦
所以使用 context
,由 context
保管 state
,然后 context
包裹 App.jsx
这样 App.jsx
和它的子组件就可以通过使用 useContext
来获取 state
在 src
下创建 context/darkModeContet.js
暴露出 darkMode
和可以改变它的函数
-
脚本
import { useState, createContext, useEffect } from 'react'; export const DarkModeContext = createContext(); export function DarkModeContextProvider({ children }) { const [darkMode, setDarkMod] = useState(JSON.parse(localStorage.getItem('darkMode')) || false); const toggle = () => { setDarkMod(!darkMode); } useEffect(() => { localStorage.setItem('darkMode', darkMode) }, [darkMode]); return ( <DarkModeContext.Provider value={{ darkMode, toggle }} > {children} </ DarkModeContext.Provider> ) }
// index.js // .............................. root.render( <React.StrictMode> <DarkModeContextProvider> <App /> </DarkModeContextProvider> </React.StrictMode> );
// App.jsx // ..................... function App() { const currentUser = true; **const { darkMode } = useContext(DarkModeContext);** const Layout = () => { return ( **<div className={`theme-${darkMode ? 'dark' : 'light'}`}>** <NavBar /> <div style={{ display: 'flex' }}> <LeftBar /> <div style={{ flex: 6 }}> <Outlet /> </div> <RightBar /> </div> </div> ) } //................................ }
// navBar.jsx export default function NavBar() { **const {darkMode,toggle} = useContext(DarkModeContext);** return ( <div className='nav-bar'> <div className="left"> <Link to='/'> <span>lamasocial</span> </Link> <i className='iconfont home-icon'></i> **{darkMode ? <i className="iconfont moon" onClick={toggle}></i> : <i className="iconfont moon" onClick={toggle}></i>}** <i className="iconfont function"></i> <div className="search"> <i className="iconfont search-icon"></i> <input type="text" placeholder='search....' /> </div> </div> <div className="right"> <i className="iconfont profile"></i> <i className="iconfont email"></i> <i className="iconfont remind"></i> <div className="user"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-g7oj73.jpg"/> <span>John Doe</span> </div> </div> </div> ) }
初步实现检查是否登录
首先是像创建 darkModeContext
一样创建 authContext
user 的数据暂时固定
import { useState, createContext, useEffect } from 'react';
export const AuthContext = createContext();
export function AuthContextProvider({ children }) {
const [currentUser, setCurrentUser] = useState(JSON.parse(localStorage.getItem('user')) || null);
const login = () => {
// TO DO
setCurrentUser({
id: 1,
name: 'John Doe',
profilePic: 'https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-g7oj73.jpg'
})
}
useEffect(() => {
localStorage.setItem('user', JSON.stringify(currentUser))
}, [currentUser]);
return (
<AuthContext.Provider value={{ currentUser, login }} >
{children}
</ AuthContext.Provider>
)
}
更改 login.jsx
让其的 login 按钮触发 AuthContext
的 login
方法
再修改凡是与 currentUser
有关的组件,这些组件使用 currentUer
的数据
stories
-
HTML结构
import './stories.scss'; import {useContext} from 'react'; import { AuthContext } from '../../context/authContext'; export default function Stories() { const {currentUser} = useContext(AuthContext); const stories = [ { id:1, name: 'klns', img: 'https://i.328888.xyz/2023/03/22/YvOPv.jpeg' }, { id:2, name: 'llxs', img: 'https://i.328888.xyz/2023/03/22/YvHEU.jpeg' }, { id:3, name: 'xlys', img: 'https://i.328888.xyz/2023/03/22/Yv22p.jpeg' }, { id:4, name: 'ylrs', img: 'https://i.328888.xyz/2023/03/22/YvlqL.jpeg' } ] return ( <div className="stories"> <div className="story"> <img src={currentUser.profilePic} alt="" /> <span>{currentUser.name}</span> <button>+</button> </div> {stories.map(story => ( <div className="story"> <img src={story.img} alt="" /> <span>{story.name}</span> </div> ))} </div> ) }
-
样式
.stories { display: flex; height: 250px; margin-bottom: 30px; gap: 10px; .story { flex: 1; border-radius: 10px; overflow: hidden; position: relative; img { width: 100%; height: 100%; object-fit: cover; } span { position: absolute; bottom: 10px; left: 10px; color: white; font-weight: 500; } button { display: flex; justify-content: center; align-items: center; width: 30px; height: 30px; position: absolute; bottom: 40px; left: 10px; color: #fff; background-color: #5271ff; border: none; border-radius: 50%; cursor: pointer; font-size: 30px; } } }
posts
组件结构
posts 是一个父组件,子组件为 post
post 组件中还有子组件 comments,并且评论的打开关闭应该交给 post 的一个 state
代码内容
暂时用假数据
-
posts 的 jsx
import './posts.scss' import Post from '../post/post'; const posts = [ { id: 1, name: 'klns', useId: 1, profilePic: 'https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-3zdpkd.jpg', desc: 'Lorem ipsum dolor sit amet consectetur adipisicing elit', img: 'https://i.328888.xyz/2023/03/22/Y9GUy.jpeg' }, { id: 2, name: 'jane crimon', useId: 2, profilePic: 'https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-x8v5mv.jpg', desc: 'Tenetur iste voluptates dolorem rem commodi voluptate pariatur,voluptatum', img: 'https://i.328888.xyz/2023/03/22/Y96K3.jpeg' } ] export default function Posts() { return ( <div className="posts"> {posts.map(post => ( <Post post={post} key={post.id}/> ))} </div> ) }
-
posts 的样式
.posts { display: flex; flex-direction: column; gap: 50px; }
每个 post 组件大致分为四个部分,用户信息,内容,一些操作,以及评论组件
每个 post 组件都需要相关的数据,而这些数据暂时由 posts 组件传入它的假数据
-
post 的 jsx
import {Link} from 'react-router-dom'; import {useState} from 'react'; import Comments from '../comments/comments' import './post.scss' export default function Post({post}) { const [openComment, setOpenComment] = useState(false); // temporary let liked = true; return ( <div className="post"> <div className="container"> <div className="user"> <div className="user-info"> <img src={post.profilePic}/> <div className="details"> <Link to={`/profile/${post.useId}`}> <span className='name'>{post.name}</span> </Link> <span className='date'>1 min ago</span> </div> </div> <i className='iconfont more'></i> </div> <div className="content"> <p>{post.desc}</p> <img src={post.img} alt="" /> </div> <div className="info"> <div className="item"> {liked ? <i className='iconfont like' style={{color:'red'}}></i> : <i className='iconfont like'></i>} 12 Likes </div> <div className="item" onClick={() => {setOpenComment(!openComment)}}> <i className="iconfont comments"></i> 12 Comments </div> <div className="item"> <i className="iconfont share"></i> Share </div> </div> {openComment && <Comments/>} </div> </div> ) }
-
post 的样式
@import '../../style.scss'; @font-face { font-family: 'iconfont'; /* Project id 3591478 */ src: url('//at.alicdn.com/t/c/font_3591478_895afjog48r.woff2?t=1679493172073') format('woff2'), url('//at.alicdn.com/t/c/font_3591478_895afjog48r.woff?t=1679493172073') format('woff'), url('//at.alicdn.com/t/c/font_3591478_895afjog48r.ttf?t=1679493172073') format('truetype'); } .iconfont { font-family: "iconfont" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -webkit-text-stroke-width: 0.2px; -moz-osx-font-smoothing: grayscale; } .post { @include themify($themes) { -webkit-box-shadow: -14px 8px 55px -27px rgba(0, 0, 0, 0.37); -moz-box-shadow: -14px 8px 55px -27px rgba(0, 0, 0, 0.37); box-shadow: -14px 8px 55px -27px rgba(0, 0, 0, 0.37); border-radius: 20px; background-color: themed("bg"); color: themed("textColor"); a { color: inherit; } .container { padding: 20px; .user { display: flex; align-items: center; justify-content: space-between; .user-info { display: flex; gap: 20px; img { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; } .details { display: flex; flex-direction: column; .name { font-weight: 500; } .date { font-size: 12px; } } } .more { width: 20px; height: 20px; font-size: 20px; } } .content { margin: 20px 0; img { width: 100%; max-height: 500px; object-fit: cover; margin-top: 20px; } } .info { display: flex; align-items: center; gap: 20px; .item { display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: 14px; } } } } }
评论组件需要获得当前用户的信息,所以需要使用 useContext
拿到 AuthContext
的内容
-
comments 的 jsx
import './comments.scss'; import { useContext } from 'react'; import { AuthContext } from '../../context/authContext'; const comments = [ { id: 1, desc: `Beautiful project.. thanks once more Lama, you're an amazing teacher. So about the backend , using Sql, I got to try "Prisma", and it's a very powerful ORM to work with when working with SQL, and it does work with MongoDb as well. It would be nice to see it used on this project for the backend. Thanks once more'`, name: 'Jane Doe', userId: 3, profilePic: 'https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-dplz6l.jpg' }, { id:2, desc: `Amazing project as always! You're the best teacher on the YouTube for JavaScript! Appreciated your great efforts`, name: 'Jane Doe', userId: 4, profilePic: 'https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-y8zek7.png' } ] export default function Comments() { const {currentUser} = useContext(AuthContext); return ( <div className="comments"> <div className="write"> <img src={currentUser.profilePic} alt="" /> <input type="text" placeholder='write a comment' /> <button>Send</button> </div> {comments.map(comment => ( <div className="comment"> <img src={comment.profilePic} alt="" /> <div className="comment-info"> <span>{comment.name}</span> <p>{comment.desc}</p> </div> <span className='date'>1 hour ago</span> </div> ))} </div> ) }
-
comments 的样式
@import '../../style.scss'; .comments { @include themify($themes) { img { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; } .write { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin: 20px 0px; input { flex: 5; padding: 10px; border: 1px solid themed('border'); background-color: transparent; color: themed('textColor'); } button { border: none; background-color: #5271ff; color: #fff; padding: 10px; cursor: pointer; border-radius: 3px; } } .comment { display: flex; margin: 30px 0; justify-content: space-between; gap: 20px; .comment-info { display: flex; flex: 6; flex-direction: column; align-items: flex-start; gap: 3px; span { font-weight: 500; } p { color: themed('textColorSoft'); } } .date { flex: 1; display: flex; align-self: center; color: gray; font-size: 12px; } } } }
用户页面
用户页面主要分为两个部分,用户信息与 posts 组件
用户信息又分为两个部分,images
与 user-info
-
jsx
import Posts from '../../components/posts/posts'; import './profile.scss' import plaxo from '../../assets/imgs/plaxo.png'; import linkedin from '../../assets/imgs/linkedin.png'; import facebook from '../../assets/imgs/facebook.png'; import instagram from '../../assets/imgs/instagram.png'; import twiter from '../../assets/imgs/twitter.png'; export default function Profile() { return ( <div className='profile'> <div className="images"> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/wallhaven-x8v7vo.jpg" className='cover'/> <img src="https://cdn.jsdelivr.net/gh/Dklns/ImgHosting/Blog-PIC/005WWep7gy1fx7zt06lm8j31hc0u0npd.jpg" className='profilePic'/> </div> <div className="profile-container"> <div className="profile-user-info"> <div className="left"> <a href="http://facebook.com"> <img src={instagram} alt="" /> </a> <a href="http://facebook.com"> <img src={twiter} alt="" /> </a> <a href="http://facebook.com"> <img src={facebook} alt="" /> </a> <a href="http://facebook.com"> <img src={plaxo} alt="" /> </a> <a href="http://facebook.com"> <img src={linkedin} alt="" /> </a> </div> <div className="center"> <span>Jane Doe</span> <div className="info"> <div className="item"> <i className="iconfont"></i> <span>USA</span> </div> <div className="item"> <i className="iconfont"></i> <span>lama.dev</span> </div> </div> <button>Follow</button> </div> <div className="right"> <i className="iconfont"></i> <i className="iconfont"></i> </div> </div> <Posts/> </div> </div> ) }
-
样式
@import '../../style.scss'; @font-face { font-family: 'iconfont'; /* Project id 3591478 */ src: url('//at.alicdn.com/t/c/font_3591478_qgl1ezx8v2r.woff2?t=1679556339249') format('woff2'), url('//at.alicdn.com/t/c/font_3591478_qgl1ezx8v2r.woff?t=1679556339249') format('woff'), url('//at.alicdn.com/t/c/font_3591478_qgl1ezx8v2r.ttf?t=1679556339249') format('truetype'); } .iconfont { font-family: "iconfont" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -webkit-text-stroke-width: 0.2px; -moz-osx-font-smoothing: grayscale; } .profile { @include themify($themes) { background-color: themed('bgSoft'); .images { width: 100%; height: 300px; position: relative; .cover { width: 100%; height: 100%; object-fit: cover; } .profilePic { width: 200px; height: 200px; border-radius: 50%; object-fit: cover; position: absolute; left: 0; right: 0; margin: auto; top: 200px; } } .profile-container { padding: 20px 70px; @include mobile { padding: 10px; } @include tablet { padding: 20px; } .profile-user-info { height: 180px; -webkit-box-shadow: -14px 8px 55px -27px rgba(0, 0, 0, 0.37); -moz-box-shadow: -14px 8px 55px -27px rgba(0, 0, 0, 0.37); box-shadow: -14px 8px 55px -27px rgba(0, 0, 0, 0.37); border-radius: 20px; background-color: themed('bg'); color: themed('textColor'); padding: 50px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; @include mobile { flex-direction: column; height: 30vh; padding: 20px; margin-top: 100px; } .left { flex: 1; display: flex; gap: 10px; @include tablet { flex-wrap: wrap; } } .center { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 10px; span { font-size: 30px; font-weight: 500; } .info { width: 100%; display: flex; align-items: center; justify-content: space-around; .item { display: flex; justify-content: center; gap: 5px; color: themed('textColorSoft'); span { font-size: 12px; } } } button { border: none; background-color: #5271ff; color: #fff; padding: 10px 20px; border-radius: 5px; cursor: pointer; } } .right { flex: 1; display: flex; align-items: center; justify-content: flex-end; gap: 10px; } } } } }
响应式
页面面向设备种类假设只有三种移动手机,平板,电脑
电脑的话就是已有的样式即可,另外两种则在 style.scss
文件中写入 @media
@mixin mobile {
@media(max-width: 480px) {
@content;
}
}
@mixin tablet {
@media(max-width: 960px) {
@content;
}
}
然后到需要响应式的样式文件中添加响应式样式
// navBar.scss
.search {
input {
**@include mobile {
display: none;
}
@include tablet {
width: 200px;
}**
}
}
.right {
@include mobile {
display: none;
}
.user {
@include tablet {
display: none;
}
}
}
}
还需要修改的组件有,profile 的用户信息,leftBar rightBar
转载自:https://juejin.cn/post/7213733567620087845