├── .gitattributes ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public └── vite.svg ├── src ├── @types │ └── styled.d.ts ├── App.tsx ├── assets │ ├── avatar.svg │ ├── logo.svg │ ├── react.svg │ └── tech-bg.svg ├── components │ └── Header │ │ ├── index.tsx │ │ └── styles.ts ├── layouts │ └── DefaultLayout │ │ ├── index.tsx │ │ └── styles.ts ├── lib │ ├── Router.tsx │ └── axios.ts ├── main.tsx ├── pages │ ├── Home │ │ ├── PersonInfo │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── PostCard │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── index.tsx │ │ └── styles.ts │ └── PostDetail │ │ ├── index.tsx │ │ └── styles.ts ├── styles │ ├── global.ts │ └── themes │ │ └── default.ts ├── utils │ ├── formatDate.ts │ └── formatText.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Github Blog 🚀 2 | 3 | That is the challenge for Ignite Course from Rocketseat 4 | 5 | We built a Simple Github Blog, to train our fundamentals about React and to test how to use Markdown on WebApp 6 | 7 | It is totally responsive, so try to access in your phone too 8 | 9 | ### 👉 Link to Access: https://github-blog-hazel.vercel.app/ 10 | 11 | ## ▶ How to start project 12 | 13 | First you need to install and the dependencies and start the project 14 | ```shell 15 | npm run install 16 | npm run start 17 | ``` 18 | 19 | ## ⚙ Config Section 20 | 21 | ### 🛠 Tools: 22 | - React - TypeScript 23 | - Styled Components 24 | - Font Awesome 25 | - React Markdown 26 | - Axios 27 | - Date FNS 28 | - Github API 29 | 30 | 31 | 32 | ### ✔ You can: 33 | - See all Post from My Repo 34 | - See the details from The Post that you clicked 35 | - See the body from a Markdown Type 36 | 37 | 38 | ## 📸 Screenshot Section 39 | ### 💻 Desktop Mode 40 | 41 | ## Initial Page 42 | ![image](https://user-images.githubusercontent.com/62482908/184357473-09b6f213-4683-436a-b3d9-cdd0efec80ef.png) 43 | 44 | ## Initial Page - List Section 45 | ![image](https://user-images.githubusercontent.com/62482908/184357498-8f064422-61c0-4b21-8dd6-43160e73be41.png) 46 | 47 | ## Filter by Text 48 | ![image](https://user-images.githubusercontent.com/62482908/184357554-1dabd879-4175-40a5-84b0-c6f6b538c45e.png) 49 | 50 | ## Detail Page 51 | ![image](https://user-images.githubusercontent.com/62482908/184357628-77184f10-d3b4-446f-9c2f-3e0f4e79d1a1.png) 52 | 53 | 54 | ### 💻 Mobile Mode 55 | 56 | ## Initial Page 57 | ![image](https://user-images.githubusercontent.com/62482908/184357657-5082afb1-2917-4ccd-9fc7-e9b6b3680e10.png) 58 | 59 | ## initial Page - List Section 60 | ![image](https://user-images.githubusercontent.com/62482908/184357690-da5f9db1-b32c-4454-95a2-66002cccc445.png) 61 | 62 | ## Detail Page 63 | ![image](https://user-images.githubusercontent.com/62482908/184357716-b3b4dcaf-64f8-4b2f-a469-24b9c0f04a86.png) 64 | 65 | 66 | 67 | 👉 Visit my linkedin: https://www.linkedin.com/in/pedrovdf/ 68 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Github Blog 10 | 14 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-blog", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.27.2", 13 | "date-fns": "^2.29.1", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-markdown": "^8.0.3", 17 | "react-router-dom": "^6.3.0", 18 | "react-syntax-highlighter": "^15.5.0", 19 | "remark-gfm": "^3.0.1", 20 | "styled-components": "^5.3.5" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^18.0.17", 24 | "@types/react-dom": "^18.0.6", 25 | "@types/react-syntax-highlighter": "^15.5.4", 26 | "@types/styled-components": "^5.1.26", 27 | "@vitejs/plugin-react": "^2.0.1", 28 | "typescript": "^4.6.4", 29 | "vite": "^3.0.6" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/@types/styled.d.ts: -------------------------------------------------------------------------------- 1 | import 'styled-components' 2 | import { defaultTheme } from '../styles/themes/default' 3 | 4 | type ThemeType = typeof defaultTheme 5 | 6 | declare module 'styled-components' { 7 | export interface DefaultTheme extends ThemeType { } 8 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from "styled-components"; 2 | import { GlobalStyle } from "./styles/global"; 3 | import { defaultTheme } from "./styles/themes/default"; 4 | import { BrowserRouter } from "react-router-dom"; 5 | import { Router } from "./lib/Router"; 6 | 7 | function App() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /src/assets/avatar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/tech-bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HeaderContainer } from "./styles"; 3 | 4 | import techBg from "../../assets/tech-bg.svg"; 5 | 6 | export function Header() { 7 | return ( 8 | 9 | GITHUB BLOG LOGO 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Header/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const HeaderContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | max-width: 100vw; 9 | img { 10 | width: 100% 11 | 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /src/layouts/DefaultLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router-dom"; 2 | import { Header } from "../../components/Header"; 3 | import { LayoutContainer } from "./styles"; 4 | 5 | export function DefaultLayout() { 6 | return ( 7 | 8 |
9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/layouts/DefaultLayout/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const LayoutContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | `; 7 | -------------------------------------------------------------------------------- /src/lib/Router.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Routes } from "react-router-dom"; 3 | import { DefaultLayout } from "../layouts/DefaultLayout"; 4 | import { Home } from "../pages/Home"; 5 | import { PostDetail } from "../pages/PostDetail"; 6 | 7 | export function Router() { 8 | return ( 9 | 10 | }> 11 | }> 12 | }> 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/axios.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const api = axios.create({ 4 | baseURL: 'https://api.github.com/' 5 | }) -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | 5 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /src/pages/Home/PersonInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { PersonInfoContainer } from "./styles"; 3 | 4 | import avatar from "../../../assets/avatar.svg"; 5 | import { api } from "../../../lib/axios"; 6 | 7 | interface IUserInfo { 8 | name: string; 9 | followers: number; 10 | githubUsername: string; 11 | company: string; 12 | url: string; 13 | imgUrl: string; 14 | description: string; 15 | } 16 | 17 | export function PersonInfo() { 18 | const [userInfo, setUserInfo] = useState(); 19 | 20 | async function fetchUsers() { 21 | const response = await api.get("users/devpedrodiass"); 22 | const { name, followers, login, company, html_url, avatar_url, bio } = 23 | response.data; 24 | const newUserObj = { 25 | name, 26 | followers, 27 | githubUsername: login, 28 | company, 29 | url: html_url, 30 | imgUrl: avatar_url, 31 | description: bio, 32 | }; 33 | setUserInfo(newUserObj); 34 | } 35 | 36 | useEffect(() => { 37 | fetchUsers(); 38 | }, []); 39 | 40 | return ( 41 | 42 | Person Photo 43 |
44 |
45 |

{userInfo?.name}

46 | 47 | GITHUB 48 | 49 |
50 |
51 |

{userInfo?.description}

52 |
53 |
54 | 55 | 56 | {userInfo?.githubUsername} 57 | 58 | 59 | 60 | {userInfo?.company} 61 | 62 | 63 | 64 | {userInfo?.followers} Followers 65 | 66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/pages/Home/PersonInfo/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const PersonInfoContainer = styled.div` 4 | max-width: 864px; 5 | width: 100%; 6 | height: 212px; 7 | display: flex; 8 | background: ${props => props.theme['base-profile']}; 9 | box-shadow: 0px 2px 28px rgba(0, 0, 0, 0.2); 10 | border-radius: 10px; 11 | padding: 2rem; 12 | gap: 2rem; 13 | img { 14 | border-radius: 10px; 15 | } 16 | div { 17 | width: 100%; 18 | height: 100%; 19 | display: flex; 20 | flex-direction: column; 21 | gap: 0.5rem; 22 | header { 23 | display: flex; 24 | justify-content: space-between; 25 | align-items: center; 26 | h1 { 27 | font-weight: 700; 28 | font-size: 1.5rem; 29 | line-height: 130%; 30 | } 31 | a { 32 | font-weight: 700; 33 | font-size: 0.75rem; 34 | line-height: 160%; 35 | text-transform: uppercase; 36 | text-decoration: none; 37 | display: flex; 38 | gap: 0.5rem; 39 | align-items: center; 40 | color: ${props => props.theme.blue}; 41 | transition: border 0.2s; 42 | border-bottom: 2px solid transparent; 43 | &:hover { 44 | border-bottom: 2px solid ${props => props.theme.blue}; 45 | } 46 | } 47 | } 48 | main { 49 | p { 50 | margin-top: 0.5rem; 51 | word-wrap: break-word; 52 | } 53 | } 54 | footer { 55 | display: flex; 56 | height: 100%; 57 | align-items: flex-end; 58 | gap: 1.5rem; 59 | span { 60 | display: flex; 61 | align-items: center; 62 | gap: 0.5rem; 63 | color: ${props => props.theme['base-subtitle']}; 64 | i { 65 | color: ${props => props.theme['base-label']}; 66 | } 67 | } 68 | 69 | } 70 | 71 | } 72 | @media (max-width:680px) { 73 | display: flex; 74 | flex-direction: column; 75 | height: auto; 76 | align-items: center; 77 | justify-content: center; 78 | } 79 | @media (max-width: 450px) { 80 | div { 81 | header { 82 | flex-direction: column; 83 | gap: 0.8rem; 84 | } 85 | main { 86 | p { 87 | text-align: center; 88 | } 89 | } 90 | footer { 91 | display: flex; 92 | flex-direction: column; 93 | align-items: center ; 94 | } 95 | } 96 | } 97 | `; 98 | -------------------------------------------------------------------------------- /src/pages/Home/PostCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { formatDistanceToNow } from "date-fns"; 2 | import { enUS } from "date-fns/locale"; 3 | import React from "react"; 4 | import { IPost } from ".."; 5 | import { formatText } from "../../../utils/formatText"; 6 | import { PostCardContainer } from "./styles"; 7 | 8 | interface IPostCard { 9 | post: IPost; 10 | } 11 | 12 | export function PostCard({ post }: IPostCard) { 13 | const { created_at, body, title, number } = post; 14 | const formattedDate = formatDistanceToNow(new Date(created_at), { 15 | locale: enUS, 16 | addSuffix: true, 17 | }); 18 | return ( 19 | 20 |
21 |

{title}

22 | {formattedDate} 23 |
24 |
25 |

{formatText(body, 80)}

26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/Home/PostCard/styles.ts: -------------------------------------------------------------------------------- 1 | import { NavLink } from 'react-router-dom'; 2 | import styled from 'styled-components'; 3 | 4 | export const PostCardContainer = styled(NavLink)` 5 | width: 100%; 6 | text-decoration: none; 7 | display: flex; 8 | flex-direction: column; 9 | gap: 1.25rem; 10 | padding: 2rem; 11 | 12 | background: ${props => props.theme['base-post']}; 13 | border-radius: 10px; 14 | border: 2px solid transparent; 15 | 16 | height: 260px; 17 | overflow: hidden; 18 | 19 | transition: border 0.2s; 20 | 21 | cursor: pointer; 22 | header { 23 | display: flex; 24 | justify-content: space-between; 25 | gap: 1rem; 26 | h1 { 27 | font-weight: 700; 28 | font-size: 1.125rem; 29 | line-height: 160%; 30 | color: ${props => props.theme['base-title']}; 31 | text-align: justify; 32 | } 33 | 34 | span { 35 | font-size: 0.875rem; 36 | line-height: 160%; 37 | color: ${props => props.theme['base-span']}; 38 | 39 | } 40 | } 41 | 42 | main { 43 | height: 112px; 44 | overflow: hidden; 45 | p { 46 | height: 100%; 47 | text-align: justify; 48 | color: ${props => props.theme['base-text']}; 49 | } 50 | } 51 | 52 | &:hover { 53 | border: 2px solid ${props => props.theme['base-label']}; 54 | } 55 | `; 56 | -------------------------------------------------------------------------------- /src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { api } from "../../lib/axios"; 3 | import { PersonInfo } from "./PersonInfo"; 4 | import { PostCard } from "./PostCard"; 5 | import { 6 | HomeContainer, 7 | HomeContent, 8 | ListSection, 9 | SearchSection, 10 | } from "./styles"; 11 | 12 | export interface IPost { 13 | title: string; 14 | body: string; 15 | created_at: string; 16 | number: string; 17 | } 18 | 19 | export function Home() { 20 | const [posts, setPosts] = useState([] as IPost[]); 21 | const [postsCounter, setPostsCounter] = useState(0); 22 | 23 | async function fetchPosts(query = "") { 24 | const response = await api.get( 25 | `search/issues?q=${ 26 | query ? query : "" 27 | }%20repo:${"devpedrodiass"}/Github-blog-issues` 28 | ); 29 | setPosts(response.data.items); 30 | setPostsCounter(response.data.total_count); 31 | } 32 | 33 | useEffect(() => { 34 | fetchPosts(); 35 | }, []); 36 | 37 | return ( 38 | 39 | 40 | 41 | 42 |
43 | Posts 44 | {postsCounter} posts 45 |
46 | fetchPosts(e.target.value)} 49 | placeholder="Search a Post" 50 | /> 51 |
52 | 53 | {posts && 54 | posts.map((post) => ( 55 | 59 | ))} 60 | 61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/pages/Home/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const HomeContainer = styled.div` 4 | width: 100%; 5 | margin-top: -5.5rem; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | justify-content: center; 11 | gap: 4.5rem; 12 | padding: 1rem 2rem; 13 | `; 14 | 15 | export const HomeContent = styled.div` 16 | max-width: 864px; 17 | width: 100%; 18 | display: flex; 19 | flex-direction: column; 20 | gap: 3rem; 21 | `; 22 | 23 | export const SearchSection = styled.section` 24 | width: 100%; 25 | div { 26 | display: flex ; 27 | justify-content: space-between; 28 | span { 29 | font-weight: 700; 30 | font-size: 1.125rem; 31 | line-height: 160%; 32 | color: ${props => props.theme['base-subtitle']}; 33 | } 34 | small { 35 | font-style: normal; 36 | font-weight: 400; 37 | font-size: 14px; 38 | line-height: 160%; 39 | color: ${props => props.theme['base-span']}; 40 | } 41 | } 42 | input { 43 | margin-top: 0.75rem; 44 | background: ${props => props.theme['base-input']}; 45 | border: 1px solid ${props => props.theme['base-border']}; 46 | border-radius: 6px; 47 | padding: 0.75rem 1rem; 48 | width: 100%; 49 | color: ${props => props.theme['base-text']}; 50 | &::placeholder { 51 | color: ${props => props.theme['base-label']}; 52 | } 53 | } 54 | ` 55 | 56 | export const ListSection = styled.div` 57 | display: grid; 58 | grid-template-columns: repeat(2,1fr); 59 | gap: 2rem; 60 | 61 | @media (max-width: 950px) { 62 | grid-template-columns: 1fr; 63 | } 64 | ` -------------------------------------------------------------------------------- /src/pages/PostDetail/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 3 | 4 | import ReactMarkdown from "react-markdown"; 5 | import { useParams } from "react-router-dom"; 6 | import remarkGfm from "remark-gfm"; 7 | import { api } from "../../lib/axios"; 8 | import { dark } from "react-syntax-highlighter/dist/esm/styles/prism"; 9 | import { 10 | NavButton, 11 | PostDetailCard, 12 | PostDetailContainer, 13 | PostDetailContent, 14 | } from "./styles"; 15 | import { formatDistanceToNow } from "date-fns"; 16 | import { enUS } from "date-fns/locale"; 17 | 18 | interface IPostDetail { 19 | title: string; 20 | comments: number; 21 | createdAt: string; 22 | githubUsername: string; 23 | url: string; 24 | body: string; 25 | } 26 | 27 | export function PostDetail() { 28 | const [post, setPost] = useState({} as IPostDetail); 29 | const { id } = useParams(); 30 | 31 | async function fetchPost() { 32 | const response = await api.get( 33 | `/repos/devpedrodiass/Github-blog-issues/issues/${id}` 34 | ); 35 | const { title, comments, created_at, user, html_url, body } = response.data; 36 | const newPostObj = { 37 | title, 38 | githubUsername: user.login, 39 | comments, 40 | createdAt: formatDistanceToNow(new Date(created_at), { 41 | locale: enUS, 42 | addSuffix: true, 43 | }), 44 | url: html_url, 45 | body, 46 | }; 47 | setPost(newPostObj); 48 | } 49 | 50 | useEffect(() => { 51 | fetchPost(); 52 | }, []); 53 | 54 | return ( 55 | 56 | 57 |
58 | 59 | 60 | Back 61 | 62 | 63 | See on Github 64 | 65 | 66 |
67 |
68 |

{post.title}

69 |
70 |
71 | 72 | 73 | {post.githubUsername} 74 | 75 | 76 | 77 | {post.createdAt} 78 | 79 | 80 | 81 | {post.comments} Comments 82 | 83 |
84 |
85 | 86 |
87 | {post.body} 88 |
89 |
90 |
91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/pages/PostDetail/styles.ts: -------------------------------------------------------------------------------- 1 | import { NavLink } from 'react-router-dom'; 2 | import styled from 'styled-components'; 3 | 4 | export const PostDetailContainer = styled.div` 5 | margin-top: -5.5rem; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: center; 10 | padding: 0 1rem; 11 | `; 12 | 13 | export const PostDetailCard = styled.div` 14 | max-width: 864px; 15 | width: 100%; 16 | height: auto; 17 | background: ${props => props.theme['base-profile']}; 18 | padding: 2rem; 19 | box-shadow: 0px 2px 28px rgba(0, 0, 0, 0.2); 20 | border-radius: 10px; 21 | display: flex; 22 | flex-direction: column; 23 | 24 | header { 25 | display: flex; 26 | align-items: center; 27 | justify-content: space-between; 28 | a { 29 | text-decoration: none; 30 | background: transparent; 31 | color: ${props => props.theme.blue}; 32 | transition: border 0.2s; 33 | border-bottom: 2px solid transparent; 34 | display: flex; 35 | align-items: center; 36 | gap: 0.5rem; 37 | text-transform: uppercase; 38 | 39 | font-weight: 700; 40 | font-size: 0.75rem; 41 | line-height: 160%; 42 | 43 | &:hover { 44 | border-bottom: 2px solid ${props => props.theme.blue}; 45 | } 46 | } 47 | } 48 | div { 49 | margin-top: 1.5rem; 50 | } 51 | footer { 52 | margin-top: 0.5rem; 53 | display: flex; 54 | align-items: center; 55 | gap: 1.5rem; 56 | span { 57 | display: flex; 58 | align-items: center; 59 | gap: 0.5rem; 60 | color: ${props => props.theme['base-subtitle']}; 61 | i { 62 | color: ${props => props.theme['base-label']}; 63 | } 64 | } 65 | } 66 | 67 | @media (max-width:500px) { 68 | div { 69 | h1 { 70 | text-align: center; 71 | } 72 | } 73 | footer { 74 | flex-direction: column; 75 | } 76 | } 77 | ` 78 | 79 | export const NavButton = styled(NavLink)` 80 | text-decoration: none; 81 | background: transparent; 82 | color: ${props => props.theme.blue}; 83 | transition: border 0.2s; 84 | border-bottom: 2px solid transparent; 85 | display: flex; 86 | align-items: center; 87 | gap: 0.5rem; 88 | text-transform: uppercase; 89 | 90 | font-weight: 700; 91 | font-size: 0.75rem; 92 | line-height: 160%; 93 | 94 | &:hover { 95 | border-bottom: 2px solid ${props => props.theme.blue}; 96 | } 97 | ` 98 | export const PostDetailContent = styled.main` 99 | max-width: 864px; 100 | width: 100%; 101 | padding: 2.5rem; 102 | white-space: pre-wrap; 103 | overflow: hidden; 104 | div { 105 | overflow-x: auto; 106 | width: 100%; 107 | height: 100%; 108 | /* width */ 109 | ::-webkit-scrollbar { 110 | width: 10px; 111 | height: 8px; 112 | } 113 | 114 | /* Track */ 115 | ::-webkit-scrollbar-track { 116 | background: ${props => props.theme['base-profile']}; 117 | } 118 | 119 | /* Handle */ 120 | ::-webkit-scrollbar-thumb { 121 | background: ${props => props.theme.blue}; 122 | } 123 | } 124 | 125 | 126 | 127 | ` -------------------------------------------------------------------------------- /src/styles/global.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | 3 | export const GlobalStyle = createGlobalStyle` 4 | * { 5 | margin: 0; 6 | padding: 0; 7 | box-sizing: border-box; 8 | } 9 | 10 | :focus { 11 | outline: 0; 12 | box-shadow: 0 0 0 2px ${props => props.theme['blue']}; 13 | } 14 | 15 | body { 16 | background: ${props => props.theme['base-background']}; 17 | color: ${props => props.theme['base-text']}; 18 | -webkit-font-smoothing: antialiased; 19 | } 20 | 21 | body, input, textarea, button { 22 | font: 400 1rem Nunito, 'sans-serif'; 23 | } 24 | 25 | /* width */ 26 | ::-webkit-scrollbar { 27 | width: 10px; 28 | height: 8px; 29 | } 30 | 31 | /* Track */ 32 | ::-webkit-scrollbar-track { 33 | background: ${props => props.theme['base-profile']}; 34 | } 35 | 36 | /* Handle */ 37 | ::-webkit-scrollbar-thumb { 38 | background: ${props => props.theme.blue}; 39 | } 40 | ` -------------------------------------------------------------------------------- /src/styles/themes/default.ts: -------------------------------------------------------------------------------- 1 | export const defaultTheme = { 2 | white: '#fff', 3 | blue: '#3294F8', 4 | 'base-title': '#E7EDF4', 5 | 'base-subtitle': '#C4D4E3', 6 | 'base-text': '#AFC2D4', 7 | 'base-span': '#7B96B2', 8 | 'base-label': '#3A536B', 9 | 'base-border': '#1C2F41', 10 | 'base-post': '#112131', 11 | 'base-profile': '#0B1B2B', 12 | 'base-background': '#071422', 13 | 'base-input': '#040F1A', 14 | } as const -------------------------------------------------------------------------------- /src/utils/formatDate.ts: -------------------------------------------------------------------------------- 1 | export const formatDate = new Intl.DateTimeFormat('en-US') -------------------------------------------------------------------------------- /src/utils/formatText.ts: -------------------------------------------------------------------------------- 1 | export function formatText(text: string, limitLength = 50) { 2 | const textArr = text.split(" ") 3 | const newText = textArr.map((string, index) => { 4 | if (index < limitLength) return string 5 | }).filter(string => string !== undefined 6 | ) 7 | return `${newText.toString().replaceAll(",", " ")}...` 8 | } -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | --------------------------------------------------------------------------------