├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierrc.json ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── README_ZH.md ├── babel.config.js ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── images.d.ts ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── scripts ├── build.js ├── start.js └── test.js ├── src ├── Routers.tsx ├── apis │ ├── about.service.ts │ ├── article.service.ts │ ├── cv.service.ts │ ├── home.service.ts │ ├── index.service.ts │ ├── layouts.service.ts │ └── music.service.ts ├── assets │ ├── fonts │ │ ├── DankMono-Italic.woff2 │ │ └── Ubuntu-Regular.woff2 │ ├── images │ │ ├── kyotoanimation.png │ │ ├── normal.cur │ │ ├── stripes_grey.png │ │ ├── yancey-official-blog-cat-scroll.png │ │ ├── yancey-official-blog-logo.png │ │ └── yancey-official-blog-svg-icons.svg │ └── styles │ │ ├── _functions.scss │ │ ├── _mixins.scss │ │ ├── _variables.scss │ │ └── global.scss ├── baseUrl.ts ├── components │ ├── CV │ │ ├── Card.module.scss │ │ └── Card.tsx │ ├── Common │ │ ├── AutoBackToTop │ │ │ └── AutoBackToTop.tsx │ │ ├── ErrorMonitor │ │ │ └── ErrorMonitor.tsx │ │ ├── Footer │ │ │ ├── Footer.module.scss │ │ │ └── Footer.tsx │ │ ├── Header │ │ │ ├── Header.module.scss │ │ │ └── Header.tsx │ │ ├── Loading │ │ │ └── Loading.tsx │ │ └── Title │ │ │ └── Title.tsx │ ├── HoC │ │ └── withPersistentData.tsx │ ├── Music │ │ ├── Card.module.scss │ │ └── Card.tsx │ ├── Post │ │ ├── Like │ │ │ ├── Like.scss │ │ │ └── Like.tsx │ │ ├── LinkCard │ │ │ ├── LinkCard.module.scss │ │ │ └── LinkCard.tsx │ │ ├── PostSummary │ │ │ ├── PostSummary.module.scss │ │ │ └── PostSummary.tsx │ │ ├── Search │ │ │ ├── Search.module.scss │ │ │ └── Search.tsx │ │ └── Tag │ │ │ ├── Tag.module.scss │ │ │ └── Tag.tsx │ ├── Skeletons │ │ ├── BlogDetailSkeleton │ │ │ ├── Skeletons.module.scss │ │ │ └── Skeletons.tsx │ │ ├── BlogSummarySkeleton │ │ │ ├── Skeletons.module.scss │ │ │ └── Skeletons.tsx │ │ ├── FeaturedRecordSkeleton │ │ │ └── Skeletons.tsx │ │ ├── LinkCardSkeleton │ │ │ ├── Skeletons.module.scss │ │ │ └── Skeletons.tsx │ │ ├── LiveTourSkeleton │ │ │ └── Skeletons.tsx │ │ └── YanceyMusicSkeleton │ │ │ └── Skeletons.tsx │ └── Widget │ │ ├── Bubble │ │ └── Bubble.jsx │ │ ├── Player │ │ ├── Player.tsx │ │ └── player.scss │ │ └── ScrollToTop │ │ ├── ScrollToTop.module.scss │ │ └── ScrollToTop.tsx ├── constants │ ├── constants.ts │ └── routePath.ts ├── containers │ ├── About │ │ ├── About.scss │ │ └── About.tsx │ ├── Apps │ │ ├── Apps.module.scss │ │ └── Apps.tsx │ ├── Archive │ │ ├── Archive.module.scss │ │ └── Archive.tsx │ ├── Blog │ │ ├── Blog.module.scss │ │ └── Blog.tsx │ ├── BlogDetail │ │ ├── BlogDetail.scss │ │ └── BlogDetail.tsx │ ├── CV │ │ ├── CV.module.scss │ │ └── CV.tsx │ ├── Home │ │ ├── Home.module.scss │ │ └── Home.tsx │ ├── Legal │ │ ├── Legal.module.scss │ │ └── Legal.tsx │ ├── Music │ │ ├── FeaturedRecords.tsx │ │ ├── LiveTours.tsx │ │ ├── Music.module.scss │ │ ├── Music.tsx │ │ ├── MusicNotes.tsx │ │ └── YanceyMusic.tsx │ └── NotFound │ │ ├── NotFound.module.scss │ │ └── NotFound.tsx ├── history.ts ├── index.tsx ├── layouts │ └── Layouts.tsx ├── react-app-env.d.ts ├── registerServiceWorker.ts ├── stores │ ├── aboutStore.ts │ ├── articleStore.ts │ ├── cvStore.ts │ ├── homeStore.ts │ ├── index.ts │ ├── layoutsStore.ts │ └── musicStore.ts ├── test │ └── tools.spec.ts ├── tools │ ├── axios.ts │ └── tools.ts └── types │ ├── about.ts │ ├── apps.ts │ ├── article.ts │ ├── common.ts │ ├── cv.ts │ ├── home.ts │ ├── layout.ts │ ├── music.ts │ ├── presistent.ts │ └── widget.ts ├── tsconfig.json ├── tsconfig.prod.json ├── tsconfig.test.json ├── tslint.json └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Before submitting a pull request,** please make sure the following is done: 2 | 3 | 1. Fork the repository and create your branch from master. 4 | 5 | 2. Run yarn in the repository root. 6 | 7 | 3. If you've fixed a bug or added code that should be tested, add tests! 8 | 9 | 4. Ensure the test suite passes (yarn test). Tip: yarn test --watch TestName is helpful in development. 10 | 11 | 5. Run yarn test-prod to test in the production environment. It supports the same options as yarn test. 12 | 13 | 6. If you need a debugger, run yarn debug-test --watch TestName, open chrome://inspect, and press "Inspect". 14 | 15 | 7. Format your code with prettier (yarn prettier). 16 | 17 | 8. Make sure your code lints (yarn lint). Tip: yarn linc to only check changed files. 18 | 19 | 9. Run the Flow typechecks (yarn flow). 20 | 21 | 10. If you haven't already, complete the CLA. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | build 4 | dist 5 | 6 | # testing 7 | /coverage 8 | 9 | # misc 10 | .DS_Store 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | # IDE 21 | .idea 22 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": true, 5 | "bracketSpacing": true, 6 | "trailingComma": "all", 7 | "proseWrap": "preserve" 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: true 3 | node_js: 4 | - '8' 5 | cache: 6 | directories: 7 | - node_modules 8 | jobs: 9 | include: 10 | - stage: test 11 | if: (branch = master) AND (NOT (type IN (pull_request))) 12 | script: 13 | - yarn install 14 | - yarn test 15 | - yarn build 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.3.1 (2019-05-13) 2 | 3 | Support skeleton for blog summary component and try a new lazyload component. 4 | 5 | ## 2.3.0 (2019-05-12) 6 | 7 | Add Canvas Bubble for Home Page. 8 | 9 | ## 2.2.0 (2019-05-08) 10 | 11 | Use React.lazy() replace react-loadable. 12 | 13 | ## 2.1.0 (2019-04-03) 14 | 15 | Add skeleton for blog detail page. 16 | 17 | ## 2.0.0 (2019-01-14) 18 | 19 | Refactor with TypeScript. 20 | 21 | ## 1.1.0 (2018-12-30) 22 | 23 | Remove dependencies on jQuery and optimize performance. 24 | 25 | ## 1.0.0 (2018-10-14) 26 | 27 | First Blood. 28 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Yancey Inc. has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](https://code.fb.com/codeofconduct/) so that you can understand what actions will and will not be tolerated. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to BLOG_FE 2 | 3 | Want to contribute to BLOG_FE? There are a few things you need to know. 4 | 5 | We wrote a [contribution guide](https://reactjs.org/contributing/how-to-contribute.html) to help you get started. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yancey Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Blog FE for PC](https://www.yanceyleo.com/) 2 | 3 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/5b925ed8c8c64f379dea6f8b685a731b)](https://app.codacy.com/app/YanceyOfficial/BLOG_FE?utm_source=github.com&utm_medium=referral&utm_content=Yancey-Blog/BLOG_FE&utm_campaign=Badge_Grade_Dashboard) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 5 | [![Version](https://img.shields.io/badge/version-2.4.1-blue.svg)](https://github.com/Yancey-Blog/BLOG_FE) 6 | [![Node](https://img.shields.io/badge/node-%3E%3D8.0.0-green.svg)](https://github.com/Yancey-Blog/BLOG_FE) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-green.svg)](https://github.com/Yancey-Blog/BLOG_FE/pulls) 8 | [![Build Status](https://travis-ci.org/Yancey-Blog/BLOG_FE.svg?branch=master)](https://travis-ci.org/Yancey-Blog/BLOG_FE) 9 | 10 | English | [中国語](https://github.com/Yancey-Blog/BLOG_FE/blob/master/README_ZH.md) 11 | 12 | ## Introduction 13 | 14 | This is the second blog website I wrote, The first version was released in March 2018 which wrote by Django and Bootstrap. With the booming of SPA, I decided to write a react version and add some new features. After about two months of design and coding, the second version was released. 15 | 16 | Mainwhile, I also wrote a [CMS](https://github.com/Yancey-Blog/BLOG_CMS/) to manage and operate the data. You can click the link to fork. 17 | 18 | Now, I am writing the [mobile side pages](https://github.com/Yancey-Blog/BLOG_WAP/), coming soon~ 19 | 20 | ## Technology Stack 21 | 22 | - BLOG_FE_FOR_PC: react + react-router-4 + mobx + TypeScript; 23 | - CMS: react + react-router-4 + mobx + Google reCAPTCHA + Ant Design; 24 | - BE Express + Mongo + JWT + Ali OSS + Google reCAPTCHA 25 | 26 | I alse used CSS Module、Webp、SVG Sprite and so on... 27 | 28 | ## Page 29 | 30 | ### Home Page 31 | 32 | ![Cover](https://static.yancey.app/Jietu20190513-120854%402x.jpg) 33 | ![Home](https://static.yancey.app/Jietu20181017-174609@2x.jpg?x-oss-process=image/quality,Q_60) 34 | 35 | The home page contains five parts: 36 | 37 | - Background 38 | - Motto 39 | - Announcement 40 | - The Latest 3 Projects 41 | - The Latest 10 Articles 42 | 43 | #### Background 44 | 45 | The first time you visited my website, you will see the latest background, meanwhile, the id of this background will saved in localStorge. You can switch background by clicking the `left arrow` or `right arrow`. So, when open the website again, you will see the current background usless clear cache or I delete/hide the background in CMS. 46 | 47 | #### Motto 48 | 49 | My motto. 50 | 51 | #### Announcement 52 | 53 | I always publish new information in the component. 54 | 55 | #### The Latest 3 Projects 56 | 57 | Display the latest 3 open source projects of mine, click on any one to jump to the corresponding GitHub page 58 | 59 | #### The Latest 10 Articles 60 | 61 | Display the latest 10 articles summary, which is include release date, title, PV, likes, tag, summary and so on, click one to jump to the article detail page. 62 | 63 | ### Blog Page 64 | 65 | ![Blog](https://static.yancey.app/Jietu20181017-181438@2x.jpg?x-oss-process=image/quality,Q_60) 66 | 67 | The left part is a pageable summary list; The right part includes two parts: `tags list` and `top 7 most viewed` 68 | 69 | In addition, you can see a search button in the rightmost position of `header` component. Yep,a lovely Hatsune Miku will appear. 70 | 71 | ![Blog](https://static.yancey.app/Jietu20181017-181947.jpg?x-oss-process=image/quality,Q_60) 72 | 73 | ### Blog Detail Page 74 | 75 | ![Blog Detail](https://static.yancey.app/Jietu20181017-182519@2x.jpg?x-oss-process=image/quality,Q_20) 76 | 77 | - Collect people views counts. 78 | 79 | - Display the article cover, title, publish date(show the lastest update date when you are moving in the text.) 80 | 81 | - The right part is menu 82 | 83 | - In the maim body 84 | 85 | - Click on the picture to zoom in 86 | - Click the header of code to zoom in 87 | - Like 88 | - Comment 89 | - Previous article and Next article 90 | - Share to Twitter 91 | 92 | ### Archive Page 93 | 94 | ![Blog Detail](https://static.yancey.app/Jietu20181017-183530@2x.jpg?x-oss-process=image/quality,Q_60) 95 | 96 | - Click on the circle to show the current month's articles. 97 | 98 | - Click on the `Fold` to hide all articles. 99 | 100 | - Click on the `Unfold` to show all articles. 101 | 102 | ### Music Page 103 | 104 | ![Music-1](https://static.yancey.app/Jietu20181017-184221%402x.jpg?x-oss-process=image/quality,Q_60) 105 | 106 | ![Music-2](https://static.yancey.app/Jietu20181017-184130@2x.jpg?x-oss-process=image/quality,Q_10) 107 | 108 | - The Lives image 109 | - Music notes 110 | - Featured reecords 111 | - My works 112 | 113 | ### Apps Page 114 | 115 | ![Apps](https://static.yancey.app/Jietu20181017-185001@2x.jpg?x-oss-process=image/quality,Q_60) 116 | 117 | Todos: 118 | 119 | - Blog for Android 120 | - Blog for iOS 121 | - Blog for Mac 122 | 123 | ### CV Page 124 | 125 | - My basic information 126 | - Work experience 127 | - Program experience 128 | 129 | ### About Page 130 | 131 | ![About](https://static.yancey.app/Jietu20181017-185855@2x.jpg?x-oss-process=image/quality,Q_10) 132 | 133 | Display the development history of the blog. 134 | 135 | ## Change Logs 136 | 137 | - 2018-10-14 First blood. 138 | - 2018-12-30 Remove dependencies on jQuery and optimize performance. 139 | - 2019-01-14 Refactor with TypeScript. 140 | - 2019-04-03 Add skeleton for blog detail page. 141 | - 2019-05-08 Use React.lazy() replace react-loadable. 142 | - 2019-05-12 Add Canvas Bubble for Home Page. 143 | - 2019-05-13 Support skeleton for blog summary component and try a new lazyload component. 144 | 145 | ## TODO 146 | 147 | - SSR 148 | - Optimize performance 149 | - Fragment page 150 | - RSS 151 | 152 | ## License 153 | 154 | BLOG FE is [MIT licensed](https://opensource.org/licenses/MIT). 155 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | # [Blog FE for PC](https://wwww.yanceyleo.com/) 2 | 3 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/5b925ed8c8c64f379dea6f8b685a731b)](https://app.codacy.com/app/YanceyOfficial/BLOG_FE?utm_source=github.com&utm_medium=referral&utm_content=Yancey-Blog/BLOG_FE&utm_campaign=Badge_Grade_Dashboard) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 5 | [![Version](https://img.shields.io/badge/version-2.4.1-blue.svg)](https://github.com/Yancey-Blog/BLOG_FE) 6 | [![Node](https://img.shields.io/badge/node-%3E%3D8.0.0-green.svg)](https://github.com/Yancey-Blog/BLOG_FE) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-green.svg)](https://github.com/Yancey-Blog/BLOG_FE/pulls) 8 | 9 | 中文 | [English](https://github.com/Yancey-Blog/BLOG_FE/blob/master/README.md) 10 | 11 | ## Introduction 12 | 13 | 这是我写的第二个博客网站。第一个是在今年(2018 年)3 月份完成的,用的 Bootstrap + Django. 至于为什么写第二版,无非是看到别人的博客太好看了 😂。 14 | 15 | 2.0 的数据还在迁移中...因此下面的图各种 demo1 demo2... 16 | 17 | 2.0 版本是一个前后端分离的项目,这次除了前端和后端,还专门写了一个[后台管理系统](https://github.com/Yancey-Blog/BLOG_CMS/blob/master/README.md)。其中: 18 | 19 | - 前端主要技术栈是 react + react-router-4 + mobx; 20 | - 管理后台用的是 react + react-router-4 + mobx + Google reCAPTCHA + Ant Design; 21 | - 后端则是 Express + Mongo + JWT + Ali OSS + Google reCAPTCHA + request promise. 22 | 23 | 全端用到了 Airbnb 的 eslint,前端还用到了 CSS Module、Webp、SVG Sprite 等等一些好玩的技术,下面具体介绍 24 | 一下整个前端。 25 | 26 | 因为刚毕业不久,工作时间也不多,感觉做的项目还稍显稚嫩,因此决定开源出来接受大佬们的意见。 27 | 28 | ## Detail 29 | 30 | ### Global Component 31 | 32 | 全局无非就是标配的 header、footer、滚动进度指示条、当然还有一个音乐播放器的组件。 33 | 34 | 此外,我还后端写了一个`glonalConfig`的接口,暂时只想到一个功能,就是控制前端的`filter: grayscale(100%);`属性,用在一些哀悼日时,后台会开启这个按钮。 35 | 36 | ### Home Page 37 | 38 | ![Cover](https://static.yancey.app/Jietu20181017-174103%402x.jpg?x-oss-process=image/quality,Q_20) 39 | ![Home](https://static.yancey.app/Jietu20181017-174609@2x.jpg?x-oss-process=image/quality,Q_60) 40 | 41 | 主页分为 5 个部分: 42 | 43 | - Background 44 | - Motto 45 | - Announcement 46 | - The Latest 3 Projects 47 | - The Latest 10 Articles 48 | 49 | #### Background 50 | 51 | 先说背景图,后台存有多张背景图,因此通过左右按钮可以切换背景图。并且当前那张背景图的 id 会存储到 localStorage,因此只要不清掉 localStorage,下次打开还是当前那张背景图。 52 | 53 | 当然如果 localStorage 没有相关 id 或者这张图片被我在后台删除了,将会返回最新发布的图片。 54 | 55 | 其实后台我还设置了图片的显隐按钮,当某张图片的 id 在 localStorage,但被我在后台隐藏了,同样将会返回最新发布的图片。 56 | 57 | #### Motto 58 | 59 | Motto 部分对应上面第一张图这个部分。 60 | 61 | 死は生の対極としてではなく、その一部として存在している 62 | 63 | 同样是请求后端接口,取得最新的那条 Motto 64 | 65 | _ps: 上面那句话来自「ノルウェイの森」(《挪威的森林》)_ 66 | 67 | #### Announcement 68 | 69 | 和 Motto 部分同理,用途是发布一些最新消息。 70 | 71 | #### The Latest 3 Projects 72 | 73 | 这个是用来展示我最新的三个开源项目,url 会连接到相应的 GitHub. 74 | 75 | #### The Latest 10 Articles 76 | 77 | 整个博客的核心部分之一,在首页会显示最新 10 篇文章的摘要模块,上面显示发布时间、title、PV 量、点赞量、Tags、summary、show more,点击图片、标题或者 show more 都可以进入到文章细节页。 78 | 79 | ### Blog Page 80 | 81 | ![Blog](https://static.yancey.app/Jietu20181017-181438@2x.jpg?x-oss-process=image/quality,Q_60) 82 | 83 | 左边是最新的十篇 summary, 而下面是后端分页的分页器;右边上面是文章的标签集合,下面是 7 篇最高 PV 的文章(设计大家都懂,知乎的设计)。 84 | 85 | 此外,其实在 header 的右上角还有一个*搜索*按钮,点进去是这个样子: 86 | 87 | ![Blog](https://static.yancey.app/Jietu20181017-181947.jpg?x-oss-process=image/quality,Q_60) 88 | 89 | 没错,可爱的初音ミク, 通过在搜索框输入,模糊匹配文章名。当然这里没有第一版好,第一版用了 whoosh + jieba 搜索引擎,效果理论上要比这版好一些。 90 | 91 | ### Blog Detail Page 92 | 93 | ![Blog Detail](https://static.yancey.app/Jietu20181017-182519@2x.jpg?x-oss-process=image/quality,Q_20) 94 | 95 | 关于 Blog Detail 页面其实有很多地方,一张图放不下: 96 | 97 | - 从上面来看是 header cover、标题、发布时间(鼠标移入显示最后修改时间)、tags 等; 98 | - 正文部分的图片可以点击放大,形成一个手动轮播图的效果; 99 | - code 部分用了 highlight.js 100 | - 其中点击 code 的头部,也就是仿 Mac 按钮那个部分,代码块也会全屏放大 101 | - 右边是 toc 102 | - 下面还有点赞、Twitter(这个地方恐怕要做 SSR,因为 Twitter Card 必须要拿到实际的 meta 信息,如果是 JS 生成的,比如用了 react-helmet,是不会被识别出来的) 103 | - 然后就是用了 LiveRe 的评论插件。 104 | - 最后还有人见人恨的复制文本附带版权信息: 105 | 106 | 不知道大家看到一个小细节没,打开Chrome开发者工具,选择查看元素,将鼠标移动到html页面,发现浏览器自动给栅格标上了虚线,看下图。 107 | 108 | Article Name: Grid 109 | Article URL: https://blog.yanceyleo.com/p/5bc202a26b48dfee0a0dcedf 110 | License: Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) 111 | 112 | **后期确实要把这个页面做成 SSR,除了 Twitter Card 的问题,因为现在正文用的 dangerouslySetInnerHTML,这一块也没法做懒加载。** 113 | 114 | ### Archive Page 115 | 116 | ![Blog Detail](https://static.yancey.app/Jietu20181017-183530@2x.jpg?x-oss-process=image/quality,Q_60) 117 | 118 | 这个部分显示归档,话说毕竟好久不写后端了,写聚合分组 SQL 那一块的时候确实花了些时间。 119 | 120 | 点击大一点儿的圆圈会显示/隐藏某个月的文章归档信息,默认展示最新一个月的归档信息,右边的按钮控制展示全部和关闭全部,其实原理就是用了 checkbox. 121 | 122 | ### Music Page 123 | 124 | 我的业余爱好是做音乐,因此 Blog 也不会少了 Music 模块。 125 | 126 | ![Music-1](https://static.yancey.app/Jietu20181017-184221%402x.jpg?x-oss-process=image/quality,Q_60) 127 | 128 | ![Music-2](https://static.yancey.app/Jietu20181017-184130@2x.jpg?x-oss-process=image/quality,Q_10) 129 | 130 | 第一张图的左上角是我看过的 Live 的轮播图片,当然图片肯定都是在拍照时间拍的; 131 | 132 | 右边是 Music Notes,实际上就是`articles?tag=Music`,然后取最新的四篇,当然数据还没迁移过来,随便找了篇文章加上了 Music 的 tag; 133 | 134 | 第二张图上面是一些我喜欢的唱片,关于购买地址,没有任何商业用途,一般链接来自日亚抑或 cdjapan; 135 | 136 | 下面则是我的一些小作品了,链接指向 SoundCloud(需 fq) 137 | 138 | ### Apps Page 139 | 140 | ![Apps](https://static.yancey.app/Jietu20181017-185001@2x.jpg?x-oss-process=image/quality,Q_60) 141 | 142 | 现在还没去做,后期会计划写 Wap 版(1.0 是用的响应式,这次想把 Wap 单独抽离出来);用 NR 写 iOS 和 Andriod;用 Electron 写个 Mac 版,毕竟用着 Nav Bar 的 MBP, 还是想在这个地方做点儿好玩的事情出来。 143 | 144 | ### CV Page 145 | 146 | 这里就不放图了,简历分三部分,都是从后端取出来的: 147 | 148 | - 基本信息 149 | - 工作经历 150 | - 项目经历 151 | 152 | ### About 153 | 154 | ![About](https://static.yancey.app/Jietu20181017-185855@2x.jpg?x-oss-process=image/quality,Q_10) 155 | 156 | About 页面也是从后端取出来的,用来记录 Blog 发展的大事记(估计就是 Bug 修改历程 噗 x)。 157 | 158 | ## License 159 | 160 | BLOG FE is [MIT licensed](https://opensource.org/licenses/MIT). 161 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-react'] 3 | }; 4 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebook/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 50 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in Webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example, . 77 | // This should only be used as an escape hatch. Normally you would put 78 | // images into the `src` and `import` them in code to get their paths. 79 | PUBLIC_URL: publicUrl, 80 | } 81 | ); 82 | // Stringify all values so we can feed into Webpack DefinePlugin 83 | const stringified = { 84 | 'process.env': Object.keys(raw).reduce((env, key) => { 85 | env[key] = JSON.stringify(raw[key]); 86 | return env; 87 | }, {}), 88 | }; 89 | 90 | return { raw, stringified }; 91 | } 92 | 93 | module.exports = getClientEnvironment; 94 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | const assetFilename = JSON.stringify(path.basename(filename)); 11 | 12 | if (filename.match(/\.svg$/)) { 13 | return `module.exports = { 14 | __esModule: true, 15 | default: ${assetFilename}, 16 | ReactComponent: (props) => ({ 17 | $$typeof: Symbol.for('react.element'), 18 | type: 'svg', 19 | ref: null, 20 | key: null, 21 | props: Object.assign({}, props, { 22 | children: ${assetFilename} 23 | }) 24 | }), 25 | };`; 26 | } 27 | 28 | return `module.exports = ${assetFilename};`; 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(inputPath, needsSlash) { 15 | const hasSlash = inputPath.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return inputPath.substr(0, inputPath.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${inputPath}/`; 20 | } else { 21 | return inputPath; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 61 | 62 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | 18 | const fs = require('fs'); 19 | const chalk = require('chalk'); 20 | const webpack = require('webpack'); 21 | const WebpackDevServer = require('webpack-dev-server'); 22 | const clearConsole = require('react-dev-utils/clearConsole'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const { 25 | choosePort, 26 | createCompiler, 27 | prepareProxy, 28 | prepareUrls, 29 | } = require('react-dev-utils/WebpackDevServerUtils'); 30 | const openBrowser = require('react-dev-utils/openBrowser'); 31 | const paths = require('../config/paths'); 32 | const config = require('../config/webpack.config.dev'); 33 | const createDevServerConfig = require('../config/webpackDevServer.config'); 34 | 35 | const useYarn = fs.existsSync(paths.yarnLockFile); 36 | const isInteractive = process.stdout.isTTY; 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 40 | process.exit(1); 41 | } 42 | 43 | // Tools like Cloud9 rely on this. 44 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 45 | const HOST = process.env.HOST || '0.0.0.0'; 46 | 47 | if (process.env.HOST) { 48 | console.log( 49 | chalk.cyan( 50 | `Attempting to bind to HOST environment variable: ${chalk.yellow( 51 | chalk.bold(process.env.HOST) 52 | )}` 53 | ) 54 | ); 55 | console.log( 56 | `If this was unintentional, check that you haven't mistakenly set it in your shell.` 57 | ); 58 | console.log( 59 | `Learn more here: ${chalk.yellow('http://bit.ly/CRA-advanced-config')}` 60 | ); 61 | console.log(); 62 | } 63 | 64 | // We require that you explictly set browsers and do not fall back to 65 | // browserslist defaults. 66 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 67 | checkBrowsers(paths.appPath, isInteractive) 68 | .then(() => { 69 | // We attempt to use the default port but if it is busy, we offer the user to 70 | // run on a different port. `choosePort()` Promise resolves to the next free port. 71 | return choosePort(HOST, DEFAULT_PORT); 72 | }) 73 | .then(port => { 74 | if (port == null) { 75 | // We have not found a port. 76 | return; 77 | } 78 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 79 | const appName = require(paths.appPackageJson).name; 80 | const urls = prepareUrls(protocol, HOST, port); 81 | // Create a webpack compiler that is configured with custom messages. 82 | const compiler = createCompiler(webpack, config, appName, urls, useYarn); 83 | // Load proxy config 84 | const proxySetting = require(paths.appPackageJson).proxy; 85 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 86 | // Serve webpack assets generated by the compiler over a web server. 87 | const serverConfig = createDevServerConfig( 88 | proxyConfig, 89 | urls.lanUrlForConfig 90 | ); 91 | const devServer = new WebpackDevServer(compiler, serverConfig); 92 | // Launch WebpackDevServer. 93 | devServer.listen(port, HOST, err => { 94 | if (err) { 95 | return console.log(err); 96 | } 97 | if (isInteractive) { 98 | clearConsole(); 99 | } 100 | console.log(chalk.cyan('Starting the development server...\n')); 101 | openBrowser(urls.localUrlForBrowser); 102 | }); 103 | 104 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 105 | process.on(sig, function() { 106 | devServer.close(); 107 | process.exit(); 108 | }); 109 | }); 110 | }) 111 | .catch(err => { 112 | if (err && err.message) { 113 | console.log(err.message); 114 | } 115 | process.exit(1); 116 | }); 117 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI, in coverage mode, or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--coverage') === -1 && 45 | argv.indexOf('--watchAll') === -1 46 | ) { 47 | // https://github.com/facebook/create-react-app/issues/5210 48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 49 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 50 | } 51 | 52 | 53 | jest.run(argv); 54 | -------------------------------------------------------------------------------- /src/Routers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, lazy, Suspense } from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | import NotFound from './containers/NotFound/NotFound'; 4 | import Loading from '@components/Common/Loading/Loading'; 5 | import routePath from '@constants/routePath'; 6 | 7 | const Home = lazy(() => import('./containers/Home/Home')); 8 | const Blog = lazy(() => import('./containers/Blog/Blog')); 9 | const BlogDetail = lazy(() => import('./containers/BlogDetail/BlogDetail')); 10 | const Archive = lazy(() => import('./containers/Archive/Archive')); 11 | const Legal = lazy(() => import('./containers/Legal/Legal')); 12 | const Apps = lazy(() => import('./containers/Apps/Apps')); 13 | const CV = lazy(() => import('./containers/CV/CV')); 14 | const Music = lazy(() => import('./containers/Music/Music')); 15 | const About = lazy(() => import('./containers/About/About')); 16 | 17 | class Routers extends Component<{}, {}> { 18 | constructor(props: {}) { 19 | super(props); 20 | this.state = {}; 21 | } 22 | 23 | public render() { 24 | return ( 25 | }> 26 | 27 | } /> 28 | } /> 29 | } 32 | /> 33 | } 36 | /> 37 | } /> 38 | } 41 | /> 42 | } /> 43 | } /> 44 | } /> 45 | } /> 46 | } /> 47 | } /> 48 | } /> 49 | 50 | 51 | ); 52 | } 53 | } 54 | 55 | export default Routers; 56 | -------------------------------------------------------------------------------- /src/apis/about.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GET, 3 | } from '../tools/axios'; 4 | 5 | import { 6 | AxiosResponse, 7 | } from 'axios'; 8 | 9 | import { 10 | IAbout, 11 | } from '../types/about'; 12 | 13 | class AboutService { 14 | public getAbouts(): Promise < AxiosResponse < IAbout[] >> { 15 | return GET('/abouts', null, ''); 16 | } 17 | } 18 | 19 | 20 | const aboutService = new AboutService(); 21 | 22 | export default aboutService; -------------------------------------------------------------------------------- /src/apis/article.service.ts: -------------------------------------------------------------------------------- 1 | import { GET, PUT } from '../tools/axios'; 2 | 3 | import { AxiosResponse } from 'axios'; 4 | 5 | import { 6 | IArticleDetail, 7 | IArchive, 8 | IDetail, 9 | ILike, 10 | IIncreasePV, 11 | } from '../types/article'; 12 | 13 | import { ipify } from '../constants/constants'; 14 | 15 | class ArticleService { 16 | public getPostById(id: string): Promise> { 17 | return GET(`/articles/${id}`, null, ''); 18 | } 19 | 20 | public getPostsByPage( 21 | page: number, 22 | ): Promise> { 23 | return GET(`/articleList/page/${page}`, null, ''); 24 | } 25 | 26 | public getPostsByTitle( 27 | title: string, 28 | ): Promise> { 29 | return GET(`/articlesByTitle?q=${title}`, null, ''); 30 | } 31 | 32 | public getAllTags(): Promise> { 33 | return GET('/allTags', null, ''); 34 | } 35 | 36 | public getPostsByTag(tag: string): Promise> { 37 | return GET(`/articlesByTag?tag=${tag}`, null, ''); 38 | } 39 | 40 | public getHots(): Promise> { 41 | return GET('/articlesByPV', null, ''); 42 | } 43 | 44 | public getArchives(): Promise> { 45 | return GET('/archives', null, ''); 46 | } 47 | 48 | public handleLikes(id: string, ip: string): Promise> { 49 | return PUT(`/likes/${id}?ip=${ip}`, null, ''); 50 | } 51 | 52 | public getLikes(id: string, ip: string): Promise> { 53 | return GET(`/likes/${id}?ip=${ip}`, null, ''); 54 | } 55 | 56 | public getIp(): Promise> { 57 | return GET(ipify, null, ''); 58 | } 59 | 60 | public increasePV(id: string): Promise> { 61 | return PUT(`/articlePV/${id}`, null, ''); 62 | } 63 | } 64 | 65 | const articleService = new ArticleService(); 66 | 67 | export default articleService; 68 | -------------------------------------------------------------------------------- /src/apis/cv.service.ts: -------------------------------------------------------------------------------- 1 | import { GET } from '../tools/axios'; 2 | 3 | import { AxiosResponse } from 'axios'; 4 | 5 | import { IUser, IWorkExperience, IProgramExperience } from '../types/cv'; 6 | 7 | class CVService { 8 | public getUser(): Promise> { 9 | return GET('/userInfo', null, ''); 10 | } 11 | 12 | public getWorkExperience(): Promise> { 13 | return GET('/workExperience', null, ''); 14 | } 15 | 16 | public getProgramExperience(): Promise> { 17 | return GET('/programExperience', null, ''); 18 | } 19 | } 20 | 21 | const cvService = new CVService(); 22 | 23 | export default cvService; 24 | -------------------------------------------------------------------------------- /src/apis/home.service.ts: -------------------------------------------------------------------------------- 1 | import { GET } from '../tools/axios'; 2 | 3 | import { AxiosResponse } from 'axios'; 4 | 5 | import { IAnnouncement, IMotto, IProject, ICover } from '../types/home'; 6 | 7 | class HomeService { 8 | public getAnnouncement(): Promise> { 9 | return GET('/latestAnnouncements', null, ''); 10 | } 11 | 12 | public getMotto(): Promise> { 13 | return GET('/latestMotto', null, ''); 14 | } 15 | 16 | public getProject(): Promise> { 17 | return GET('/latestThreeProjects', null, ''); 18 | } 19 | 20 | public getCover( 21 | curId: string, 22 | position: string, 23 | ): Promise> { 24 | return GET(`/covers/${curId}?position=${position}`, null, ''); 25 | } 26 | } 27 | 28 | const homeService = new HomeService(); 29 | 30 | export default homeService; 31 | -------------------------------------------------------------------------------- /src/apis/index.service.ts: -------------------------------------------------------------------------------- 1 | import articleService from './article.service'; 2 | import layoutsService from './layouts.service'; 3 | import musicService from './music.service'; 4 | import homeService from './home.service'; 5 | import cvService from './cv.service'; 6 | import aboutService from './about.service'; 7 | 8 | export { 9 | articleService, 10 | layoutsService, 11 | musicService, 12 | homeService, 13 | cvService, 14 | aboutService, 15 | } -------------------------------------------------------------------------------- /src/apis/layouts.service.ts: -------------------------------------------------------------------------------- 1 | import { GET } from '../tools/axios'; 2 | 3 | import { AxiosResponse } from 'axios'; 4 | 5 | import { IPlayer, IGlobalStatus } from '../types/layout'; 6 | 7 | class LayoutsService { 8 | public getGlobalStatus(): Promise> { 9 | return GET(`/globalStatus`, null, ''); 10 | } 11 | 12 | public getPlayers(): Promise> { 13 | return GET(`/litePlayers`, null, ''); 14 | } 15 | } 16 | 17 | const layoutsService = new LayoutsService(); 18 | 19 | export default layoutsService; 20 | -------------------------------------------------------------------------------- /src/apis/music.service.ts: -------------------------------------------------------------------------------- 1 | import { GET } from '../tools/axios'; 2 | 3 | import { AxiosResponse } from 'axios'; 4 | 5 | import { ILiveTours, IFeaturedRecords, IYanceyMusic } from '../types/music'; 6 | 7 | class MusicService { 8 | public getLiveTours(): Promise> { 9 | return GET(`/liveTours`, null, ''); 10 | } 11 | 12 | public getFeaturedRecords(): Promise> { 13 | return GET(`/latestFourFeaturedRecords`, null, ''); 14 | } 15 | 16 | public getYanceyMusic(): Promise> { 17 | return GET(`/yanceyMusic`, null, ''); 18 | } 19 | } 20 | 21 | const musicService = new MusicService(); 22 | 23 | export default musicService; 24 | -------------------------------------------------------------------------------- /src/assets/fonts/DankMono-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/BLOG_DESKTOP/2375ca6cc4d964b893f37e88293af6ec6fc3fa64/src/assets/fonts/DankMono-Italic.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Ubuntu-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/BLOG_DESKTOP/2375ca6cc4d964b893f37e88293af6ec6fc3fa64/src/assets/fonts/Ubuntu-Regular.woff2 -------------------------------------------------------------------------------- /src/assets/images/kyotoanimation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/BLOG_DESKTOP/2375ca6cc4d964b893f37e88293af6ec6fc3fa64/src/assets/images/kyotoanimation.png -------------------------------------------------------------------------------- /src/assets/images/normal.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/BLOG_DESKTOP/2375ca6cc4d964b893f37e88293af6ec6fc3fa64/src/assets/images/normal.cur -------------------------------------------------------------------------------- /src/assets/images/stripes_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/BLOG_DESKTOP/2375ca6cc4d964b893f37e88293af6ec6fc3fa64/src/assets/images/stripes_grey.png -------------------------------------------------------------------------------- /src/assets/images/yancey-official-blog-cat-scroll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/BLOG_DESKTOP/2375ca6cc4d964b893f37e88293af6ec6fc3fa64/src/assets/images/yancey-official-blog-cat-scroll.png -------------------------------------------------------------------------------- /src/assets/images/yancey-official-blog-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/BLOG_DESKTOP/2375ca6cc4d964b893f37e88293af6ec6fc3fa64/src/assets/images/yancey-official-blog-logo.png -------------------------------------------------------------------------------- /src/assets/styles/_functions.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/BLOG_DESKTOP/2375ca6cc4d964b893f37e88293af6ec6fc3fa64/src/assets/styles/_functions.scss -------------------------------------------------------------------------------- /src/assets/styles/_mixins.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/BLOG_DESKTOP/2375ca6cc4d964b893f37e88293af6ec6fc3fa64/src/assets/styles/_mixins.scss -------------------------------------------------------------------------------- /src/assets/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $ossBaseUrl: '//yancey-assets.oss-cn-beijing.aliyuncs.com/'; 2 | 3 | $wechatQR: #{$ossBaseUrl}#{'wechat-official-account.jpg'}; 4 | $twitterQR: #{$ossBaseUrl}#{'twitter-qr-code.jpg'}; 5 | $xmas: #{$ossBaseUrl}#{'cap.12a740a.svg'}; 6 | $andriod: #{$ossBaseUrl}#{'andriod-effect.png'}; 7 | $mac: #{$ossBaseUrl}#{'mbp_effect.png'}; 8 | $iOS: #{$ossBaseUrl}#{'ios_effect_meitu_1.png'}; 9 | 10 | $black: #000000; 11 | $white: #ffffff; 12 | $svg_color: #666666; 13 | $orange: #ffa500; 14 | $blue: #39b3ed; 15 | -------------------------------------------------------------------------------- /src/assets/styles/global.scss: -------------------------------------------------------------------------------- 1 | // main 2 | @font-face { 3 | font-family: 'Ubuntu'; 4 | src: url('../../assets/fonts/Ubuntu-Regular.woff2'); 5 | font-display: swap; 6 | } 7 | 8 | // code tag 9 | @font-face { 10 | font-family: 'Dank Mono'; 11 | src: url('../../assets/fonts/DankMono-Italic.woff2'); 12 | font-display: swap; 13 | } 14 | 15 | * { 16 | margin: 0; 17 | padding: 0; 18 | box-sizing: border-box; 19 | } 20 | 21 | html { 22 | font-size: 20px; 23 | } 24 | 25 | body { 26 | font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, 27 | Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, WenQuanYi Micro Hei, 28 | sans-serif; 29 | -webkit-font-smoothing: antialiased; 30 | position: relative; 31 | } 32 | 33 | html, 34 | body { 35 | cursor: url('../images/normal.cur'), auto; 36 | } 37 | 38 | .full_site_gray { 39 | filter: grayscale(100%); 40 | } 41 | 42 | .content { 43 | min-height: 100vh; 44 | } 45 | 46 | ::-webkit-scrollbar { 47 | width: 6px; 48 | } 49 | 50 | ::-webkit-scrollbar-thumb { 51 | border-radius: 6px; 52 | background: rgba(#3a3a3a, 0.6); 53 | } 54 | 55 | li { 56 | list-style-type: none; 57 | } 58 | 59 | a { 60 | text-decoration: none; 61 | outline: none; 62 | } 63 | 64 | .no-user-select { 65 | user-select: none; 66 | } 67 | 68 | .overlay { 69 | position: absolute; 70 | top: 0; 71 | right: 0; 72 | bottom: 0; 73 | left: 0; 74 | margin: auto; 75 | background-color: rgba(#000, 0.6); 76 | } 77 | 78 | .clearfix::after { 79 | content: '.'; 80 | clear: both; 81 | display: block; 82 | height: 0; 83 | overflow: hidden; 84 | visibility: hidden; 85 | } 86 | 87 | .clearfix { 88 | zoom: 1; 89 | } 90 | 91 | .Toastify__toast-container { 92 | box-sizing: border-box !important; 93 | margin: 0 !important; 94 | padding: 0 !important; 95 | color: rgba(0, 0, 0, 0.65) !important; 96 | font-size: 14px !important; 97 | font-variant: tabular-nums !important; 98 | line-height: 1.5 !important; 99 | list-style: none !important; 100 | font-feature-settings: 'tnum' !important; 101 | position: fixed !important; 102 | top: 16px !important; 103 | left: 0 !important; 104 | z-index: 99999 !important; 105 | width: 100% !important; 106 | pointer-events: none !important; 107 | text-align: center; 108 | } 109 | 110 | .toasting { 111 | display: inline-block !important; 112 | padding: 6px 12px !important; 113 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; 114 | background: #fff !important; 115 | min-height: 0 !important; 116 | border-radius: 4px !important; 117 | font-size: 14px !important; 118 | color: rgba(0, 0, 0, 0.65) !important; 119 | } 120 | 121 | .Toastify__close-button { 122 | display: none; 123 | } 124 | 125 | @media screen and (max-width: 2560px) { 126 | html { 127 | font-size: 22px; 128 | } 129 | } 130 | 131 | @media screen and (max-width: 1920px) { 132 | html { 133 | font-size: 20px; 134 | } 135 | } 136 | 137 | @media screen and (max-width: 1440px) { 138 | html { 139 | font-size: 18px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/baseUrl.ts: -------------------------------------------------------------------------------- 1 | import { api } from '@constants/constants'; 2 | 3 | const baseUrl = { 4 | dev: api.dev, 5 | prod: api.prod, 6 | }; 7 | 8 | export default baseUrl; 9 | -------------------------------------------------------------------------------- /src/components/CV/Card.module.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .detail_wrapper { 4 | margin: 0 0 0.8rem; 5 | padding: 1.2rem; 6 | box-shadow: rgba(#000, 0.16) 0 3px 10px, rgba(#000, 0.23) 0 3px 10px; 7 | border-radius: 0.5rem; 8 | color: rgba(#000, 0.87); 9 | transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms; 10 | 11 | &:last-child { 12 | margin-bottom: 0; 13 | } 14 | 15 | .program_name { 16 | display: inline-block; 17 | font-size: 1.2rem; 18 | color: #000; 19 | } 20 | 21 | .summary { 22 | display: flex; 23 | padding: 0.8rem; 24 | 25 | &:last-child { 26 | padding: 0 0.8rem; 27 | } 28 | 29 | .company_name { 30 | font-size: 0.75rem; 31 | } 32 | 33 | .position { 34 | margin: 0.2rem 0; 35 | color: rgba(#000, 0.54); 36 | } 37 | 38 | .work_range { 39 | font-size: 0.7rem; 40 | color: #5da4d9; 41 | } 42 | } 43 | 44 | .work_content { 45 | font-size: 0.7rem; 46 | text-shadow: 0 0 1px rgba(0, 0, 0, 0.1); 47 | line-height: 1.8; 48 | padding: 0 0.8rem 0.8rem; 49 | 50 | .work_content_detail { 51 | color: rgba(#000, 0.87); 52 | } 53 | 54 | .technology_stack { 55 | margin-top: 0.8rem; 56 | font-weight: bold; 57 | color: #5da4d9; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/CV/Card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './Card.module.scss'; 3 | import { ICardProps } from '../../types/cv'; 4 | 5 | class Card extends React.Component { 6 | constructor(props: ICardProps) { 7 | super(props); 8 | this.state = {}; 9 | } 10 | 11 | public render() { 12 | const { 13 | type, 14 | name, 15 | position, 16 | inService, 17 | programLink, 18 | detail, 19 | techStack, 20 | } = this.props; 21 | return ( 22 |
23 |
24 |
25 |

26 | {type === 'Work Experience' ? ( 27 | <>{name} 28 | ) : ( 29 | 30 | {name} 31 | 32 | )} 33 |

34 | {type === 'Work Experience' ? ( 35 | <> 36 |

{position}

37 |

38 | {inService[0]} ~ {inService[1]} 39 |

40 | 41 | ) : null} 42 |
43 |
44 |
45 |

{detail}

46 |

47 | Tech: {techStack.toString()} 48 |

49 |
50 |
51 | ); 52 | } 53 | } 54 | 55 | export default Card; 56 | -------------------------------------------------------------------------------- /src/components/Common/AutoBackToTop/AutoBackToTop.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | 4 | class ScrollToTop extends React.Component { 5 | public componentDidUpdate(prevProps: any) { 6 | if (this.props.location.pathname !== prevProps.location.pathname) { 7 | window.scrollTo(0, 0); 8 | } 9 | } 10 | 11 | public render() { 12 | return this.props.children; 13 | } 14 | } 15 | 16 | export default withRouter(ScrollToTop); 17 | -------------------------------------------------------------------------------- /src/components/Common/ErrorMonitor/ErrorMonitor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as Sentry from '@sentry/browser'; 3 | import { sentryDNS } from '@constants/constants'; 4 | 5 | Sentry.init({ 6 | dsn: sentryDNS, 7 | }); 8 | 9 | interface IErrorMonitorState { 10 | error: any; 11 | eventId: any; 12 | } 13 | 14 | class ErrorMonitor extends React.Component<{}, IErrorMonitorState> { 15 | constructor(props: {}) { 16 | super(props); 17 | this.state = { error: null, eventId: null }; 18 | } 19 | 20 | public componentDidCatch(error, errorInfo) { 21 | this.setState({ error }); 22 | Sentry.withScope((scope) => { 23 | scope.setExtras(errorInfo); 24 | const eventId = Sentry.captureException(error); 25 | this.setState({ eventId }); 26 | }); 27 | } 28 | 29 | public render() { 30 | if (this.state.error) { 31 | return ( 32 | 34 | Sentry.showReportDialog({ eventId: this.state.eventId }) 35 | } 36 | > 37 | Report feedback 38 | 39 | ); 40 | } else { 41 | return this.props.children; 42 | } 43 | } 44 | } 45 | 46 | export default ErrorMonitor; 47 | -------------------------------------------------------------------------------- /src/components/Common/Footer/Footer.module.scss: -------------------------------------------------------------------------------- 1 | .yancey_common_footer { 2 | font-family: 'Ubuntu', sans-serif; 3 | padding: 1.8rem 0; 4 | letter-spacing: 0.025rem; 5 | background: #fff; 6 | 7 | .footer_container { 8 | margin: 0 auto; 9 | width: 40rem; 10 | } 11 | 12 | @at-root .creator { 13 | margin-bottom: 0.3rem; 14 | font-size: 0.75rem; 15 | font-weight: bold; 16 | color: #2c2e2f; 17 | 18 | .icon_heart { 19 | width: 0.9rem; 20 | height: 0.9rem; 21 | position: relative; 22 | top: 0.2rem; 23 | margin: 0 0.2rem; 24 | animation: pluse 1500ms infinite linear; 25 | } 26 | } 27 | 28 | .dot_split { 29 | width: 100%; 30 | height: 1px; 31 | background: linear-gradient( 32 | to right, 33 | #b7bcbf 50%, 34 | rgba(255, 255, 255, 0) 40% 35 | ) 36 | repeat-x top; 37 | background-size: 3px 1px; 38 | margin: 0; 39 | padding: 0; 40 | border-top: 0; 41 | border-bottom: 0; 42 | box-sizing: content-box; 43 | } 44 | 45 | @at-root .copyright_wrapper { 46 | display: flex; 47 | justify-content: space-between; 48 | padding-top: 1rem; 49 | font-size: 0.65rem; 50 | color: #6c7378; 51 | 52 | .copyright { 53 | text-shadow: 0 0 1px rgba(0, 0, 0, 0.1); 54 | } 55 | 56 | @at-root .copyright_item { 57 | display: inline-block; 58 | margin-left: 0.4rem; 59 | padding-right: 0.4rem; 60 | border-right: 1px solid #d9d9d9; 61 | 62 | &:last-child { 63 | border-right: none; 64 | padding-right: 0; 65 | } 66 | 67 | a { 68 | color: #6c7378; 69 | transition: all 300ms linear; 70 | 71 | &:hover { 72 | color: #000; 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | @keyframes pluse { 80 | 0% { 81 | transform: scale(1.1); 82 | } 83 | 84 | 50% { 85 | transform: scale(0.8); 86 | } 87 | 88 | 100% { 89 | transform: scale(1.1); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/components/Common/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import svgIcons from '@assets/images/yancey-official-blog-svg-icons.svg'; 4 | import styles from './Footer.module.scss'; 5 | import routePath from '@constants/routePath'; 6 | import { svgSprite, socialMedia } from '@constants/constants'; 7 | 8 | const copyright = { 9 | about: { 10 | url: routePath.about, 11 | name: 'About', 12 | }, 13 | privacyPolicy: { 14 | url: routePath.legal, 15 | name: 'Privacy Policy', 16 | }, 17 | apps: { 18 | url: routePath.apps, 19 | name: 'Apps', 20 | }, 21 | }; 22 | 23 | class Footer extends React.Component<{}, {}> { 24 | constructor(props: {}) { 25 | super(props); 26 | this.state = {}; 27 | } 28 | 29 | public render() { 30 | return ( 31 |
32 |
33 |

34 | Brought to you with 35 | 36 | 37 | 38 | by Yancey 39 |

40 | 41 |
42 |
43 |

44 | Copyright © {new Date().getFullYear()} Yancey Inc. All rights 45 | reserved. 46 |

47 |
    48 | {Object.keys(copyright).map(key => ( 49 |
  • 50 | {copyright[key].name} 51 |
  • 52 | ))} 53 |
  • 54 | Contact 55 |
  • 56 |
57 |
58 |
59 |
60 | ); 61 | } 62 | } 63 | 64 | export default Footer; 65 | -------------------------------------------------------------------------------- /src/components/Common/Header/Header.module.scss: -------------------------------------------------------------------------------- 1 | .yancey_common_header { 2 | position: fixed; 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: center; 6 | width: 100%; 7 | height: 3.25rem; 8 | font: 0.8rem 'Ubuntu', sans-serif; 9 | background: rgba($white, 0.95); 10 | box-shadow: 0 1px 40px -8px rgba($black, 0.5); 11 | z-index: 2; 12 | transition: all 500ms ease; 13 | &:visited { 14 | outline: none; 15 | } 16 | .yancey_logo { 17 | width: 11rem; 18 | height: 2.2rem; 19 | margin-left: 1.5rem; 20 | background: url('../../../assets/images/yancey-official-blog-logo.png') 21 | no-repeat center top; 22 | background-size: cover; 23 | text-indent: -99999px; 24 | } 25 | } 26 | 27 | .clear_navbar_bg { 28 | box-shadow: none; 29 | background: transparent; 30 | transition: all 600ms ease; 31 | &:hover { 32 | background: rgba($white, 0.95); 33 | box-shadow: 0 1px 40px -8px rgba($black, 0.5); 34 | } 35 | } 36 | 37 | .yancey_nav_item { 38 | position: relative; 39 | display: inline-block; 40 | margin-right: 1.4rem; 41 | padding-right: 0.2rem; 42 | cursor: pointer; 43 | &:last-child::after { 44 | height: 0; 45 | } 46 | &::after { 47 | position: absolute; 48 | content: ''; 49 | width: 0; 50 | height: 4px; 51 | background: $orange; 52 | bottom: 0; 53 | left: 0; 54 | transition: all 300ms ease; 55 | } 56 | &:hover::after { 57 | width: 100%; 58 | transition: all 300ms ease; 59 | } 60 | .icon_search { 61 | position: relative; 62 | top: 0.4rem; 63 | width: 1.3rem; 64 | height: 1.3rem; 65 | fill: $svg_color; 66 | } 67 | a { 68 | display: block; 69 | height: 3.25rem; 70 | line-height: 3.25rem; 71 | color: $svg_color; 72 | .header_icon { 73 | position: relative; 74 | top: 0.1rem; 75 | width: 0.8rem; 76 | height: 0.8rem; 77 | fill: $svg_color; 78 | margin-right: 0.4rem; 79 | } 80 | .menu_name { 81 | text-transform: capitalize; 82 | transition: all 500ms linear; 83 | } 84 | &:hover { 85 | color: $orange; 86 | > .header_icon { 87 | fill: $orange; 88 | } 89 | } 90 | &:hover > .icon_home { 91 | animation: horizontal 2s ease infinite; 92 | } 93 | &:hover > .icon_blog { 94 | animation: wrench 2s ease infinite; 95 | } 96 | &:hover > .icon_archive { 97 | animation: vertical 2s ease infinite; 98 | } 99 | &:hover > .icon_music { 100 | animation: tada 2s ease infinite; 101 | } 102 | &:hover > .icon_photo { 103 | animation: horizontal 2s ease infinite; 104 | } 105 | &:hover > .icon_CV { 106 | animation: wrench 2s ease infinite; 107 | } 108 | &:hover > .icon_apps { 109 | animation: pulse 2s ease infinite; 110 | } 111 | &:hover > .icon_RSS { 112 | animation: tada 2s ease infinite; 113 | } 114 | } 115 | } 116 | 117 | @keyframes horizontal { 118 | 0% { 119 | transform: translate(0, 0); 120 | } 121 | 6% { 122 | transform: translate(5px, 0); 123 | } 124 | 12% { 125 | transform: translate(0, 0); 126 | } 127 | 18% { 128 | transform: translate(5px, 0); 129 | } 130 | 24% { 131 | transform: translate(0, 0); 132 | } 133 | 30% { 134 | transform: translate(5px, 0); 135 | } 136 | 100%, 137 | 36% { 138 | transform: translate(0, 0); 139 | } 140 | } 141 | 142 | @keyframes wrench { 143 | 0% { 144 | transform: rotate(-12deg); 145 | } 146 | 8% { 147 | transform: rotate(12deg); 148 | } 149 | 10% { 150 | transform: rotate(24deg); 151 | } 152 | 18%, 153 | 20% { 154 | transform: rotate(-24deg); 155 | } 156 | 28%, 157 | 30% { 158 | transform: rotate(24deg); 159 | } 160 | 38%, 161 | 40% { 162 | transform: rotate(-24deg); 163 | } 164 | 48%, 165 | 50% { 166 | transform: rotate(24deg); 167 | } 168 | 58%, 169 | 60% { 170 | transform: rotate(-24deg); 171 | } 172 | 68% { 173 | transform: rotate(24deg); 174 | } 175 | 100%, 176 | 75% { 177 | transform: rotate(0); 178 | } 179 | } 180 | 181 | @keyframes vertical { 182 | 0% { 183 | transform: translate(0, -3px); 184 | } 185 | 4% { 186 | transform: translate(0, 3px); 187 | } 188 | 8% { 189 | transform: translate(0, -3px); 190 | } 191 | 12% { 192 | transform: translate(0, 3px); 193 | } 194 | 16% { 195 | transform: translate(0, -3px); 196 | } 197 | 20% { 198 | transform: translate(0, 3px); 199 | } 200 | 100%, 201 | 22% { 202 | transform: translate(0, 0); 203 | } 204 | } 205 | 206 | @keyframes tada { 207 | 0% { 208 | transform: scale(1); 209 | } 210 | 10%, 211 | 20% { 212 | transform: scale(0.9) rotate(-8deg); 213 | } 214 | 30%, 215 | 50%, 216 | 70% { 217 | transform: scale(1.3) rotate(8deg); 218 | } 219 | 40%, 220 | 60% { 221 | transform: scale(1.3) rotate(-8deg); 222 | } 223 | 100%, 224 | 80% { 225 | transform: scale(1) rotate(0); 226 | } 227 | } 228 | 229 | @keyframes pulse { 230 | 0% { 231 | transform: scale(1.1); 232 | } 233 | 50% { 234 | transform: scale(0.8); 235 | } 236 | 100% { 237 | transform: scale(1.1); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/components/Common/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { observer, inject } from 'mobx-react'; 4 | import cs from 'classnames'; 5 | import _ from 'lodash'; 6 | import Search from '@components/Post/Search/Search'; 7 | import styles from './Header.module.scss'; 8 | import routePath from '@constants/routePath'; 9 | import { svgSprite } from '@constants/constants'; 10 | import svgIcons from '@assets/images/yancey-official-blog-svg-icons.svg'; 11 | import { noop } from '@tools/tools'; 12 | import { IArticleProps, IHeaderState } from '../../../types/article'; 13 | 14 | const headerList = { 15 | home: { 16 | url: routePath.home, 17 | icon: svgSprite.home, 18 | }, 19 | blog: { 20 | url: routePath.blog, 21 | icon: svgSprite.blog, 22 | }, 23 | archive: { 24 | url: routePath.archive, 25 | icon: svgSprite.archive, 26 | }, 27 | music: { 28 | url: routePath.music, 29 | icon: svgSprite.music, 30 | }, 31 | apps: { 32 | url: routePath.apps, 33 | icon: svgSprite.apps, 34 | }, 35 | CV: { 36 | url: routePath.cv, 37 | icon: svgSprite.cv, 38 | }, 39 | }; 40 | 41 | @inject('articleStore') 42 | @observer 43 | class Header extends React.Component { 44 | constructor(props: IArticleProps) { 45 | super(props); 46 | this.state = { 47 | isTop: true, 48 | }; 49 | } 50 | 51 | public componentDidMount() { 52 | this.switchNavbarBackgroundColor(); 53 | } 54 | 55 | public componentWillUnmount() { 56 | window.removeEventListener('scroll', noop); 57 | } 58 | 59 | public switchNavbarBackgroundColor() { 60 | const top = document.documentElement.scrollTop || document.body.scrollTop; 61 | if (!top) { 62 | this.setState({ 63 | isTop: true, 64 | }); 65 | } 66 | window.addEventListener( 67 | 'scroll', 68 | _.throttle(() => { 69 | const tops = 70 | document.documentElement.scrollTop || document.body.scrollTop; 71 | if (!tops) { 72 | this.setState({ 73 | isTop: true, 74 | }); 75 | } else { 76 | this.setState({ 77 | isTop: false, 78 | }); 79 | } 80 | }, 150), 81 | ); 82 | } 83 | 84 | public render() { 85 | const { isTop } = this.state; 86 | const { articleStore } = this.props; 87 | return ( 88 |
95 | 96 | Yancey Official Blog 97 | 98 | 130 |
131 | ); 132 | } 133 | } 134 | 135 | export default Header; 136 | -------------------------------------------------------------------------------- /src/components/Common/Loading/Loading.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | class Loading extends React.Component<{}, {}> { 4 | constructor(props: {}) { 5 | super(props); 6 | this.state = {}; 7 | } 8 | 9 | public render() { 10 | const styles = { 11 | position: 'fixed' as 'fixed', 12 | top: 0, 13 | right: 0, 14 | bottom: 0, 15 | left: 0, 16 | margin: 'auto', 17 | textAlign: 'center' as 'center', 18 | color: '#000000', 19 | width: '96px', 20 | height: '96px', 21 | background: 22 | 'url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjBweCIgeT0iMHB4Igp3aWR0aD0iOTYiIGhlaWdodD0iOTYiCnZpZXdCb3g9IjAgMCA0OCA0OCIKc3R5bGU9IiBmaWxsOiMwMDAwMDA7Ij48ZyBpZD0ic3VyZmFjZTEiPjxwYXRoIHN0eWxlPSIgZmlsbDojMDNBOUY0OyIgZD0iTSA0MiAxMi40Mjk2ODggQyA0MC42NzU3ODEgMTMuMDE1NjI1IDM5LjI1MzkwNiAxMy40MDYyNSAzNy43NTM5MDYgMTMuNTg5ODQ0IEMgMzkuMjc3MzQ0IDEyLjY4MzU5NCA0MC40NTMxMjUgMTEuMjM4MjgxIDQxLjAwMzkwNiA5LjUzMTI1IEMgMzkuNTc0MjE5IDEwLjM3MTA5NCAzNy45OTIxODggMTAuOTg0Mzc1IDM2LjMxMjUgMTEuMzA4NTk0IEMgMzQuOTY4NzUgOS44ODI4MTMgMzMuMDUwNzgxIDkgMzAuOTI1NzgxIDkgQyAyNi44NDc2NTYgOSAyMy41MzkwNjMgMTIuMjc3MzQ0IDIzLjUzOTA2MyAxNi4zMjAzMTMgQyAyMy41MzkwNjMgMTYuODkwNjI1IDIzLjYwNTQ2OSAxNy40NDkyMTkgMjMuNzMwNDY5IDE3Ljk4ODI4MSBDIDE3LjU5Mzc1IDE3LjY4MzU5NCAxMi4xNDg0MzggMTQuNzY1NjI1IDguNTA3ODEzIDEwLjMzNTkzOCBDIDcuODY3MTg4IDExLjQxNzk2OSA3LjUwNzgxMyAxMi42ODM1OTQgNy41MDc4MTMgMTQuMDIzNDM4IEMgNy41MDc4MTMgMTYuNTYyNSA4LjgwODU5NCAxOC44MDA3ODEgMTAuNzkyOTY5IDIwLjExNzE4OCBDIDkuNTgyMDMxIDIwLjA4MjAzMSA4LjQ0MTQwNiAxOS43NDIxODggNy40NDUzMTMgMTkuMjAzMTI1IEMgNy40NDUzMTMgMTkuMjI2NTYzIDcuNDQ1MzEzIDE5LjI1NzgxMyA3LjQ0NTMxMyAxOS4yODkwNjMgQyA3LjQ0NTMxMyAyMi44Mzk4NDQgOS45OTIxODggMjUuNzk2ODc1IDEzLjM2NzE4OCAyNi40NzI2NTYgQyAxMi43NSAyNi42NDA2MjUgMTIuMDk3NjU2IDI2LjczNDM3NSAxMS40MjU3ODEgMjYuNzM0Mzc1IEMgMTAuOTQ5MjE5IDI2LjczNDM3NSAxMC40ODQzNzUgMjYuNjc5Njg4IDEwLjAzNTE1NiAyNi41OTc2NTYgQyAxMC45NzI2NTYgMjkuNSAxMy42OTkyMTkgMzEuNjIxMDk0IDE2LjkzMzU5NCAzMS42ODM1OTQgQyAxNC40MDIzNDQgMzMuNjQ0NTMxIDExLjIxODc1IDM0LjgyMDMxMyA3Ljc1NzgxMyAzNC44MjAzMTMgQyA3LjE2MDE1NiAzNC44MjAzMTMgNi41NzQyMTkgMzQuNzg1MTU2IDUuOTk2MDk0IDM0LjcxNDg0NCBDIDkuMjY5NTMxIDM2Ljc4NTE1NiAxMy4xNTIzNDQgMzggMTcuMzIwMzEzIDM4IEMgMzAuOTA2MjUgMzggMzguMzM5ODQ0IDI2Ljg0Mzc1IDM4LjMzOTg0NCAxNy4xNjQwNjMgQyAzOC4zMzk4NDQgMTYuODQ3NjU2IDM4LjMyODEyNSAxNi41MzEyNSAzOC4zMTI1IDE2LjIyMjY1NiBDIDM5Ljc2MTcxOSAxNS4xOTUzMTMgNDEuMDExNzE5IDEzLjkwNjI1IDQyIDEyLjQyOTY4OCAiPjwvcGF0aD48L2c+PC9zdmc+") 50% 50% no-repeat', 23 | backgroundSize: '100%', 24 | }; 25 | 26 | return
; 27 | } 28 | } 29 | 30 | export default Loading; 31 | -------------------------------------------------------------------------------- /src/components/Common/Title/Title.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | export interface ITitleProps { 5 | title: string; 6 | } 7 | 8 | class Title extends React.Component { 9 | constructor(props: ITitleProps) { 10 | super(props); 11 | this.state = {}; 12 | } 13 | 14 | public render() { 15 | const { title } = this.props; 16 | 17 | return ( 18 | 19 | {title} | Yancey Inc. 20 | 21 | ); 22 | } 23 | } 24 | 25 | export default Title; 26 | -------------------------------------------------------------------------------- /src/components/HoC/withPersistentData.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IPersistent from '../../types/presistent'; 3 | 4 | const withPersistentData = (PersistentComponent: any) => { 5 | return class extends React.Component<{}, IPersistent> { 6 | public componentWillMount() { 7 | const isWebp = window.localStorage.getItem('isWebp') === 'true'; 8 | this.setState({ isWebp }); 9 | } 10 | 11 | public render() { 12 | const { isWebp } = this.state; 13 | return ; 14 | } 15 | }; 16 | }; 17 | 18 | export default withPersistentData; 19 | -------------------------------------------------------------------------------- /src/components/Music/Card.module.scss: -------------------------------------------------------------------------------- 1 | $pink: #d62b6b; 2 | $gray: #4a4a4a; 3 | $gray_outline: #bebebe; 4 | $white: #fff; 5 | $aoi: #35c0c0; 6 | 7 | .post_container { 8 | position: relative; 9 | height: 12.4rem; 10 | &::before { 11 | content: ''; 12 | position: absolute; 13 | width: calc(100% - 4px); 14 | height: calc(100% - 4px); 15 | top: 2px; 16 | left: 2px; 17 | outline: 2px dashed $gray_outline; 18 | } 19 | img { 20 | width: calc(100% - 2px); 21 | height: calc(100% - 2px); 22 | position: relative; 23 | top: 1px; 24 | left: 1px; 25 | object-fit: cover; 26 | } 27 | } 28 | 29 | .artist_item { 30 | &::before { 31 | outline-color: $gray_outline; 32 | } 33 | &:hover::before { 34 | outline: none; 35 | transition: all 200ms linear; 36 | } 37 | &:hover .artist_intro { 38 | top: 0; 39 | border: 2px solid $aoi; 40 | box-shadow: 0 15px 30px 0 rgba(0, 0, 0, 0.5); 41 | transition: all 200ms linear; 42 | } 43 | .artist_intro { 44 | top: 50%; 45 | will-change: transform; 46 | transition: all 200ms linear; 47 | .artist_title { 48 | font-size: 0.8rem; 49 | padding-bottom: 1rem; 50 | max-height: 3rem; 51 | overflow: hidden; 52 | text-overflow: ellipsis; 53 | display: -webkit-box; 54 | -webkit-line-clamp: 2; 55 | -webkit-box-orient: vertical; 56 | } 57 | } 58 | } 59 | 60 | .meta_intro { 61 | position: absolute; 62 | padding: 1rem 1.5rem; 63 | left: 1px; 64 | bottom: 1px; 65 | width: calc(100% - 2px); 66 | background: hsla(0, 0%, 100%, 0.9); 67 | overflow: hidden; 68 | } 69 | 70 | .meta_date { 71 | color: $pink; 72 | display: block; 73 | font-size: 0.6rem; 74 | } 75 | 76 | .meta_title { 77 | color: $gray; 78 | margin-right: 1.5rem; 79 | padding: 0.75rem 0 1.5rem; 80 | } 81 | 82 | .music_split { 83 | border: 0; 84 | border-top: 1px solid $pink; 85 | margin-top: 0.4rem; 86 | margin-bottom: 1.6rem; 87 | margin-left: 0; 88 | width: 3rem; 89 | } 90 | 91 | .music_btn { 92 | border: 1px solid $aoi; 93 | border-radius: 2rem; 94 | color: $aoi; 95 | padding: 0.35rem 1.4rem; 96 | font-size: 0.75rem; 97 | transition: all 200ms linear; 98 | &:hover { 99 | background: $aoi; 100 | color: $white; 101 | transition: all 200ms linear; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/components/Music/Card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import cs from 'classnames'; 4 | import styles from './Card.module.scss'; 5 | import { webpSuffix } from '@constants/constants'; 6 | import { formatJSONDate } from '@tools/tools'; 7 | import routePath from '@constants/routePath'; 8 | import { ICardProps } from '../../types/music'; 9 | 10 | class Card extends React.Component { 11 | constructor(props: ICardProps) { 12 | super(props); 13 | this.state = {}; 14 | } 15 | 16 | public render() { 17 | const { type, url, title, date, cover } = this.props; 18 | 19 | const isWebp = window.localStorage.getItem('isWebp') === 'true'; 20 | return ( 21 |
22 | {title} 23 |
24 | 27 |

{title}

28 |
29 | {type === 'note' ? ( 30 | 34 | READ MORE 35 | 36 | ) : ( 37 | 43 | LISTEN 44 | 45 | )} 46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | export default Card; 53 | -------------------------------------------------------------------------------- /src/components/Post/Like/Like.scss: -------------------------------------------------------------------------------- 1 | .like_wrapper{ 2 | display: inline-block; 3 | position: relative; 4 | top: 2px; 5 | margin-left: 1.2rem; 6 | .like_number{ 7 | position: relative; 8 | top: -.2rem; 9 | left: .4rem; 10 | font-size: .7rem; 11 | } 12 | } 13 | 14 | [id='toggle-heart'] { 15 | position: absolute; 16 | left: -100vw; 17 | } 18 | [id='toggle-heart']:checked + label { 19 | color: #e2264d; 20 | will-change: font-size; 21 | animation: heart 1s cubic-bezier(0.17, 0.89, 0.32, 1.49); 22 | } 23 | [id='toggle-heart']:checked + label:before, [id='toggle-heart']:checked + label:after { 24 | animation: inherit; 25 | animation-timing-function: ease-out; 26 | } 27 | [id='toggle-heart']:checked + label:before { 28 | will-change: transform, border-width, border-color; 29 | animation-name: bubble; 30 | } 31 | [id='toggle-heart']:checked + label:after { 32 | will-change: opacity, box-shadow; 33 | animation-name: particles; 34 | } 35 | 36 | [for='toggle-heart'] { 37 | align-self: center; 38 | position: relative; 39 | color: #aab8c2; 40 | font-size: 1.4rem; 41 | user-select: none; 42 | cursor: pointer; 43 | } 44 | [for='toggle-heart']:before, [for='toggle-heart']:after { 45 | position: absolute; 46 | z-index: -1; 47 | top: 50%; 48 | left: 50%; 49 | border-radius: 50%; 50 | content: ''; 51 | } 52 | [for='toggle-heart']:before { 53 | box-sizing: border-box; 54 | margin: -2.25rem; 55 | border: solid 2.25rem #e2264d; 56 | width: 4.5rem; 57 | height: 4.5rem; 58 | transform: scale(0); 59 | } 60 | [for='toggle-heart']:after { 61 | margin: -0.1875rem; 62 | width: 0.375rem; 63 | height: 0.375rem; 64 | box-shadow: 0.32476rem -3rem 0 -0.20625rem #ff8080, -0.32476rem -2.625rem 0 -0.20625rem #ffed80, 2.54798rem -1.61656rem 0 -0.20625rem #ffed80, 1.84982rem -1.89057rem 0 -0.20625rem #a4ff80, 2.85252rem 0.98418rem 0 -0.20625rem #a4ff80, 2.63145rem 0.2675rem 0 -0.20625rem #80ffc8, 1.00905rem 2.84381rem 0 -0.20625rem #80ffc8, 1.43154rem 2.22414rem 0 -0.20625rem #80c8ff, -1.59425rem 2.562rem 0 -0.20625rem #80c8ff, -0.84635rem 2.50595rem 0 -0.20625rem #a480ff, -2.99705rem 0.35095rem 0 -0.20625rem #a480ff, -2.48692rem 0.90073rem 0 -0.20625rem #ff80ed, -2.14301rem -2.12438rem 0 -0.20625rem #ff80ed, -2.25479rem -1.38275rem 0 -0.20625rem #ff8080; 65 | } 66 | 67 | @keyframes heart { 68 | 0%, 17.5% { 69 | font-size: 0; 70 | } 71 | } 72 | @keyframes bubble { 73 | 15% { 74 | transform: scale(1); 75 | border-color: #cc8ef5; 76 | border-width: 2.25rem; 77 | } 78 | 30%, 100% { 79 | transform: scale(1); 80 | border-color: #cc8ef5; 81 | border-width: 0; 82 | } 83 | } 84 | @keyframes particles { 85 | 0%, 20% { 86 | opacity: 0; 87 | } 88 | 25% { 89 | opacity: 1; 90 | box-shadow: 0.32476rem -2.4375rem 0 0rem #ff8080, -0.32476rem -2.0625rem 0 0rem #ffed80, 2.1082rem -1.26585rem 0 0rem #ffed80, 1.41004rem -1.53985rem 0 0rem #a4ff80, 2.30412rem 0.85901rem 0 0rem #a4ff80, 2.08305rem 0.14233rem 0 0rem #80ffc8, 0.76499rem 2.33702rem 0 0rem #80ffc8, 1.18748rem 1.71734rem 0 0rem #80c8ff, -1.35019rem 2.0552rem 0 0rem #80c8ff, -0.60229rem 1.99916rem 0 0rem #a480ff, -2.44865rem 0.22578rem 0 0rem #a480ff, -1.93852rem 0.77557rem 0 0rem #ff80ed, -1.70323rem -1.77366rem 0 0rem #ff80ed, -1.81501rem -1.03204rem 0 0rem #ff8080; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/components/Post/Like/Like.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import './Like.scss'; 4 | import { IArticleProps } from '../../../types/article'; 5 | 6 | @inject('articleStore') 7 | @observer 8 | class Like extends React.Component { 9 | constructor(props: IArticleProps) { 10 | super(props); 11 | this.state = {}; 12 | } 13 | 14 | public render() { 15 | const { articleStore } = this.props; 16 | return ( 17 |
18 | 25 | 26 | 27 | {articleStore!.likeNum} {articleStore!.likeNum > 1 ? 'likes' : 'like'} 28 | 29 |
30 | ); 31 | } 32 | } 33 | 34 | export default Like; 35 | -------------------------------------------------------------------------------- /src/components/Post/LinkCard/LinkCard.module.scss: -------------------------------------------------------------------------------- 1 | .card_item{ 2 | margin-bottom: 1rem; 3 | a{ 4 | position: relative; 5 | display: block; 6 | margin: 0 auto; 7 | width: 100%; 8 | max-width: 100%; 9 | border-radius: .6rem; 10 | overflow: hidden; 11 | } 12 | 13 | .card_bg { 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | right: 0; 18 | bottom: 0; 19 | filter: blur(10px); 20 | background-size: cover; 21 | background-position: 50%; 22 | background-repeat: no-repeat; 23 | } 24 | @at-root .card_content { 25 | position: relative; 26 | display: flex; 27 | align-items: center; 28 | justify-content: space-between; 29 | padding: .6rem; 30 | background-color: hsla(0, 0%, 96%, .8); 31 | .card_title { 32 | font-size: .8rem; 33 | font-weight: 500; 34 | line-height: 1.25; 35 | color: #1a1a1a; 36 | display: block; 37 | max-width: 15rem; 38 | width: 15rem; 39 | text-overflow: ellipsis; 40 | overflow: hidden; 41 | white-space: nowrap; 42 | } 43 | .card_url { 44 | display: block; 45 | margin-top: .5rem; 46 | font-size: .7rem; 47 | line-height: 1.25; 48 | color: #999; 49 | max-width: 15rem; 50 | width: 15rem; 51 | text-overflow: ellipsis; 52 | overflow: hidden; 53 | white-space: nowrap; 54 | } 55 | .card_img_cell { 56 | margin-left: .4rem; 57 | width: 3rem; 58 | height: 3rem; 59 | border-radius: .3rem; 60 | img { 61 | width: 3rem; 62 | height: 3rem; 63 | object-fit: cover; 64 | border-radius: inherit; 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/components/Post/LinkCard/LinkCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import { Link } from 'react-router-dom'; 4 | import styles from './LinkCard.module.scss'; 5 | import { middleThumbSuffix } from '@constants/constants'; 6 | import Skeleton from '@components/Skeletons/LinkCardSkeleton/Skeletons'; 7 | import routePath from '@constants/routePath'; 8 | import { domain } from '@constants/constants'; 9 | import { IArticleProps, IArticleDetail } from '../../../types/article'; 10 | 11 | @inject('articleStore') 12 | @observer 13 | class LinkCard extends React.Component { 14 | constructor(props: IArticleProps) { 15 | super(props); 16 | this.state = {}; 17 | } 18 | 19 | public render() { 20 | const { articleStore } = this.props; 21 | return ( 22 | <> 23 | {articleStore!.isLinkCardLoading ? ( 24 | 25 | ) : ( 26 |
    27 | {articleStore!.hots.map((item: IArticleDetail) => ( 28 |
  • 29 | 30 | 38 | 39 | 40 | {item.title} 41 | 42 | {`${domain}${routePath.blogDetail}${item._id}`} 43 | 44 | 45 | 46 | {item.title} 50 | 51 | 52 | 53 |
  • 54 | ))} 55 |
56 | )} 57 | 58 | ); 59 | } 60 | } 61 | 62 | export default LinkCard; 63 | -------------------------------------------------------------------------------- /src/components/Post/PostSummary/PostSummary.module.scss: -------------------------------------------------------------------------------- 1 | $small_text: #888; 2 | 3 | .blog_summary_content { 4 | font-family: 'Ubuntu', sans-serif; 5 | display: flex; 6 | width: 40rem; 7 | height: 15rem; 8 | margin: 0 auto 2rem; 9 | text-align: right; 10 | border-radius: .5rem; 11 | box-shadow: 0 1px 20px -8px rgba(#000, .5); 12 | text-shadow: 0 0 1px rgba(0, 0, 0, .1); 13 | overflow: hidden; 14 | 15 | .blog_thumb_wrapper { 16 | overflow: hidden; 17 | max-width: 22rem; 18 | width: 22rem; 19 | cursor: pointer; 20 | 21 | .blog_thumb { 22 | height: 100%; 23 | background-color: hsla(0, 0%, 96%, .88); 24 | transition: all 500ms linear; 25 | 26 | &:hover { 27 | transform: scale(1.1); 28 | transition: all 500ms linear; 29 | } 30 | 31 | .img { 32 | width: 100%; 33 | height: 100%; 34 | object-fit: cover; 35 | } 36 | } 37 | } 38 | 39 | .blog_info { 40 | max-width: 18rem; 41 | width: 18rem; 42 | padding: 1rem 1.8rem; 43 | background: #fff; 44 | 45 | svg { 46 | width: .7rem; 47 | height: .7rem; 48 | position: relative; 49 | top: .15rem; 50 | margin-right: .4rem; 51 | fill: $small_text; 52 | } 53 | 54 | .publish_date { 55 | font-size: .6rem; 56 | color: $small_text; 57 | } 58 | 59 | .title { 60 | color: #504e4e; 61 | font-size: .9rem; 62 | overflow: hidden; 63 | text-overflow: ellipsis; 64 | white-space: nowrap; 65 | margin: 1rem 0; 66 | } 67 | 68 | .extra_info { 69 | display: flex; 70 | justify-content: space-between; 71 | width: 100%; 72 | font-size: .6rem; 73 | color: $small_text; 74 | 75 | .category { 76 | a { 77 | max-width: 3rem; 78 | overflow: hidden; 79 | text-overflow: ellipsis; 80 | white-space: nowrap; 81 | } 82 | } 83 | 84 | a { 85 | color: $small_text; 86 | } 87 | } 88 | 89 | .summary_content { 90 | color: rgba(#000, .66); 91 | text-shadow: 0 0 1px rgba(#000, .1); 92 | font-size: .75rem; 93 | margin-top: 1rem; 94 | line-height: 1.5; 95 | height: 5.6rem; 96 | overflow: hidden; 97 | display: -webkit-box; 98 | -webkit-box-orient: vertical; 99 | -webkit-line-clamp: 5; 100 | overflow: hidden; 101 | } 102 | 103 | .show_detail_wrapper { 104 | .icon_more { 105 | width: 1.4rem; 106 | height: 1.4rem; 107 | margin-right: 0; 108 | &:hover{ 109 | fill: #FE9600; 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | .reverse { 117 | flex-direction: row-reverse; 118 | text-align: left; 119 | } -------------------------------------------------------------------------------- /src/components/Post/PostSummary/PostSummary.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import cs from 'classnames'; 3 | import { Link } from 'react-router-dom'; 4 | import { observer, inject } from 'mobx-react'; 5 | import { 6 | LazyLoadImage, 7 | trackWindowScroll, 8 | } from 'react-lazy-load-image-component'; 9 | import 'react-lazy-load-image-component/src/effects/blur.css'; 10 | import styles from './PostSummary.module.scss'; 11 | import svgIcons from '@assets/images/yancey-official-blog-svg-icons.svg'; 12 | import routePath from '@constants/routePath'; 13 | import { formatJSONDate } from '@tools/tools'; 14 | import { webpSuffix, svgSprite } from '@constants/constants'; 15 | import Skeletons from '@components/Skeletons/BlogSummarySkeleton/Skeletons'; 16 | import { IArticleDetail, IArticleProps } from '../../../types/article'; 17 | 18 | @inject('articleStore') 19 | @observer 20 | class PostSummary extends React.Component { 21 | constructor(props: IArticleProps) { 22 | super(props); 23 | this.state = {}; 24 | } 25 | 26 | public render() { 27 | const { articleStore } = this.props; 28 | const isWebp = window.localStorage.getItem('isWebp') === 'true'; 29 | 30 | return ( 31 | <> 32 | {articleStore!.isSummaryLoading ? ( 33 | 34 | ) : ( 35 | articleStore!.posts.map((post: IArticleDetail, key: number) => ( 36 |
43 |
44 | 45 |
46 | 58 |
59 | 60 |
61 |
62 |

63 | 64 | 65 | 66 | Released {formatJSONDate(post.publish_date)} 67 |

68 | 69 |

{post.title}

70 | 71 |
72 | 73 | 74 | 75 | 76 | {post.pv_count} PV 77 | 78 | 79 | 80 | 81 | 82 | 83 | {post.like_count.length}{' '} 84 | {post.like_count.length > 1 ? 'Likes' : 'Like'} 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | {post.tags[0]} 93 | 94 | 95 |
96 |

{post.summary}

97 |
98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 |
106 | )) 107 | )} 108 | 109 | ); 110 | } 111 | } 112 | 113 | export default trackWindowScroll(PostSummary); 114 | -------------------------------------------------------------------------------- /src/components/Post/Search/Search.module.scss: -------------------------------------------------------------------------------- 1 | .search_full_screen { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | background: rgba(#fff, .99); 8 | .icon_close { 9 | position: absolute; 10 | top: 1.2rem; 11 | right: 1.6rem; 12 | width: 1.6rem; 13 | height: 1.6rem; 14 | fill: #B5B5B5; 15 | cursor: pointer; 16 | } 17 | @at-root .search_cell { 18 | position: relative; 19 | width: 30rem; 20 | margin: 0 auto; 21 | top: 36vh; 22 | .search_title { 23 | margin-bottom: 1.2rem; 24 | margin-left: 1.2rem; 25 | color: #404040; 26 | font-size: .9rem; 27 | } 28 | .search_icon { 29 | position: absolute; 30 | top: 3rem; 31 | left: .9rem; 32 | fill: #B5B5B5; 33 | width: 1.6rem; 34 | height: 1.6rem; 35 | } 36 | input { 37 | width: 30rem; 38 | padding: .6rem 1.2rem .6rem 3.2rem; 39 | color: #b5b5b5; 40 | font-family: "Ubuntu", sans-serif; 41 | font-size: 1.4rem; 42 | outline: 0; 43 | border: 1px solid #8f8f8f; 44 | border-radius: 2.7rem; 45 | transition: all 100ms linear; 46 | &:focus { 47 | color: #111; 48 | transition: all 100ms linear; 49 | } 50 | } 51 | } 52 | .miku_chan { 53 | position: absolute; 54 | right: 2rem; 55 | bottom: 2rem; 56 | background: no-repeat center center; 57 | background-size: cover; 58 | width: 16rem; 59 | height: 35rem; 60 | } 61 | } -------------------------------------------------------------------------------- /src/components/Post/Search/Search.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import cs from 'classnames'; 4 | import _ from 'lodash'; 5 | import styles from './Search.module.scss'; 6 | import svgIcons from '@assets/images/yancey-official-blog-svg-icons.svg'; 7 | import { webpSuffix, svgSprite, cup } from '@constants/constants'; 8 | import { IArticleProps } from '../../../types/article'; 9 | 10 | @inject('articleStore') 11 | @observer 12 | class Search extends React.Component { 13 | constructor(props: IArticleProps) { 14 | super(props); 15 | this.state = {}; 16 | } 17 | 18 | public render() { 19 | const { articleStore } = this.props; 20 | const isWebp = window.localStorage.getItem('isWebp') === 'true'; 21 | return ( 22 | 23 | {articleStore!.showSearch ? ( 24 |
25 | articleStore!.toggleShowSearch()} 28 | > 29 | 30 | 31 |
32 |

find something?

33 | 34 | 35 | 36 | 44 |
45 |
53 |
54 | ) : null} 55 |
56 | ); 57 | } 58 | } 59 | 60 | export default Search; 61 | -------------------------------------------------------------------------------- /src/components/Post/Tag/Tag.module.scss: -------------------------------------------------------------------------------- 1 | $gary: #42a6f8; 2 | $gary_hover: #1976d2; 3 | $txt: #fafafa; 4 | 5 | .tags li, 6 | .tags a { 7 | display: inline-block; 8 | height: 28px; 9 | line-height: 28px; 10 | position: relative; 11 | font-size: 12px; 12 | margin-bottom: 1rem; 13 | } 14 | 15 | .tags a { 16 | margin-left: 20px; 17 | padding: 0 10px 0 12px; 18 | background: $gary; 19 | color: $txt; 20 | text-decoration: none; 21 | border-bottom-right-radius: 4px; 22 | border-top-right-radius: 4px; 23 | } 24 | 25 | .tags a::before { 26 | content: ''; 27 | position: absolute; 28 | top: 0; 29 | left: -14px; 30 | border-color: transparent $gary transparent transparent; 31 | border-style: solid; 32 | border-width: 14px 14px 14px 0; 33 | } 34 | 35 | .tags a::after { 36 | content: ''; 37 | position: absolute; 38 | top: 12px; 39 | left: 0; 40 | width: 6px; 41 | height: 6px; 42 | border-radius: 50%; 43 | background: $txt; 44 | box-shadow: -1px -1px 1px 0 rgba($black, 0.3); 45 | } 46 | 47 | .tags a:hover { 48 | background: $gary_hover; 49 | color: $txt; 50 | transition: all 300ms linear; 51 | box-shadow: 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 52 | 0px 8px 10px 1px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12); 53 | } 54 | 55 | .tags a:hover::before { 56 | border-color: transparent $gary_hover transparent transparent; 57 | transition: all 300ms linear; 58 | } 59 | -------------------------------------------------------------------------------- /src/components/Post/Tag/Tag.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import { Link } from 'react-router-dom'; 4 | import styles from './Tag.module.scss'; 5 | import routePath from '@constants/routePath'; 6 | import { IArticleProps } from '../../../types/article'; 7 | 8 | @inject('articleStore') 9 | @observer 10 | class Tag extends React.Component { 11 | constructor(props: IArticleProps) { 12 | super(props); 13 | this.state = {}; 14 | } 15 | 16 | public render() { 17 | const { articleStore } = this.props; 18 | return ( 19 |
    20 | {articleStore!.tags.map((item, index) => ( 21 |
  • 22 | {item} 23 |
  • 24 | ))} 25 |
26 | ); 27 | } 28 | } 29 | 30 | export default Tag; 31 | -------------------------------------------------------------------------------- /src/components/Skeletons/BlogDetailSkeleton/Skeletons.module.scss: -------------------------------------------------------------------------------- 1 | .skeleton_wrapper { 2 | width: 40rem; 3 | padding-top: 2rem; 4 | margin: 0 auto; 5 | 6 | .meta { 7 | display: inline-block; 8 | margin-top: 1rem; 9 | } 10 | 11 | .pv { 12 | margin: 0 .5rem; 13 | } 14 | 15 | .tag { 16 | margin: .5rem; 17 | } 18 | 19 | .blockquote { 20 | margin: 1rem 0; 21 | } 22 | 23 | .split { 24 | margin-top: .3rem; 25 | margin-bottom: .8rem; 26 | } 27 | 28 | .paragh { 29 | margin-bottom: .3rem; 30 | } 31 | } -------------------------------------------------------------------------------- /src/components/Skeletons/BlogDetailSkeleton/Skeletons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Skeleton from 'react-loading-skeleton'; 3 | import styles from './Skeletons.module.scss'; 4 | 5 | class Skeletons extends React.Component { 6 | public render() { 7 | return ( 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 | export default Skeletons; 51 | -------------------------------------------------------------------------------- /src/components/Skeletons/BlogSummarySkeleton/Skeletons.module.scss: -------------------------------------------------------------------------------- 1 | .skeleton_wrapper { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | width: 40rem; 6 | height: 15rem; 7 | margin: 0 auto 2rem; 8 | 9 | .meta { 10 | max-width: 18rem; 11 | width: 18rem; 12 | margin-right: 1.8rem; 13 | .title { 14 | margin: 0.75rem 0; 15 | } 16 | 17 | .info { 18 | display: flex; 19 | align-items: center; 20 | justify-content: space-between; 21 | } 22 | 23 | .summary { 24 | margin-top: 0.6rem; 25 | margin-bottom: 1rem; 26 | line-height: 1.4; 27 | } 28 | } 29 | 30 | .img { 31 | width: 24rem; 32 | } 33 | } 34 | 35 | .skeleton_wrapper_reverse { 36 | flex-direction: row-reverse; 37 | text-align: right; 38 | .meta { 39 | margin-right: 0; 40 | margin-left: 1.8rem; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/Skeletons/BlogSummarySkeleton/Skeletons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Skeleton from 'react-loading-skeleton'; 3 | import classnames from 'classnames'; 4 | import styles from './Skeletons.module.scss'; 5 | 6 | class Skeletons extends React.Component<{}, {}> { 7 | constructor(props: {}) { 8 | super(props); 9 | this.state = {}; 10 | } 11 | public render() { 12 | return ( 13 | <> 14 | {Array.from({ length: 10 }).map((v, k) => ( 15 |
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 | export default Skeletons; 49 | -------------------------------------------------------------------------------- /src/components/Skeletons/FeaturedRecordSkeleton/Skeletons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Skeleton from 'react-loading-skeleton'; 3 | 4 | class Skeletons extends React.Component { 5 | public render() { 6 | const styles = { 7 | margin: '.75rem 0', 8 | }; 9 | return ( 10 | <> 11 | {Array.from({ length: 4 }).map((v, k) => ( 12 |
13 | 14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | ))} 28 | 29 | ); 30 | } 31 | } 32 | 33 | export default Skeletons; 34 | -------------------------------------------------------------------------------- /src/components/Skeletons/LinkCardSkeleton/Skeletons.module.scss: -------------------------------------------------------------------------------- 1 | .skeleton_wrapper { 2 | display: flex; 3 | justify-content: space-between; 4 | width: 20rem; 5 | margin: 0 auto 1rem; 6 | 7 | .meta { 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: space-between; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Skeletons/LinkCardSkeleton/Skeletons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Skeleton from 'react-loading-skeleton'; 3 | import styles from './Skeletons.module.scss'; 4 | 5 | class Skeletons extends React.Component { 6 | public render() { 7 | return ( 8 | <> 9 | {Array.from({ length: 7 }).map((v, k) => ( 10 |
11 | 12 | 13 | 14 | 15 | 16 |
17 | ))} 18 | 19 | ); 20 | } 21 | } 22 | 23 | export default Skeletons; 24 | -------------------------------------------------------------------------------- /src/components/Skeletons/LiveTourSkeleton/Skeletons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Skeleton from 'react-loading-skeleton'; 3 | 4 | class Skeletons extends React.Component { 5 | public render() { 6 | const styles = { 7 | margin: '1rem 0', 8 | }; 9 | return ( 10 | <> 11 | 12 |
13 | 14 |
15 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | export default Skeletons; 22 | -------------------------------------------------------------------------------- /src/components/Skeletons/YanceyMusicSkeleton/Skeletons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Skeleton from 'react-loading-skeleton'; 3 | 4 | class Skeletons extends React.Component { 5 | public render() { 6 | const styles = { 7 | margin: '.75rem 0', 8 | }; 9 | return ( 10 | <> 11 | {Array.from({ length: 4 }).map((v, k) => ( 12 |
13 | 14 |
15 | 16 |
17 |
18 | 19 |
20 | 21 |
22 | ))} 23 | 24 | ); 25 | } 26 | } 27 | 28 | export default Skeletons; 29 | -------------------------------------------------------------------------------- /src/components/Widget/Bubble/Bubble.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Circle { 4 | constructor(radius, color, clearOffset, width, height, ctx, randomColor) { 5 | const that = this; 6 | that.pos = {}; 7 | init(); 8 | 9 | function init() { 10 | that.pos.x = Math.random() * width; 11 | that.pos.y = height + Math.random() * 100; 12 | that.alpha = 0.1 + Math.random() * clearOffset; 13 | that.scale = 0.1 + Math.random() * 0.3; 14 | that.speed = Math.random(); 15 | if (color === 'random') { 16 | that.color = randomColor(); 17 | } else { 18 | that.color = color; 19 | } 20 | } 21 | this.draw = function() { 22 | if (that.alpha <= 0) { 23 | init(); 24 | } 25 | that.pos.y -= that.speed; 26 | that.alpha -= 0.0005; 27 | 28 | const circle = new Path2D(); 29 | circle.arc(that.pos.x, that.pos.y, that.scale * radius, 0, 2 * Math.PI); 30 | ctx.fillStyle = that.color; 31 | ctx.fill(circle); 32 | }; 33 | } 34 | } 35 | 36 | class Bubble extends Component { 37 | constructor(props) { 38 | super(props); 39 | this.state = {}; 40 | 41 | this.width = 0; 42 | this.height = 0; 43 | this.pos = {}; 44 | this.alpha = 0; 45 | this.scale = 0; 46 | this.speed = 0; 47 | this.circles = []; 48 | 49 | this.canvasRef = React.createRef(); 50 | this.ctx = null; 51 | } 52 | 53 | componentDidMount() { 54 | this.initCanvas(); 55 | } 56 | 57 | componentWillUnmount() { 58 | window.cancelAnimationFrame(this.animate); 59 | } 60 | 61 | initCanvas = () => { 62 | const { density, radius, color, clearOffset } = this.props; 63 | 64 | this.width = window.innerWidth; 65 | this.height = window.innerHeight; 66 | 67 | this.canvasRef.current.width = this.width; 68 | this.canvasRef.current.height = this.height; 69 | 70 | this.ctx = this.canvasRef.current.getContext('2d'); 71 | 72 | this.canvasRef.current.parentElement.style.overflow = 'hidden'; 73 | 74 | for (let x = 0; x < this.width * density; x += 1) { 75 | const circle = new Circle( 76 | radius, 77 | color, 78 | clearOffset, 79 | this.width, 80 | this.height, 81 | this.ctx, 82 | this.randomColor, 83 | ); 84 | this.circles.push(circle); 85 | } 86 | 87 | this.animate(); 88 | }; 89 | 90 | randomColor = () => { 91 | const r = Math.floor(Math.random() * 255); 92 | const g = Math.floor(Math.random() * 255); 93 | const b = Math.floor(Math.random() * 255); 94 | const alpha = Math.random().toPrecision(2); 95 | return `rgba(${r}, ${g}, ${b}, ${alpha})`; 96 | }; 97 | 98 | animate = () => { 99 | this.ctx.clearRect(0, 0, this.width, this.height); 100 | for (let i in this.circles) { 101 | this.circles[i].draw(); 102 | } 103 | window.requestAnimationFrame(this.animate); 104 | }; 105 | 106 | render() { 107 | return ; 108 | } 109 | } 110 | 111 | Bubble.defaultProps = { 112 | color: 'rgba(255, 255, 255, .5)', 113 | radius: 16, 114 | density: 0.3, 115 | clearOffset: 0.6, 116 | }; 117 | 118 | export default Bubble; 119 | -------------------------------------------------------------------------------- /src/components/Widget/Player/Player.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import 'aplayer/dist/APlayer.min.css'; 3 | import './player.scss'; 4 | 5 | class Player extends React.Component<{}, {}> { 6 | constructor(props: {}) { 7 | super(props); 8 | this.state = {}; 9 | } 10 | 11 | public render() { 12 | return
; 13 | } 14 | } 15 | 16 | export default Player; 17 | -------------------------------------------------------------------------------- /src/components/Widget/Player/player.scss: -------------------------------------------------------------------------------- 1 | .aplayer .aplayer-lrc p.aplayer-lrc-current { 2 | opacity: 1; 3 | overflow: visible; 4 | height: auto !important; 5 | min-height: 17px; 6 | font-size: 16px; 7 | color: #FE9600; 8 | text-shadow: none; 9 | } 10 | 11 | .aplayer.aplayer-fixed .aplayer-lrc { 12 | bottom: .9rem !important; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Widget/ScrollToTop/ScrollToTop.module.scss: -------------------------------------------------------------------------------- 1 | .back_to_top { 2 | background: url("../../../assets/images/yancey-official-blog-cat-scroll.png") no-repeat 0 0; 3 | width: 70px; 4 | height: 900px; 5 | position: fixed; 6 | top: -60rem; 7 | right: 25px; 8 | transition: 600ms all cubic-bezier(.25, .1, .3, 1.5); 9 | animation: float 2s linear infinite; 10 | outline: none; 11 | &:hover { 12 | cursor: pointer; 13 | } 14 | } 15 | 16 | @keyframes float { 17 | 0% { 18 | transform: translateY(0); 19 | } 20 | 50% { 21 | transform: translateY(-6px); 22 | } 23 | 100% { 24 | transform: translateY(0); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Widget/ScrollToTop/ScrollToTop.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import _ from 'lodash'; 3 | import { noop } from '@tools/tools'; 4 | import styles from './ScrollToTop.module.scss'; 5 | 6 | class ScrollToTop extends React.Component<{}, {}> { 7 | private backToTop = React.createRef(); 8 | constructor(props: {}) { 9 | super(props); 10 | this.state = {}; 11 | this.backToTop = React.createRef(); 12 | } 13 | 14 | public componentDidMount() { 15 | this.handlePosition(); 16 | } 17 | 18 | public scrollToTop = () => { 19 | let timer: number = 0; 20 | cancelAnimationFrame(timer); 21 | const startTime = +new Date(); 22 | const b = document.body.scrollTop || document.documentElement.scrollTop; 23 | const d = 500; 24 | const c = b; 25 | timer = requestAnimationFrame(function func() { 26 | const t = d - Math.max(0, startTime - +new Date() + d); 27 | document.documentElement.scrollTop = document.body.scrollTop = 28 | (t * -c) / d + b; 29 | timer = requestAnimationFrame(func); 30 | if (t === d) { 31 | cancelAnimationFrame(timer); 32 | } 33 | }); 34 | }; 35 | 36 | public handlePosition = () => { 37 | const backToTop = this.backToTop.current; 38 | if (backToTop) { 39 | window.addEventListener( 40 | 'scroll', 41 | _.throttle(() => { 42 | const tops = 43 | document.documentElement.scrollTop || document.body.scrollTop; 44 | if (tops > 800) { 45 | backToTop.style.top = '-10rem'; 46 | } else { 47 | backToTop.style.top = '-60rem'; 48 | } 49 | }, 150), 50 | ); 51 | } 52 | }; 53 | 54 | public componentWillUnmount() { 55 | window.removeEventListener('scroll', noop); 56 | } 57 | 58 | public render() { 59 | return ( 60 |
65 | ); 66 | } 67 | } 68 | 69 | export default ScrollToTop; 70 | -------------------------------------------------------------------------------- /src/constants/constants.ts: -------------------------------------------------------------------------------- 1 | export const domain = 'https://yanceyleo.com'; 2 | export const mDomain = 'https://m.yanceyleo.com'; 3 | export const wwwDomain = 'https://www.yanceyleo.com'; 4 | 5 | export const api = { 6 | prod: 'https://api.yanceyleo.com/api', 7 | dev: '127.0.0.1:3001/api', 8 | }; 9 | 10 | export const sentryDNS = 'https://2998f0f7a05044969a7859a2596e6977@sentry.io/1468725'; 11 | 12 | export const aliOSS = '//yancey-assets.oss-cn-beijing.aliyuncs.com'; 13 | export const webpSuffix = '?x-oss-process=image/format,webp'; 14 | export const thumbSuffix = '?x-oss-process=image/resize,w_120/quality,Q_10'; 15 | export const middleThumbSuffix = 16 | '?x-oss-process=image/resize,w_360/quality,Q_90'; 17 | 18 | export const miku = `${aliOSS}/miku.gif`; 19 | export const blogBg = `${aliOSS}/static/blog_page_header.jpg`; 20 | export const archiveBg = `${aliOSS}/NZqlumt.jpg`; 21 | export const musicBg = `${aliOSS}/static/music_page_header.jpg`; 22 | export const avatar = `${aliOSS}/static/logo_avatar.jpg`; 23 | export const legalBg = `${aliOSS}/static/legal_page_header.jpg`; 24 | export const cup = `${aliOSS}/drink.hash-d2bcd7.png`; 25 | 26 | export const GA = 'UA-114532340-1'; 27 | 28 | export const ipify = 'https://api.ipify.org/'; 29 | 30 | export const byNcSa = 31 | 'https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en'; 32 | 33 | export const livere = 'MTAyMC8zOTU5NC8xNjEyMQ=='; 34 | 35 | export const svgSprite = { 36 | telegram: '#telegram', 37 | github: '#github', 38 | soundcloud: '#soundcloud', 39 | paypal: '#paypal', 40 | rocket1: '#rocket', 41 | megaphone: '#megaphone', 42 | more: '#more', 43 | sandClock: '#sand-clock', 44 | heart: '#heart', 45 | rocket2: '#startup', 46 | instagram: '#instagram', 47 | fire: '#flame', 48 | wechat: '#wechat', 49 | twitter1: '#twitter', 50 | barcelona: '#barcelona', 51 | fireworks: '#fireworks', 52 | eye: '#eye', 53 | apple: '#apple', 54 | ubuntu: '#ubuntu', 55 | new: '#new', 56 | cv: '#curriculum-vitae', 57 | tools: '#gear', 58 | mail: '#mail', 59 | comments1: '#multimedia', 60 | code1: '#html-coding', 61 | openFolder: '#open-folder', 62 | archive: '#archive-black-box', 63 | home: '#home', 64 | share1: '#share-symbol', 65 | rss: '#rss-symbol', 66 | apps: '#app-store-apple-symbol', 67 | blog: '#blogger-letter-logotype', 68 | comments2: '#blog-comment-speech-bubble-symbol', 69 | search1: '#musica-searcher', 70 | photo: '#frame-landscape', 71 | settings: '#settings', 72 | camera: '#photo-camera', 73 | rightArrow: '#right-arrow', 74 | leftArrow: '#left-arrow', 75 | link: '#unlink', 76 | user: '#user', 77 | share2: '#share', 78 | like: '#like', 79 | time: '#time', 80 | closeFolder: '#folder', 81 | music: '#music-player', 82 | leftQuote: '#left-quote', 83 | rightQuote: '#right-quote', 84 | search2: '#magnifying-glass', 85 | history: '#history', 86 | mortarBoard: '#mortarboard', 87 | code2: '#code', 88 | crown: '#placeholder', 89 | tag: '#price-tag', 90 | balloons: '#balloons', 91 | twitter2: '#twitter-1', 92 | planet: '#twitter-1', 93 | astronaut: '#astronaut', 94 | close: '#close', 95 | }; 96 | 97 | export const socialMedia = { 98 | github: { 99 | url: 'https://github.com/YanceyOfficial/', 100 | icon: '#github', 101 | }, 102 | twitter: { 103 | url: 'https://twitter.com/YanceyOfficial/', 104 | icon: '#twitter', 105 | }, 106 | instagram: { 107 | url: 'https://www.instagram.com/yancey_leo/', 108 | icon: '#instagram', 109 | }, 110 | soundCloud: { 111 | url: 'https://soundcloud.com/yancey-leo/', 112 | icon: '#soundcloud', 113 | }, 114 | telegram: { 115 | url: 'https://t.me/YanceyOfficial', 116 | icon: '#telegram', 117 | }, 118 | paypal: { 119 | url: 'https://www.paypal.me/yanceyleo/10usd', 120 | icon: '#paypal', 121 | }, 122 | wechat: { 123 | url: '/', 124 | icon: '#wechat', 125 | }, 126 | email: { 127 | url: 'mailto:yanceyofficial@gmail.com', 128 | icon: '#mail', 129 | }, 130 | }; 131 | -------------------------------------------------------------------------------- /src/constants/routePath.ts: -------------------------------------------------------------------------------- 1 | const routePath = { 2 | home: '/', 3 | blog: '/blog', 4 | search: '/search', 5 | music: '/music', 6 | archive: '/archive', 7 | blogDetail: '/p/', 8 | tag: '/t/', 9 | legal: '/legal', 10 | about: '/about', 11 | apps: '/apps', 12 | cv: '/cv', 13 | notFound: '/404', 14 | }; 15 | 16 | export default routePath; -------------------------------------------------------------------------------- /src/containers/About/About.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import Title from '@components/Common/Title/Title'; 4 | import Swiper from 'swiper/dist/js/swiper.min'; 5 | import 'swiper/dist/css/swiper.min.css'; 6 | import './About.scss'; 7 | import { formatJSONDate } from '@tools/tools'; 8 | import { webpSuffix } from '@constants/constants'; 9 | import { IAbout, IAboutProps } from '../../types/about'; 10 | 11 | @inject('aboutStore') 12 | @observer 13 | class About extends React.Component { 14 | constructor(props: IAboutProps) { 15 | super(props); 16 | this.state = {}; 17 | } 18 | 19 | public async componentDidMount() { 20 | const { aboutStore } = this.props; 21 | await aboutStore!.getAbouts(); 22 | this.handleSwiper(); 23 | } 24 | 25 | public handleSwiper() { 26 | // tslint:disable-next-line:no-unused-expression 27 | new Swiper('.swiper-container', { 28 | direction: 'vertical', 29 | loop: false, 30 | speed: 1600, 31 | pagination: { 32 | el: '.swiper-pagination', 33 | clickable: true, 34 | renderBullet(index: any, className: any) { 35 | const year = document 36 | .querySelectorAll('.swiper-slide') 37 | [index].getAttribute('data-year'); 38 | return `${year}`; 39 | }, 40 | }, 41 | navigation: { 42 | nextEl: '.swiper-button-next', 43 | prevEl: '.swiper-button-prev', 44 | }, 45 | breakpoints: { 46 | 768: { 47 | direction: 'horizontal', 48 | }, 49 | }, 50 | }); 51 | } 52 | 53 | public render() { 54 | const { aboutStore } = this.props; 55 | 56 | const isWebp = window.localStorage.getItem('isWebp') === 'true'; 57 | return ( 58 |
59 | 60 | <div className='about-container'> 61 | <div className='timeline'> 62 | <div className='swiper-container'> 63 | <div className='swiper-wrapper'> 64 | {aboutStore!.abouts.map((item: IAbout) => ( 65 | <div 66 | className='swiper-slide' 67 | key={item._id} 68 | style={{ 69 | backgroundImage: `url(${ 70 | isWebp ? `${item.cover}${webpSuffix}` : item.cover 71 | })`, 72 | }} 73 | data-year={formatJSONDate(item.release_date).slice(0, 10)} 74 | > 75 | <div className='swiper-slide-content'> 76 | <span className='timeline-year'> 77 | {formatJSONDate(item.release_date).slice(0, 10)} 78 | </span> 79 | <h4 className='timeline-title'>{item.title}</h4> 80 | <p className='timeline-text'>{item.introduction}</p> 81 | </div> 82 | </div> 83 | ))} 84 | </div> 85 | <div className='swiper-button-prev' /> 86 | <div className='swiper-button-next' /> 87 | <div className='swiper-pagination' /> 88 | </div> 89 | </div> 90 | </div> 91 | </main> 92 | ); 93 | } 94 | } 95 | 96 | export default About; 97 | -------------------------------------------------------------------------------- /src/containers/Apps/Apps.module.scss: -------------------------------------------------------------------------------- 1 | .apps_wrapper { 2 | display: grid; 3 | grid-template-columns: 1fr 1fr; 4 | grid-column-gap: 1rem; 5 | margin: 0 auto; 6 | padding-top: 4.25rem; 7 | width: 72rem; 8 | height: 100%; 9 | text-align: center; 10 | @at-root .platform { 11 | padding: 3.7rem 0 6.6rem; 12 | .platform_title { 13 | margin-bottom: 0.35rem; 14 | font-size: 0.7rem; 15 | line-height: 1.8rem; 16 | font-weight: 500; 17 | letter-spacing: 0.1rem; 18 | color: rgba($black, 0.5); 19 | } 20 | .platform_type { 21 | font-size: 1.4rem; 22 | margin-bottom: 3.8rem; 23 | font-weight: 300; 24 | line-height: 1.45rem; 25 | letter-spacing: 0.1rem; 26 | } 27 | a { 28 | color: #39b3ed; 29 | } 30 | } 31 | .mobile_wrapper { 32 | background-color: #edf8f5; 33 | @at-root .phone_list { 34 | display: flex; 35 | justify-content: space-around; 36 | width: 18rem; 37 | margin: 0 auto 0; 38 | .phone_item { 39 | display: inline-block; 40 | height: 12rem; 41 | background: no-repeat center top; 42 | background-size: cover; 43 | .platform_name { 44 | display: inline-block; 45 | margin-top: 13rem; 46 | font-size: 0.7rem; 47 | color: rgba(#1d2129, 0.65); 48 | background-color: $white; 49 | line-height: 1.45rem; 50 | padding: 0 0.85rem; 51 | border-radius: 1rem; 52 | } 53 | } 54 | .mobile_android { 55 | width: 5.4rem; 56 | background-image: url($andriod); 57 | } 58 | .mobile_iOS { 59 | width: 6rem; 60 | background-image: url($iOS); 61 | } 62 | } 63 | .mobile_download_url { 64 | margin: 5.05rem auto 0; 65 | width: 60%; 66 | font-size: 1.15rem; 67 | font-weight: 300; 68 | line-height: 1.6; 69 | } 70 | } 71 | @at-root .desktop_wrapper { 72 | background-color: #faf7eb; 73 | .desktop_mac { 74 | margin: 3.8rem auto 1rem; 75 | width: 20.61rem; 76 | height: 12rem; 77 | background: url($mac) no-repeat center top; 78 | background-size: cover; 79 | } 80 | .mac_version_support { 81 | width: 20rem; 82 | margin: 0 auto; 83 | font-size: 0.7rem; 84 | line-height: 1.1rem; 85 | color: rgba(#414a53, 0.4); 86 | a { 87 | color: rgba(#414a53, 0.4); 88 | border-bottom: 1px solid rgba(#414a53, 0.4); 89 | } 90 | } 91 | .download_btn { 92 | margin: 3rem auto 3.5rem; 93 | background-color: #01e675; 94 | border: none; 95 | border-radius: 2rem; 96 | a { 97 | display: inline-block; 98 | padding: 0.9rem 1.6rem; 99 | color: $white; 100 | font-size: 0.8rem; 101 | font-weight: 300; 102 | line-height: 0.95rem; 103 | letter-spacing: 0.1rem; 104 | } 105 | } 106 | .not_mac { 107 | margin-bottom: 1rem; 108 | font-size: 0.675rem; 109 | color: rgba($black, 0.5); 110 | letter-spacing: 0.1rem; 111 | } 112 | .window_download_url { 113 | font-size: 0.85rem; 114 | } 115 | } 116 | @at-root .overlay { 117 | position: fixed; 118 | top: 0; 119 | right: 0; 120 | bottom: 0; 121 | left: 0; 122 | background: rgba($black, 0.8); 123 | z-index: 100; 124 | div { 125 | margin-top: 50vh; 126 | margin-left: 50vw; 127 | transform: translate(-50%, -50%); 128 | font-size: 1rem; 129 | width: 30rem; 130 | height: 12.5rem; 131 | padding: 1.5rem; 132 | border: none; 133 | border-radius: 6px; 134 | color: #f6f6f6; 135 | background: rgba(#1ebea5, 0.6); 136 | h1 { 137 | font-weight: 100; 138 | letter-spacing: 0.1rem; 139 | } 140 | p { 141 | margin: 0.4rem 0; 142 | line-height: 1.6; 143 | font-weight: 100; 144 | letter-spacing: 0.05rem; 145 | } 146 | button { 147 | width: 7.5rem; 148 | margin: 1rem auto; 149 | background: none; 150 | text-align: center; 151 | vertical-align: middle; 152 | border: 1px solid $white; 153 | border-radius: 7px; 154 | outline: none; 155 | a { 156 | display: block; 157 | font-size: 0.75rem; 158 | padding: 0.6rem 0; 159 | color: #f6f6f6; 160 | &:hover { 161 | color: #ffbb39; 162 | } 163 | } 164 | &:hover { 165 | border-color: #ffbb39; 166 | cursor: pointer; 167 | opacity: 1; 168 | } 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/containers/Apps/Apps.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import cs from 'classnames'; 4 | import Title from '@components/Common/Title/Title'; 5 | import styles from './Apps.module.scss'; 6 | import routePath from '@constants/routePath'; 7 | import { domain } from '@constants/constants'; 8 | 9 | class Apps extends React.Component<{}, {}> { 10 | constructor(props: {}) { 11 | super(props); 12 | this.state = {}; 13 | } 14 | 15 | public render() { 16 | return ( 17 | <main className={cs(styles.apps_wrapper, 'no-user-select')}> 18 | <Title title='Apps' /> 19 | <section className={cs(styles.platform, styles.mobile_wrapper)}> 20 | <h3 className={styles.platform_title}> 21 | DOWNLOAD YANCEY BLOG APP FOR 22 | </h3> 23 | <h1 className={styles.platform_type}>Phones</h1> 24 | <div className={styles.phone_list}> 25 | <Link to={routePath.home}> 26 | <figure className={cs(styles.phone_item, styles.mobile_android)}> 27 | <span className={styles.platform_name}>Android</span> 28 | </figure> 29 | </Link> 30 | <Link to={routePath.home}> 31 | <figure className={cs(styles.phone_item, styles.mobile_iOS)}> 32 | <span className={styles.platform_name}>iOS</span> 33 | </figure> 34 | </Link> 35 | </div> 36 | <p className={styles.mobile_download_url}> 37 | Visit{' '} 38 | <Link to={routePath.apps}>{`${domain}${routePath.apps}`}</Link> on 39 | your mobile phone to install. 40 | </p> 41 | </section> 42 | <section className={cs(styles.platform, styles.desktop_wrapper)}> 43 | <h3 className={styles.platform_title}> 44 | DOWNLOAD YANCEY BLOG APP FOR 45 | </h3> 46 | <h1 className={styles.platform_type}>Mac or Windows PC</h1> 47 | <figure className={styles.desktop_mac} /> 48 | <p className={styles.mac_version_support}> 49 | Mac OS X 10.9 and higher. By clicking the Download button, you agree 50 | to our <Link to={routePath.legal}>Terms & Privacy Policy.</Link> 51 | </p> 52 | <button className={styles.download_btn} type='button'> 53 | <Link to={routePath.home}>DOWNLOAD FOR MAC OS X</Link> 54 | </button> 55 | <p className={styles.not_mac}>NOT ON A MAC?</p> 56 | <p className={styles.window_download_url}> 57 | <Link to={routePath.home}> 58 | Download for Windows 8 and higher (64-bit) 59 | </Link> 60 | </p> 61 | </section> 62 | <section className={styles.overlay}> 63 | <div> 64 | <h1>Sorry</h1> 65 | <p> 66 | Those apps are currently under construction. 67 | <br /> 68 | Please check back at a later time. 69 | </p> 70 | <button type='button'> 71 | <Link to={routePath.home}>Go Home</Link> 72 | </button> 73 | </div> 74 | </section> 75 | </main> 76 | ); 77 | } 78 | } 79 | 80 | export default Apps; 81 | -------------------------------------------------------------------------------- /src/containers/Archive/Archive.module.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | $axis: #6ecaf5; 4 | $circle: #00bbff; 5 | $shadow: #bbb; 6 | $white: #fff; 7 | $black: #000; 8 | $orange: #ffa500; 9 | $btn_gray: #e0e1e2; 10 | $btn_green: #21ba45; 11 | $btn_gray_hover: #cacbcd; 12 | $btn_green_hover: #13ae38; 13 | $text: #504e4e; 14 | 15 | .archive_wrapper { 16 | .bg_header { 17 | display: flex; 18 | -ms-flex-pack: center; 19 | justify-content: center; 20 | padding-top: 15rem; 21 | width: 100%; 22 | height: 22rem; 23 | background: no-repeat 10% 60%; 24 | background-size: cover; 25 | 26 | span { 27 | font-size: 3rem; 28 | background: -webkit-gradient(linear, right top, left top, color-stop(33%, #c4987a), color-stop(73%, #ffd5bf)); 29 | background: -webkit-linear-gradient(right, #c4987a 33%, #ffd5bf 73%); 30 | background: linear-gradient(to left, #c4987a 33%, #ffd5bf 73%); 31 | -webkit-background-clip: text; 32 | -webkit-text-fill-color: transparent; 33 | background-clip: text; 34 | -webkit-font-smoothing: antialiased; 35 | } 36 | } 37 | } 38 | 39 | .archive_container { 40 | margin: 1rem auto 0; 41 | max-width: 40rem; 42 | font-size: .8rem; 43 | font-weight: 500; 44 | } 45 | 46 | .fold_unfold_wrapper { 47 | position: relative; 48 | display: flex; 49 | width: 8rem; 50 | margin: 2rem 32rem 0; 51 | 52 | .total_count { 53 | position: absolute; 54 | top: .4rem; 55 | left: -4rem; 56 | } 57 | 58 | .btn { 59 | padding: 0 1rem; 60 | height: 1.8rem; 61 | font-size: .7rem; 62 | font-weight: bold; 63 | outline: none; 64 | border: none; 65 | cursor: pointer; 66 | } 67 | 68 | .left_btn { 69 | background: $btn_gray; 70 | color: rgba(#000, .6); 71 | border-top-left-radius: .2rem; 72 | border-bottom-left-radius: .2rem; 73 | transition: all .2s linear; 74 | 75 | &:hover { 76 | color: rgba(#000, .8); 77 | background: $btn_gray_hover; 78 | transition: all .2s linear; 79 | } 80 | } 81 | 82 | .right_btn { 83 | background: $btn_green; 84 | color: $white; 85 | border-top-right-radius: .2rem; 86 | border-bottom-right-radius: .2rem; 87 | transition: all .2s linear; 88 | 89 | &:hover { 90 | background: $btn_green_hover; 91 | transition: all .2s linear; 92 | } 93 | } 94 | 95 | .or { 96 | position: relative; 97 | width: .2rem; 98 | height: 1.8rem; 99 | 100 | &::before { 101 | position: absolute; 102 | text-align: center; 103 | border-radius: 50%; 104 | content: 'or'; 105 | top: 50%; 106 | left: 50%; 107 | background-color: #fff; 108 | text-shadow: none; 109 | margin-top: -.6rem; 110 | margin-left: -.6rem; 111 | width: 1.2rem; 112 | height: 1.2rem; 113 | line-height: 1.2rem; 114 | color: rgba(0, 0, 0, .4); 115 | font-style: normal; 116 | font-weight: 700; 117 | box-shadow: 0 0 0 1px transparent inset; 118 | } 119 | } 120 | } 121 | 122 | .archive_list_wrapper { 123 | max-width: 40rem; 124 | margin: 0 auto; 125 | } 126 | 127 | .year { 128 | font-size: 1rem; 129 | font-weight: bold; 130 | margin-left: 6rem; 131 | } 132 | 133 | .year_list_wrapper { 134 | position: relative; 135 | padding: .5rem 0; 136 | 137 | &::after { 138 | position: absolute; 139 | content: ''; 140 | top: 0; 141 | left: 7rem; 142 | width: 4px; 143 | height: 100%; 144 | background: $axis; 145 | z-index: -1; 146 | } 147 | 148 | .month { 149 | position: relative; 150 | display: block; 151 | line-height: 1.8; 152 | 153 | &::after { 154 | position: absolute; 155 | content: ''; 156 | top: .2rem; 157 | left: 6.65rem; 158 | width: .9rem; 159 | height: .9rem; 160 | background: $white; 161 | border-radius: 50%; 162 | box-shadow: 1px 1px 1px #bbb; 163 | z-index: 0; 164 | } 165 | 166 | &::before { 167 | position: absolute; 168 | content: ''; 169 | top: .35rem; 170 | left: 6.8rem; 171 | width: .6rem; 172 | height: .6rem; 173 | background: $circle; 174 | border-radius: 50%; 175 | z-index: 1; 176 | } 177 | } 178 | 179 | .day_list_container { 180 | padding-left: 8.2rem; 181 | max-height: 0; 182 | will-change: transform; 183 | overflow: hidden; 184 | transition: all 1s linear; 185 | } 186 | 187 | label { 188 | cursor: s-resize; 189 | } 190 | 191 | input { 192 | position: absolute; 193 | opacity: 0; 194 | z-index: -1; 195 | } 196 | 197 | input:checked~.day_list_container { 198 | max-height: 20rem; 199 | will-change: transform; 200 | transition: all 1s linear; 201 | } 202 | 203 | @at-root .day_item { 204 | position: relative; 205 | line-height: 2; 206 | 207 | &::after { 208 | position: absolute; 209 | content: ''; 210 | top: .4rem; 211 | left: -1.4rem; 212 | width: .6rem; 213 | height: .6rem; 214 | background: $white; 215 | border-radius: 50%; 216 | box-shadow: 1px 1px 1px #bbb; 217 | z-index: 0; 218 | } 219 | 220 | &::before { 221 | position: absolute; 222 | content: ''; 223 | left: -1.3rem; 224 | top: .5rem; 225 | width: .4rem; 226 | height: .4rem; 227 | background: $circle; 228 | border-radius: 50%; 229 | z-index: 1; 230 | } 231 | 232 | .day { 233 | color: $circle; 234 | display: inline-block; 235 | width: 1.2rem; 236 | margin-right: .5rem; 237 | } 238 | 239 | a { 240 | font-size: .75rem; 241 | color: $text; 242 | transition: all 200ms linear; 243 | 244 | &:hover { 245 | color: $orange; 246 | transition: all 200ms linear; 247 | } 248 | } 249 | } 250 | 251 | } -------------------------------------------------------------------------------- /src/containers/Archive/Archive.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import { Link } from 'react-router-dom'; 4 | import Title from '@components/Common/Title/Title'; 5 | import cs from 'classnames'; 6 | import styles from './Archive.module.scss'; 7 | import { archiveBg, webpSuffix } from '@constants/constants'; 8 | import { monthToEN } from '@tools/tools'; 9 | import { IArticleProps } from '../../types/article'; 10 | 11 | @inject('articleStore') 12 | @observer 13 | class Archive extends React.Component<IArticleProps, {}> { 14 | constructor(props: IArticleProps) { 15 | super(props); 16 | this.state = {}; 17 | } 18 | 19 | public componentDidMount() { 20 | const { articleStore } = this.props; 21 | articleStore!.getArchives(); 22 | } 23 | 24 | public unfold() { 25 | const checkboxs = document.querySelectorAll('input[type="checkbox"]'); 26 | checkboxs.forEach(checkbox => { 27 | (checkbox as HTMLInputElement).checked = true; 28 | }); 29 | } 30 | 31 | public fold() { 32 | const checkboxs = document.querySelectorAll('input[type="checkbox"]'); 33 | checkboxs.forEach(checkbox => { 34 | (checkbox as HTMLInputElement).checked = false; 35 | }); 36 | } 37 | 38 | public render() { 39 | const { articleStore } = this.props; 40 | const isWebp = window.localStorage.getItem('isWebp') === 'true'; 41 | return ( 42 | <main className={styles.archive_wrapper}> 43 | <Title title='Archive' /> 44 | <figure 45 | className={cs(styles.bg_header, 'no-user-select')} 46 | style={{ 47 | backgroundImage: `url(${ 48 | isWebp ? `${archiveBg}${webpSuffix}` : archiveBg 49 | })`, 50 | }} 51 | > 52 | <span>Archive</span> 53 | </figure> 54 | <div className={styles.archive_container}> 55 | <div className={styles.fold_unfold_wrapper}> 56 | <button 57 | className={cs(styles.btn, styles.left_btn)} 58 | onClick={() => this.unfold()} 59 | > 60 | Unfold 61 | </button> 62 | <div className={styles.or} /> 63 | <button 64 | className={cs(styles.btn, styles.right_btn)} 65 | onClick={() => this.fold()} 66 | > 67 | Fold 68 | </button> 69 | <p className={styles.total_count}>Total: {articleStore!.total}</p> 70 | </div> 71 | {Object.keys(articleStore!.archives).map(year => ( 72 | <section className={styles.archive_list_wrapper} key={year}> 73 | <h2 className={styles.year}> 74 | {articleStore!.archives[year]._id.year} 75 | </h2> 76 | <ul className={styles.year_list_wrapper}> 77 | {Object.keys(articleStore!.archives[year].data).map(month => ( 78 | <li key={month}> 79 | <input 80 | id={`tab_${year}_${month}`} 81 | type='checkbox' 82 | name='tabs' 83 | defaultChecked={ 84 | year === '0' && month === '0' ? true : false 85 | } 86 | /> 87 | <label htmlFor={`tab_${year}_${month}`}> 88 | <span className={styles.month}> 89 | {monthToEN( 90 | articleStore!.archives[year].data[month].month, 91 | )} 92 | {'. '}( 93 | {articleStore!.archives[year].data[month].data.length}{' '} 94 | {articleStore!.archives[year].data[month].data.length > 95 | 1 96 | ? 'articles' 97 | : 'article'} 98 | ) 99 | </span> 100 | </label> 101 | <ul className={styles.day_list_container}> 102 | {Object.keys( 103 | articleStore!.archives[year].data[month].data, 104 | ).map(day => ( 105 | <li className={styles.day_item} key={day}> 106 | <span className={styles.day}> 107 | { 108 | articleStore!.archives[year].data[month].data[day] 109 | .day 110 | } 111 | {': '} 112 | </span> 113 | <Link 114 | to={`p/${ 115 | articleStore!.archives[year].data[month].data[day] 116 | .id 117 | }`} 118 | > 119 | { 120 | articleStore!.archives[year].data[month].data[day] 121 | .title 122 | }{' '} 123 | ( 124 | { 125 | articleStore!.archives[year].data[month].data[day] 126 | .pv_count 127 | }{' '} 128 | PV ) 129 | </Link> 130 | </li> 131 | ))} 132 | </ul> 133 | </li> 134 | ))} 135 | </ul> 136 | </section> 137 | ))} 138 | </div> 139 | </main> 140 | ); 141 | } 142 | } 143 | 144 | export default Archive; 145 | -------------------------------------------------------------------------------- /src/containers/Blog/Blog.module.scss: -------------------------------------------------------------------------------- 1 | .bg_header { 2 | display: flex; 3 | justify-content: center; 4 | padding-top: 15rem; 5 | width: 100%; 6 | height: 22rem; 7 | background-repeat: no-repeat; 8 | background-position: center top; 9 | background-size: cover; 10 | span { 11 | font-size: 3rem; 12 | background: -webkit-gradient(linear, right top, left top, color-stop(33%, #c4987a), color-stop(73%, #ffd5bf)); 13 | background: -webkit-linear-gradient(right, #c4987a 33%, #ffd5bf 73%); 14 | background: linear-gradient(to left, #c4987a 33%, #ffd5bf 73%); 15 | -webkit-background-clip: text; 16 | -webkit-text-fill-color: transparent; 17 | background-clip: text; 18 | -webkit-font-smoothing: antialiased; 19 | } 20 | } 21 | 22 | .main_content { 23 | display: grid; 24 | grid-template-columns: 2fr 1fr; 25 | grid-column-gap: 2rem; 26 | width: 62rem; 27 | margin: 3rem auto 0; 28 | .no_articles { 29 | text-align: center; 30 | margin-top: 1rem; 31 | font-size: 1.8rem; 32 | color: #fc625d; 33 | } 34 | .back{ 35 | display: block; 36 | padding: .5rem 2.75rem; 37 | border: 2px solid #F12742; 38 | border-radius: 1.75rem; 39 | color: #F12742; 40 | text-align: center; 41 | margin: 2rem auto; 42 | width: 8rem; 43 | } 44 | } 45 | 46 | .pagination_list { 47 | display: flex; 48 | justify-content: space-around; 49 | width: 32rem; 50 | margin: 0 auto; 51 | .pagination_item { 52 | display: inline-block; 53 | width: 2.2rem; 54 | height: 2.2rem; 55 | border: 1px solid #8f8f8f; 56 | border-radius: 50%; 57 | text-align: center; 58 | transition: all 100ms linear; 59 | &:hover { 60 | border: 1px solid #ffA500; 61 | transition: all 100ms linear; 62 | } 63 | &:hover a { 64 | color: #ffA500; 65 | } 66 | a { 67 | display: inline-block; 68 | width: 100%; 69 | color: #8f8f8f; 70 | line-height: 2.2rem; 71 | text-align: center; 72 | } 73 | } 74 | } 75 | 76 | .aside_wrapper { 77 | margin-top: .2rem; 78 | .tags_container { 79 | margin: .2rem 0 4rem; 80 | } 81 | } 82 | 83 | .aside_title { 84 | display: flex; 85 | align-items: center; 86 | width: 12rem; 87 | margin-bottom: .8rem; 88 | padding-left: .2rem; 89 | padding-bottom: .4rem; 90 | border-bottom: 1px dashed #e6e6e6; 91 | .title_icon { 92 | width: 1.2rem; 93 | height: 1.2rem; 94 | } 95 | .title_name { 96 | margin-left: .6rem; 97 | font-size: .9rem; 98 | font-weight: 400; 99 | color: #666; 100 | } 101 | } 102 | 103 | :global(.rc-pagination) { 104 | margin: 1rem 0; 105 | :global(.rc-pagination-prev) { 106 | outline: none !important; 107 | border-radius: 50% !important; 108 | } 109 | :global(.rc-pagination-next) { 110 | outline: none !important; 111 | border-radius: 50% !important; 112 | } 113 | :global(.rc-pagination-item) { 114 | outline: none !important; 115 | border-radius: 50% !important; 116 | } 117 | :global(button) { 118 | outline: none !important; 119 | } 120 | } 121 | 122 | -------------------------------------------------------------------------------- /src/containers/Blog/Blog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import history from '../../history'; 4 | import { Link } from 'react-router-dom'; 5 | import cs from 'classnames'; 6 | import Pagination from 'rc-pagination'; 7 | import localeInfo from 'rc-pagination/lib/locale/en_US'; 8 | import 'rc-pagination/assets/index.css'; 9 | import styles from './Blog.module.scss'; 10 | import svgIcons from '@assets/images/yancey-official-blog-svg-icons.svg'; 11 | import { blogBg, webpSuffix, svgSprite } from '@constants/constants'; 12 | import routePath from '@constants/routePath'; 13 | import BlogSummary from '@components/Post/PostSummary/PostSummary'; 14 | import Tag from '@components/Post/Tag/Tag'; 15 | import LinkCard from '@components/Post/LinkCard/LinkCard'; 16 | import Title from '@components/Common/Title/Title'; 17 | import { IArticleProps } from '../../types/article'; 18 | 19 | @inject('articleStore') 20 | @observer 21 | class Blog extends React.Component<IArticleProps, {}> { 22 | constructor(props: IArticleProps) { 23 | super(props); 24 | this.state = {}; 25 | } 26 | 27 | public componentDidMount() { 28 | const { articleStore } = this.props; 29 | if (history.location.pathname.includes('t')) { 30 | articleStore!.getPostsByTag(); 31 | } else if (history.location.pathname.includes('blog')) { 32 | articleStore!.getPostsByPage(1); 33 | } 34 | articleStore!.getAllTags(); 35 | articleStore!.getHots(); 36 | } 37 | 38 | public render() { 39 | const { articleStore } = this.props; 40 | const isWebp = window.localStorage.getItem('isWebp') === 'true'; 41 | return ( 42 | <main> 43 | <Title title='Blog' /> 44 | <figure 45 | className={cs(styles.bg_header, 'no-user-select')} 46 | style={{ 47 | backgroundImage: `url(${ 48 | isWebp ? `${blogBg}${webpSuffix}` : blogBg 49 | })`, 50 | }} 51 | > 52 | <span>Tech, Music and Life.</span> 53 | </figure> 54 | <div className={styles.main_content}> 55 | <section> 56 | {!articleStore!.isSummaryLoading && 57 | articleStore!.posts.length === 0 ? ( 58 | <div> 59 | <p className={styles.no_articles}>no articles!</p> 60 | <Link to={routePath.blog} className={styles.back}> 61 | Back 62 | </Link> 63 | </div> 64 | ) : ( 65 | <BlogSummary /> 66 | )} 67 | {/* <BlogSummary /> */} 68 | {history.location.pathname.includes('blog') ? ( 69 | <Pagination 70 | showSizeChanger 71 | showQuickJumper={{ 72 | goButton: <button>OK</button>, 73 | }} 74 | defaultPageSize={10} 75 | defaultCurrent={1} 76 | onChange={articleStore!.onPageChange} 77 | total={articleStore!.total} 78 | locale={localeInfo} 79 | /> 80 | ) : null} 81 | </section> 82 | <aside className={styles.aside_wrapper}> 83 | <section> 84 | <h1 className={styles.aside_title}> 85 | <svg className={styles.title_icon}> 86 | <use xlinkHref={`${svgIcons}${svgSprite.crown}`} /> 87 | </svg> 88 | <span className={styles.title_name}>Top 7 Most Viewed</span> 89 | </h1> 90 | <LinkCard /> 91 | </section> 92 | <section className={styles.tags_container}> 93 | <h1 className={styles.aside_title}> 94 | <svg className={styles.title_icon}> 95 | <use xlinkHref={`${svgIcons}${svgSprite.tag}`} /> 96 | </svg> 97 | <span className={styles.title_name}>Tags</span> 98 | </h1> 99 | <Tag /> 100 | </section> 101 | </aside> 102 | </div> 103 | </main> 104 | ); 105 | } 106 | } 107 | 108 | export default Blog; 109 | -------------------------------------------------------------------------------- /src/containers/CV/CV.module.scss: -------------------------------------------------------------------------------- 1 | .cv_wrapper { 2 | display: grid; 3 | grid-template-columns: 1fr 2fr; 4 | grid-column-gap: 2rem; 5 | width: 60rem; 6 | margin: 0 auto; 7 | padding-top: 5rem; 8 | } 9 | 10 | .cv_basic_container { 11 | width: 100%; 12 | background: #354B6F; 13 | box-shadow: rgba(0, 0, 0, 0.16) 0 3px 10px, rgba(0, 0, 0, 0.23) 0 3px 10px; 14 | border-radius: .5rem; 15 | 16 | .avatar { 17 | height: 13rem; 18 | background-repeat: no-repeat; 19 | background-position: 0 0; 20 | background-size: cover; 21 | border-top-left-radius: .5rem; 22 | border-top-right-radius: .5rem; 23 | } 24 | 25 | .cv_basic { 26 | padding-top: 1rem; 27 | text-align: center; 28 | color: #fff; 29 | border-bottom-left-radius: .5rem; 30 | border-bottom-right-radius: .5rem; 31 | 32 | .identity { 33 | line-height: 1.8; 34 | font-size: .9rem; 35 | } 36 | 37 | .name { 38 | font-size: 1.2rem; 39 | font-weight: bold; 40 | margin-bottom: 1rem; 41 | } 42 | 43 | .media { 44 | a { 45 | text-decoration: none; 46 | color: #fff; 47 | border-bottom: 1px solid #fff; 48 | } 49 | } 50 | 51 | @at-root .self_introduction { 52 | padding: 1rem; 53 | 54 | .self_introduction_content { 55 | margin-bottom: 1rem; 56 | font-size: .8rem; 57 | text-align: left; 58 | line-height: 1.6; 59 | 60 | &:last-child { 61 | margin-bottom: 0; 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | .cv_detail_container { 69 | .cv_detail_item { 70 | display: flex; 71 | align-items: center; 72 | margin: 1rem 0; 73 | 74 | &:first-child { 75 | margin-top: 0; 76 | } 77 | 78 | .item_icon { 79 | width: 2.4rem; 80 | height: 2.4rem; 81 | fill: rgba(0, 0, 0, .54); 82 | } 83 | 84 | .item_name { 85 | margin-left: 1rem; 86 | font-size: 1.4rem; 87 | font-weight: bold; 88 | color: rgba(0, 0, 0, .54); 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/containers/CV/CV.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import cs from 'classnames'; 4 | import Title from '@components/Common/Title/Title'; 5 | import styles from './CV.module.scss'; 6 | import svgIcons from '@assets/images/yancey-official-blog-svg-icons.svg'; 7 | import { socialMedia, svgSprite, webpSuffix } from '@constants/constants'; 8 | import { ICVProps, IWorkExperience, IProgramExperience } from '../../types/cv'; 9 | 10 | import Card from '@components/CV/Card'; 11 | 12 | @inject('cvStore') 13 | @observer 14 | class CV extends React.Component<ICVProps, {}> { 15 | constructor(props: ICVProps) { 16 | super(props); 17 | this.state = {}; 18 | } 19 | 20 | public componentDidMount() { 21 | const { cvStore } = this.props; 22 | cvStore!.getUser(); 23 | cvStore!.getWorkExperience(); 24 | cvStore!.getProgramExperience(); 25 | } 26 | 27 | public render() { 28 | const { cvStore } = this.props; 29 | const isWebp = window.localStorage.getItem('isWebp') === 'true'; 30 | return ( 31 | <main className={styles.cv_wrapper}> 32 | <Title title='CV' /> 33 | <section className={styles.cv_basic_container}> 34 | <figure 35 | className={styles.avatar} 36 | style={{ 37 | backgroundImage: `url(${ 38 | isWebp 39 | ? `${cvStore!.user.avatar}${webpSuffix}` 40 | : cvStore!.user.avatar 41 | })`, 42 | }} 43 | /> 44 | <div className={styles.cv_basic}> 45 | <p className={cs(styles.identity, styles.name)}> 46 | {cvStore!.user.user_name} 47 | </p> 48 | <p className={styles.identity}> 49 | <span>Gender: </span> 50 | Man 51 | </p> 52 | <p className={styles.identity}> 53 | <span>City: </span> 54 | {cvStore!.user.city} 55 | </p> 56 | <p className={styles.identity}> 57 | <span>Age: </span> 58 | {new Date().getFullYear() - 1996} 59 | </p> 60 | <p className={styles.identity}> 61 | <span>Work Experience: </span> 62 | {new Date().getFullYear() - 2017}{' '} 63 | {new Date().getFullYear() - 2017 > 1 ? 'years' : 'year'} 64 | </p> 65 | <p className={styles.identity}> 66 | <span>Position: </span> 67 | Front-end developer 68 | </p> 69 | <p className={cs(styles.identity, styles.media)}> 70 | <span>GitHub: </span> 71 | <a 72 | href={socialMedia.github.url} 73 | target='_blank' 74 | rel='noopener noreferrer' 75 | > 76 | YanceyOfficial 77 | </a> 78 | </p> 79 | <p className={cs(styles.identity, styles.media)}> 80 | <span>Email: </span> 81 | <a href={socialMedia.email.url}> 82 | {socialMedia.email.url.split(':')[1]} 83 | </a> 84 | </p> 85 | <div className={styles.self_introduction}> 86 | <p className={styles.self_introduction_content}> 87 | {cvStore!.user.self_introduction} 88 | </p> 89 | </div> 90 | </div> 91 | </section> 92 | <section className={styles.cv_detail_container}> 93 | <div className={styles.cv_detail_item}> 94 | <svg className={styles.item_icon}> 95 | <use xlinkHref={`${svgIcons}${svgSprite.history}`} /> 96 | </svg> 97 | <span className={styles.item_name}>Work Experience</span> 98 | </div> 99 | {cvStore!.workExperience.map((item: IWorkExperience) => ( 100 | <Card 101 | key={item._id} 102 | type='workExperience' 103 | name={item.enterprise_name} 104 | position={item.position} 105 | inService={item.in_service} 106 | programLink={''} 107 | detail={item.work_content} 108 | techStack={item.work_technology_stack} 109 | /> 110 | ))} 111 | <div className={styles.cv_detail_item}> 112 | <svg className={styles.item_icon}> 113 | <use xlinkHref={`${svgIcons}${svgSprite.code2}`} /> 114 | </svg> 115 | <span className={styles.item_name}>Program Experience</span> 116 | </div> 117 | {cvStore!.programExperience.map((item: IProgramExperience) => ( 118 | <Card 119 | key={item._id} 120 | type='programExperience' 121 | name={item.program_name} 122 | position='' 123 | inService={[]} 124 | programLink={item.program_url} 125 | detail={item.program_content} 126 | techStack={item.program_technology_stack} 127 | /> 128 | ))} 129 | </section> 130 | </main> 131 | ); 132 | } 133 | } 134 | 135 | export default CV; 136 | -------------------------------------------------------------------------------- /src/containers/Legal/Legal.module.scss: -------------------------------------------------------------------------------- 1 | .bg_img { 2 | width: 100%; 3 | height: 20rem; 4 | background-repeat: no-repeat; 5 | background-position: center top; 6 | background-size: cover; 7 | } 8 | 9 | .target_fix { 10 | display: block; 11 | height: 3.5rem; 12 | margin-top: -3.5rem; 13 | visibility: hidden; 14 | } 15 | 16 | .privacy_policy_container { 17 | margin: 3rem auto 2rem; 18 | width: 40rem; 19 | color: #2c2e2f; 20 | @at-root .anchor { 21 | margin-left: 0; 22 | margin-bottom: 3rem; 23 | li { 24 | list-style-type: none; 25 | margin: .9rem 0; 26 | a { 27 | font-size: .9rem; 28 | color: #0070ba; 29 | } 30 | } 31 | } 32 | ul { 33 | margin-left: 1.7rem; 34 | li { 35 | list-style-type: disc; 36 | } 37 | } 38 | h1 { 39 | font-size: 1.8rem; 40 | margin-bottom: 3rem; 41 | font-weight: 300; 42 | } 43 | h2 { 44 | margin-bottom: 1rem; 45 | font-size: 1.2rem; 46 | font-weight: 400; 47 | } 48 | h3 { 49 | font-size: .9rem; 50 | font-weight: bold; 51 | color: #000; 52 | } 53 | p { 54 | margin: .9rem 0; 55 | font-size: .8rem; 56 | text-shadow: 0 0 1px rgba(44, 46, 47, .1); 57 | line-height: 1.8; 58 | font-weight: 300; 59 | a { 60 | color: #0070ba; 61 | } 62 | span { 63 | color: #6B6B6B; 64 | font-weight: bold; 65 | } 66 | } 67 | .update_date { 68 | font-size: .6rem; 69 | font-weight: bold; 70 | text-shadow: none; 71 | margin-top: .4rem; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/containers/Music/FeaturedRecords.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import cs from 'classnames'; 4 | import styles from './Music.module.scss'; 5 | import { webpSuffix } from '@constants/constants'; 6 | import Skeleton from '@components/Skeletons/FeaturedRecordSkeleton/Skeletons'; 7 | import { formatJSONDate } from '@tools/tools'; 8 | import { IMusicProps, IFeaturedRecords } from '../../types/music'; 9 | 10 | @inject('musicStore') 11 | @observer 12 | class FeaturedRecords extends React.Component<IMusicProps, {}> { 13 | constructor(props: IMusicProps) { 14 | super(props); 15 | this.state = {}; 16 | } 17 | 18 | public componentDidMount() { 19 | const { musicStore } = this.props; 20 | musicStore!.getFeaturedRecords(); 21 | } 22 | 23 | public render() { 24 | const { musicStore } = this.props; 25 | 26 | const isWebp = window.localStorage.getItem('isWebp') === 'true'; 27 | return ( 28 | <ul className={styles.featured_records_list}> 29 | {musicStore!.isFeaturedRecordLoading ? ( 30 | <Skeleton /> 31 | ) : ( 32 | musicStore!.featuredRecords.map((item: IFeaturedRecords) => ( 33 | <li className={styles.featured_record_item} key={item._id}> 34 | <figure 35 | className={styles.record_cover} 36 | style={{ 37 | backgroundImage: `url(${ 38 | isWebp ? `${item.cover}${webpSuffix}` : item.cover 39 | })`, 40 | }} 41 | /> 42 | <div className={styles.record_intro}> 43 | <time className={styles.meta_date}> 44 | {formatJSONDate(item.release_date).split(' ')[0]} 45 | </time> 46 | <p className={cs(styles.record_title, styles.meta_title)}> 47 | {item.album_name} 48 | <br /> 49 | <span className={styles.meta_title_artist}> 50 | {item.artist} 51 | </span> 52 | </p> 53 | <hr className={styles.music_split} /> 54 | <a 55 | href={item.buy_url} 56 | className={styles.music_btn} 57 | target='_blank' 58 | rel='noopener noreferrer' 59 | > 60 | BUY NOW 61 | </a> 62 | </div> 63 | </li> 64 | )) 65 | )} 66 | </ul> 67 | ); 68 | } 69 | } 70 | 71 | export default FeaturedRecords; 72 | -------------------------------------------------------------------------------- /src/containers/Music/LiveTours.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import cs from 'classnames'; 4 | import Carousel from 'nuka-carousel'; 5 | import styles from './Music.module.scss'; 6 | import { webpSuffix } from '@constants/constants'; 7 | import { formatJSONDate } from '@tools/tools'; 8 | import Skeleton from '@components/Skeletons/LiveTourSkeleton/Skeletons'; 9 | import { IMusicProps, ILiveTours } from '../../types/music'; 10 | 11 | @inject('musicStore') 12 | @observer 13 | class LiveTour extends React.Component<IMusicProps, {}> { 14 | constructor(props: IMusicProps) { 15 | super(props); 16 | this.state = {}; 17 | } 18 | 19 | public componentDidMount() { 20 | const { musicStore } = this.props; 21 | musicStore!.getLiveTours(); 22 | } 23 | 24 | public render() { 25 | const { musicStore } = this.props; 26 | 27 | const isWebp = window.localStorage.getItem('isWebp') === 'true'; 28 | return ( 29 | <> 30 | {musicStore!.isLiveToursLoading ? ( 31 | <Skeleton /> 32 | ) : ( 33 | <Carousel 34 | autoplay 35 | autoplayInterval={2000} 36 | transitionMode='fade' 37 | wrapAround 38 | > 39 | {musicStore!.liveTours.map((liveTour: ILiveTours) => ( 40 | <div 41 | className={cs( 42 | styles.post_container, 43 | styles.live_tours_container, 44 | )} 45 | key={liveTour._id} 46 | > 47 | <img 48 | key={liveTour._id} 49 | src={ 50 | isWebp ? `${liveTour.poster}${webpSuffix}` : liveTour.poster 51 | } 52 | alt={liveTour.title} 53 | /> 54 | <div className={styles.meta_intro}> 55 | <time className={styles.meta_date}> 56 | {formatJSONDate(liveTour.upload_date).slice(0, 10)} 57 | </time> 58 | <p className={cs(styles.meta_title, styles.live_tour_title)}> 59 | {liveTour.title} 60 | </p> 61 | </div> 62 | </div> 63 | ))} 64 | </Carousel> 65 | )} 66 | </> 67 | ); 68 | } 69 | } 70 | 71 | export default LiveTour; 72 | -------------------------------------------------------------------------------- /src/containers/Music/Music.module.scss: -------------------------------------------------------------------------------- 1 | $pink: #d62b6b; 2 | $text: #4a4a4a; 3 | $gray: #4a4a4a; 4 | $gray_outline: #bebebe; 5 | $white: #fff; 6 | $aoi: #35c0c0; 7 | $slider_height: 25.8rem; 8 | 9 | .post_container { 10 | position: relative; 11 | height: 12.4rem; 12 | &::before { 13 | content: ''; 14 | position: absolute; 15 | width: calc(100% - 4px); 16 | height: calc(100% - 4px); 17 | top: 2px; 18 | left: 2px; 19 | outline: 2px dashed $gray_outline; 20 | } 21 | img { 22 | width: calc(100% - 2px); 23 | height: calc(100% - 2px); 24 | position: relative; 25 | top: 1px; 26 | left: 1px; 27 | object-fit: cover; 28 | } 29 | } 30 | 31 | .meta_intro { 32 | position: absolute; 33 | padding: 1rem 1.5rem; 34 | left: 1px; 35 | bottom: 1px; 36 | width: calc(100% - 2px); 37 | background: hsla(0, 0%, 100%, 0.9); 38 | overflow: hidden; 39 | } 40 | 41 | .meta_date { 42 | color: $pink; 43 | display: block; 44 | font-size: 0.6rem; 45 | } 46 | 47 | .meta_title { 48 | color: $gray; 49 | margin-right: 1.5rem; 50 | padding: 0.75rem 0 1.5rem; 51 | } 52 | 53 | .meta_title_artist { 54 | font-size: 0.7rem; 55 | color: rgba(#000, 0.4); 56 | } 57 | 58 | .column_title { 59 | padding: 2rem 0; 60 | font-size: 1.1rem; 61 | color: $text; 62 | font-weight: 300; 63 | } 64 | 65 | .music_split { 66 | border: 0; 67 | border-top: 1px solid $pink; 68 | margin-top: 0.4rem; 69 | margin-bottom: 1.6rem; 70 | margin-left: 0; 71 | width: 3rem; 72 | } 73 | 74 | .music_btn { 75 | border: 1px solid $aoi; 76 | border-radius: 2rem; 77 | color: $aoi; 78 | padding: 0.35rem 1.4rem; 79 | font-size: 0.75rem; 80 | transition: all 200ms linear; 81 | &:hover { 82 | background: $aoi; 83 | color: $white; 84 | transition: all 200ms linear; 85 | } 86 | } 87 | 88 | // part 1 89 | .bg_cover { 90 | height: 22rem; 91 | background: no-repeat center top; 92 | background-size: cover; 93 | font-size: 2rem; 94 | color: $text; 95 | padding-top: 11rem; 96 | padding-left: 2rem; 97 | text-shadow: 8px 8px 1px rgba(44, 46, 47, 0.1); 98 | h1 { 99 | font-weight: 100; 100 | } 101 | p { 102 | margin: 0.5rem 0 0 0.5rem; 103 | letter-spacing: 0.2rem; 104 | font-weight: 100; 105 | } 106 | } 107 | 108 | .live_tours_artists_wrapper { 109 | width: 72rem; 110 | margin: 0 auto; 111 | display: grid; 112 | grid-template-columns: 1fr 1fr; 113 | grid-column-gap: 1rem; 114 | .live_tour_container { 115 | width: 35.5rem; 116 | } 117 | } 118 | 119 | .live_tours_container { 120 | height: $slider_height; 121 | .live_tour_title { 122 | font-size: 1.1rem; 123 | } 124 | } 125 | 126 | .artists_list { 127 | display: grid; 128 | grid-template-columns: 1fr 1fr; 129 | grid-template-rows: 1fr 1fr; 130 | grid-column-gap: 1rem; 131 | grid-row-gap: 1rem; 132 | height: $slider_height; 133 | } 134 | 135 | // part 2 136 | .featured_records_wrapper { 137 | margin-top: 3.5rem; 138 | box-shadow: inset 0 0 30px 0 rgba(0, 0, 0, 0.5); 139 | background: url('../../assets/images/stripes_grey.png') repeat 0 0; 140 | .featured_records_container { 141 | width: 72rem; 142 | margin: 0 auto; 143 | padding-bottom: 2rem; 144 | } 145 | } 146 | 147 | .featured_records_list { 148 | display: flex; 149 | justify-content: space-between; 150 | .featured_record_item { 151 | display: inline-block; 152 | width: 16.5rem; 153 | height: 29rem; 154 | box-shadow: 0 15px 30px 0 rgba(0, 0, 0, 0.5); 155 | .record_cover { 156 | width: 100%; 157 | height: 16.5rem; 158 | background: no-repeat center top; 159 | background-size: cover; 160 | } 161 | } 162 | } 163 | 164 | .record_intro { 165 | height: 12.5rem; 166 | padding: 1rem 1.5rem; 167 | background: $white; 168 | .record_title { 169 | max-height: 5.15rem; 170 | min-height: 5.15rem; 171 | margin-right: 0; 172 | } 173 | } 174 | 175 | // part 3 176 | .yancey_music_container { 177 | width: 72rem; 178 | margin: 0 auto; 179 | .yancey_music_list { 180 | display: grid; 181 | grid-template-columns: repeat(4, 1fr); 182 | } 183 | } 184 | 185 | /* reset slider style */ 186 | :global(.slider-control-bottomcenter) { 187 | display: none !important; 188 | } 189 | 190 | :global(.slider-control-centerleft) { 191 | display: none !important; 192 | } 193 | 194 | :global(.slider-control-centerright) { 195 | display: none !important; 196 | } 197 | -------------------------------------------------------------------------------- /src/containers/Music/Music.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import cs from 'classnames'; 3 | import Title from '@components/Common/Title/Title'; 4 | import styles from './Music.module.scss'; 5 | import { webpSuffix, musicBg } from '@constants/constants'; 6 | import LiveTour from './LiveTours'; 7 | import FeaturedRecords from './FeaturedRecords'; 8 | import YacneyMusic from './YanceyMusic'; 9 | import MusicNotes from './MusicNotes'; 10 | 11 | class Music extends React.Component<{}, {}> { 12 | constructor(props: {}) { 13 | super(props); 14 | this.state = {}; 15 | } 16 | public render() { 17 | const isWebp = window.localStorage.getItem('isWebp') === 'true'; 18 | return ( 19 | <main className={styles.music_wrapper}> 20 | <Title title='ミュージック' /> 21 | <figure 22 | className={styles.bg_cover} 23 | style={{ 24 | backgroundImage: `url(${ 25 | isWebp ? `${musicBg}${webpSuffix}` : musicBg 26 | })`, 27 | }} 28 | > 29 | <h1>ミュージック</h1> 30 | <p>夢を歌おう~</p> 31 | </figure> 32 | <div className={styles.live_tours_artists_wrapper}> 33 | <section className={styles.live_tour_container}> 34 | <h2 className={styles.column_title}>LIVE TOURS</h2> 35 | <LiveTour /> 36 | </section> 37 | <section> 38 | <h2 className={styles.column_title}>MUSIC NOTES</h2> 39 | <div className={cs(styles.artists_list)}> 40 | <MusicNotes /> 41 | </div> 42 | </section> 43 | </div> 44 | <div className={styles.featured_records_wrapper}> 45 | <section className={styles.featured_records_container}> 46 | <h2 className={styles.column_title}>FEATURED RECORDS</h2> 47 | <FeaturedRecords /> 48 | </section> 49 | </div> 50 | <section className={styles.yancey_music_container}> 51 | <h2 className={styles.column_title}>YANCEY MUSIC</h2> 52 | <div className={cs(styles.artists_list, styles.yancey_music_list)}> 53 | <YacneyMusic /> 54 | </div> 55 | </section> 56 | </main> 57 | ); 58 | } 59 | } 60 | 61 | export default Music; 62 | -------------------------------------------------------------------------------- /src/containers/Music/MusicNotes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import Card from '@components/Music/Card'; 4 | import Skeleton from '@components/Skeletons/YanceyMusicSkeleton/Skeletons'; 5 | import { IMusicProps } from '../../types/music'; 6 | import { IArticleDetail } from '../../types/article'; 7 | 8 | @inject('articleStore') 9 | @observer 10 | class MusicNotes extends React.Component<IMusicProps, {}> { 11 | constructor(props: IMusicProps) { 12 | super(props); 13 | this.state = {}; 14 | } 15 | 16 | public componentWillMount() { 17 | const { articleStore } = this.props; 18 | articleStore!.posts = []; 19 | } 20 | 21 | public componentDidMount() { 22 | const { articleStore } = this.props; 23 | articleStore!.getPostsByTag('Music'); 24 | } 25 | 26 | public render() { 27 | const { articleStore } = this.props; 28 | return ( 29 | <> 30 | {articleStore!.isSummaryLoading ? ( 31 | <Skeleton /> 32 | ) : ( 33 | articleStore!.posts.map((item: IArticleDetail) => ( 34 | <Card 35 | type='note' 36 | key={item._id} 37 | url={item._id} 38 | title={item.summary} 39 | date={item.publish_date} 40 | cover={item.header_cover} 41 | /> 42 | )) 43 | )} 44 | </> 45 | ); 46 | } 47 | } 48 | 49 | export default MusicNotes; 50 | -------------------------------------------------------------------------------- /src/containers/Music/YanceyMusic.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import Card from '@components/Music/Card'; 4 | import Skeleton from '@components/Skeletons/YanceyMusicSkeleton/Skeletons'; 5 | import { IMusicProps, IYanceyMusic } from '../../types/music'; 6 | 7 | @inject('musicStore') 8 | @observer 9 | class YacneyMusic extends React.Component<IMusicProps, {}> { 10 | constructor(props: IMusicProps) { 11 | super(props); 12 | this.state = {}; 13 | } 14 | 15 | public componentDidMount() { 16 | const { musicStore } = this.props; 17 | musicStore!.getYanceyMusic(); 18 | } 19 | 20 | public render() { 21 | const { musicStore } = this.props; 22 | 23 | return ( 24 | <> 25 | {musicStore!.isYanceyMusicLoading ? ( 26 | <Skeleton /> 27 | ) : ( 28 | musicStore!.yanceyMusic.map((item: IYanceyMusic) => ( 29 | <Card 30 | type='yanceyMusic' 31 | key={item._id} 32 | url={item.soundCloud_url} 33 | title={item.title} 34 | date={item.release_date} 35 | cover={item.cover} 36 | /> 37 | )) 38 | )} 39 | </> 40 | ); 41 | } 42 | } 43 | 44 | export default YacneyMusic; 45 | -------------------------------------------------------------------------------- /src/containers/NotFound/NotFound.module.scss: -------------------------------------------------------------------------------- 1 | .not_found_wrapper { 2 | background: #4a575a; 3 | padding-top: 5rem; 4 | height: 100vh; 5 | 6 | .unicorn { 7 | max-width: 100%; 8 | height: 18rem; 9 | margin: 0 0 1.3rem; 10 | } 11 | 12 | .not_found_container { 13 | width: 20rem; 14 | margin: 0 auto 4rem; 15 | text-align: center; 16 | background: transparent; 17 | } 18 | 19 | h2 { 20 | margin-bottom: 1rem; 21 | font-size: 1rem; 22 | font-weight: 400; 23 | color: #aaa; 24 | } 25 | 26 | h3 { 27 | margin: 1rem 0 0.4rem; 28 | text-align: center; 29 | font-size: 1rem; 30 | font-weight: 500; 31 | line-height: 1.4; 32 | padding: 0 1.5rem; 33 | } 34 | 35 | .warning { 36 | margin: 0 0 1.5rem 0; 37 | padding: 0 1rem 0.4rem; 38 | } 39 | .warning p { 40 | font-size: 0.8rem; 41 | color: #fff; 42 | line-height: 1.6; 43 | } 44 | 45 | .warning button { 46 | display: block; 47 | width: 9.5rem; 48 | margin: 3rem auto 1.25rem; 49 | padding: 0.6rem 0; 50 | background: none; 51 | font-size: 0.75rem; 52 | font-weight: 400; 53 | text-align: center; 54 | border: 1px solid #fff; 55 | border-radius: 7px; 56 | color: #fff; 57 | outline: none; 58 | &:hover { 59 | color: #ffbb39; 60 | border-color: #ffbb39; 61 | cursor: pointer; 62 | opacity: 1; 63 | } 64 | } 65 | 66 | .unicorn { 67 | max-width: 100%; 68 | height: 19rem; 69 | background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDUwNS43NiA1NDkuMzgiPgo8cG9seWdvbiBmaWxsPSJub25lIiBzdHJva2U9IiNGMUYyRjIiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBwb2ludHM9Ijg2LjgsMTQyIDE0LjIsMTgyLjQgMTUsMTM1LjIgODEuMSw4NCIvPgo8cG9seWdvbiBmaWxsPSJub25lIiBzdHJva2U9IiNGMUYyRjIiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBwb2ludHM9IjcuMSw4LjEgNjIuMSw5NCA4MS4yLDc5LjciLz4KPHBvbHlnb24gZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRjFGMkYyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgcG9pbnRzPSI4NC4yLDgxLjcgOTAuNywxNDIuMyA3OC45LDMwMS45IDEzNi4yLDI3MS4zIDE2Mi44LDE3Mi45IDEyNC45LDk4LjciLz4KPHBvbHlnb24gZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRjFGMkYyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgcG9pbnRzPSIxNjksMjUyLjMgMTM5LjYsMjY5LjEgMTY1LjQsMTc0LjMgMjAyLjMsMTc3LjciLz4KPHBvbHlnb24gZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRjFGMkYyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgcG9pbnRzPSIxNjUuOCwyNTguNyA3OC45LDMwNy43IDExNS45LDMyNi40IDM2LjMsNTQyIDIwMC4zLDM3OS43IDE2OC42LDM4Mi40Ii8+Cjxwb2x5Z29uIGZpbGw9Im5vbmUiIHN0cm9rZT0iI0YxRjJGMiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHBvaW50cz0iMTc0LjQsMzc3LjggMTg5LjksMzYzIDIwMS43LDM3NS44Ii8+Cjxwb2x5Z29uIGZpbGw9Im5vbmUiIHN0cm9rZT0iI0YxRjJGMiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHBvaW50cz0iMTY5LjksMjU5LjMgMTcyLjQsMzc0IDIyMS4yLDMyOS4zIi8+Cjxwb2x5Z29uIGZpbGw9Im5vbmUiIHN0cm9rZT0iI0YxRjJGMiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHBvaW50cz0iMTcxLjQsMjU1LjUgMjA0LjQsMjI3LjUgMjIxLjIsMzIzLjgiLz4KPHBvbHlnb24gZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRjFGMkYyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgcG9pbnRzPSIyMDcuOSwyMjkgMjIxLjcsMzAzLjkgMjU1LjIsMjc5LjUiLz4KPHBvbHlnb24gZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRjFGMkYyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgcG9pbnRzPSIyMjIuNCwzMDcuNSAyMjYuMiwzMjkuMyAxOTMuMiwzNjAuMyAxOTYuNiwzNjQgMjM0LjcsMzM2LjggMzYzLjQsMzI5LjkgMjQ4LjIsMjY2LjUgMjYwLjQsMjc5LjgiLz4KPHBvbHlnb24gZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRjFGMkYyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgcG9pbnRzPSIxOTguMywzNjYgMjA1LjIsMzc0LjMgMzUyLjQsMzMzLjcgMjM1LjksMzM5LjgiLz4KPHBvbHlnb24gZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRjFGMkYyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgcG9pbnRzPSIyMjYuMiwyNDMuOCAyNDAuOSwyNTkuMyAzNjMuNCwzMjYuOSAyNDguNywyMzQiLz4KPHBvbHlnb24gZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRjFGMkYyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgcG9pbnRzPSIyNzkuMSwyNTQuNSAzNTkuNCwzMjAuMyAzMTIuNywyNTQuNSIvPgo8cG9seWdvbiBmaWxsPSJub25lIiBzdHJva2U9IiNGMUYyRjIiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBwb2ludHM9IjMxNi45LDI1NC41IDM3MS42LDMzMi40IDI3MC45LDM1OS43IDMxMC4zLDQxNy43IDM4My42LDQwMi40IDM3Ny42LDM1NC40IDUwMC45LDM5OCAzNzAuNiwyNjIuNyIvPgo8cG9seWdvbiBmaWxsPSJub25lIiBzdHJva2U9IiNGMUYyRjIiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBwb2ludHM9IjMxNi42LDQxOS43IDM5OC4zLDU0MiAzODIuMyw0MDYuNCIvPgo8L3N2Zz4K) 70 | no-repeat center center; 71 | } 72 | 73 | .four_oh_four { 74 | max-width: 7.5rem; 75 | height: 6rem; 76 | text-indent: -9999px; 77 | margin: 0 auto; 78 | background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxOTYuNjY4IDk1LjY2MSIgPgo8Zz4KCTxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik00OS40NjMgOC4yMmwtMjYuMDkgNDYuOTY0SDYyLjc1djM3LjVoLTEyLjV2LTI1SDIuMTI3bDM2LjQxLTY1LjUzNkw0OS40NjMgOC4yMnogTTEzMS41IDQ4LjkgYzAgMTcuMjYxLTEzLjk4OSAzMS4yNS0zMS4yNSAzMS4yNWMtMTcuMjYyIDAtMzEuMjUtMTMuOTktMzEuMjUtMzEuMjVjMC0xNy4yNTggMTMuOTg5LTMxLjI1IDMxLjI1LTMxLjI1IEMxMTcuNTExIDE3LjcgMTMxLjUgMzEuNyAxMzEuNSA0OC45MzR6IE0xMTkgNDguOTMzYzAtMTAuMzQtOC40MS0xOC43NS0xOC43NS0xOC43NWMtMTAuMzQgMC0xOC43NTEgOC40MS0xOC43NTEgMTguOCBjMCAxMC4zIDguNCAxOC44IDE4LjggMTguNzUxQzExMC41OSA2Ny43IDExOSA1OS4zIDExOSA0OC45MzN6IE0xNTQuNjIgNTUuMTg0SDE5NHYzNy41aC0xMi41di0yNWgtNDguMTIxIGwzNi40MDgtNjUuNTM2bDEwLjkyNiA2LjA3MkwxNTQuNjIgNTUuMTg0eiIvPgo8L2c+Cjwvc3ZnPg==) 79 | no-repeat center center; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/containers/NotFound/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Title from '@components/Common/Title/Title'; 3 | import history from '../../history'; 4 | import styles from './NotFound.module.scss'; 5 | import routePath from '@constants/routePath'; 6 | 7 | class NotFound extends React.Component<{}, {}> { 8 | constructor(props: {}) { 9 | super(props); 10 | this.state = {}; 11 | } 12 | 13 | public handleBack = () => { 14 | history.push(routePath.home); 15 | }; 16 | 17 | public render() { 18 | return ( 19 | <main className={styles.not_found_wrapper}> 20 | <Title title='404' /> 21 | <div className={styles.unicorn} /> 22 | <div className={styles.not_found_container}> 23 | <div className={styles.four_oh_four}> 24 | <h1>404 Error</h1> 25 | </div> 26 | <div className={styles.warning}> 27 | <h2>All those moments will be lost in time, like tears in rain.</h2> 28 | <p> 29 | The page you are looking for might have been removed, had its name 30 | changed, or is temporarily unavailable. 31 | </p> 32 | <button onClick={() => this.handleBack()}>Back To Home</button> 33 | </div> 34 | </div> 35 | </main> 36 | ); 37 | } 38 | } 39 | 40 | export default NotFound; 41 | -------------------------------------------------------------------------------- /src/history.ts: -------------------------------------------------------------------------------- 1 | import * as history from 'history'; 2 | 3 | const createHistory = history.createBrowserHistory; 4 | 5 | export default createHistory(); 6 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { Provider } from 'mobx-react'; 4 | import * as Sentry from '@sentry/browser'; 5 | import Layouts from './layouts/Layouts'; 6 | import stores from './stores/index'; 7 | import * as serviceWorker from './registerServiceWorker'; 8 | import { sentryDNS } from '@constants/constants'; 9 | 10 | if (process.env.NODE_ENV === 'production') { 11 | Sentry.init({ 12 | dsn: sentryDNS, 13 | }); 14 | 15 | Sentry.captureException; 16 | } 17 | 18 | ReactDOM.render( 19 | <Provider {...stores}> 20 | <Layouts /> 21 | </Provider>, 22 | document.getElementById('root'), 23 | ); 24 | 25 | serviceWorker.unregister(); 26 | -------------------------------------------------------------------------------- /src/layouts/Layouts.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import { Router } from 'react-router-dom'; 4 | import classnames from 'classnames'; 5 | import { ToastContainer } from 'react-toastify'; 6 | import ReactGA from 'react-ga'; 7 | 8 | import '@assets/styles/global.scss'; 9 | import 'react-toastify/dist/ReactToastify.css'; 10 | 11 | import history from '../history'; 12 | import { checkWebp, devToolsWarning } from '@tools/tools'; 13 | import { GA } from '@constants/constants'; 14 | 15 | import AutoBackToTop from '@components/Common/AutoBackToTop/AutoBackToTop'; 16 | import Player from '@components/Widget/Player/Player'; 17 | import ScrollToTop from '@components/Widget/ScrollToTop/ScrollToTop'; 18 | import Header from '@components/Common/Header/Header'; 19 | import Footer from '@components/Common/Footer/Footer'; 20 | 21 | import Routers from '../Routers'; 22 | 23 | import { ILayoutsProps } from '../types/layout'; 24 | 25 | @inject('layoutsStore') 26 | @observer 27 | class Layouts extends Component<ILayoutsProps, {}> { 28 | constructor(props: ILayoutsProps) { 29 | super(props); 30 | this.state = {}; 31 | } 32 | 33 | // 页面初始化监听 34 | public componentWillMount() { 35 | window.localStorage.setItem('isWebp', checkWebp().toString()); 36 | this.reactGA(); 37 | devToolsWarning(); 38 | } 39 | 40 | public componentDidMount() { 41 | const { layoutsStore } = this.props; 42 | layoutsStore!.getPlayers(); 43 | layoutsStore!.getGlobalStatus(); 44 | } 45 | 46 | public reactGA() { 47 | ReactGA.initialize(GA); 48 | ReactGA.pageview(window.location.pathname + window.location.search); 49 | history.listen(() => { 50 | ReactGA.pageview(window.location.pathname + window.location.search); 51 | }); 52 | } 53 | 54 | public render() { 55 | const { layoutsStore } = this.props; 56 | const isGray = layoutsStore!.globalStatus.full_site_gray; 57 | 58 | return ( 59 | <div className={classnames(isGray && 'full_site_gray', 'content')}> 60 | <Router history={history}> 61 | <AutoBackToTop> 62 | <Header /> 63 | <div style={{ minHeight: '100vh' }}> 64 | <Routers /> 65 | </div> 66 | <ScrollToTop /> 67 | <Player /> 68 | <Footer /> 69 | <ToastContainer /> 70 | </AutoBackToTop> 71 | </Router> 72 | </div> 73 | ); 74 | } 75 | } 76 | 77 | export default Layouts; 78 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | NODE_ENV: 'development' | 'production' | 'test' 4 | PUBLIC_URL: string 5 | } 6 | } 7 | 8 | declare module '*.bmp' { 9 | const src: string; 10 | export default src; 11 | } 12 | 13 | declare module '*.gif' { 14 | const src: string; 15 | export default src; 16 | } 17 | 18 | declare module '*.jpg' { 19 | const src: string; 20 | export default src; 21 | } 22 | 23 | declare module '*.jpeg' { 24 | const src: string; 25 | export default src; 26 | } 27 | 28 | declare module '*.png' { 29 | const src: string; 30 | export default src; 31 | } 32 | 33 | declare module '*.svg' { 34 | import * as React from 'react'; 35 | 36 | export const ReactComponent: React.SFC<React.SVGProps<SVGSVGElement>>; 37 | 38 | const src: string; 39 | export default src; 40 | } 41 | 42 | declare module '*.module.css' { 43 | const classes: { [key: string]: string }; 44 | export default classes; 45 | } 46 | 47 | declare module '*.module.scss' { 48 | const classes: { [key: string]: string }; 49 | export default classes; 50 | } 51 | 52 | declare module '*.module.sass' { 53 | const classes: { [key: string]: string }; 54 | export default classes; 55 | } -------------------------------------------------------------------------------- /src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-console 2 | // In production, we register a service worker to serve assets from local cache. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on the 'N+1' visit to a page, since previously 7 | // cached resources are updated in the background. 8 | 9 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 10 | // This link also includes instructions on opting out of this behavior. 11 | 12 | const isLocalhost = Boolean( 13 | window.location.hostname === 'localhost' || 14 | // [::1] is the IPv6 localhost address. 15 | window.location.hostname === '[::1]' || 16 | // 127.0.0.1/8 is considered localhost for IPv4. 17 | window.location.hostname.match( 18 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 19 | ) 20 | ); 21 | 22 | export default function register() { 23 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 24 | // The URL constructor is available in all browsers that support SW. 25 | const publicUrl = new URL( 26 | process.env.PUBLIC_URL!, 27 | window.location.toString() 28 | ); 29 | if (publicUrl.origin !== window.location.origin) { 30 | // Our service worker won't work if PUBLIC_URL is on a different origin 31 | // from what our page is served on. This might happen if a CDN is used to 32 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 33 | return; 34 | } 35 | 36 | window.addEventListener('load', () => { 37 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 38 | 39 | if (isLocalhost) { 40 | // This is running on localhost. Lets check if a service worker still exists or not. 41 | checkValidServiceWorker(swUrl); 42 | 43 | // Add some additional logging to localhost, pointing developers to the 44 | // service worker/PWA documentation. 45 | navigator.serviceWorker.ready.then(() => { 46 | console.log( 47 | 'This web app is being served cache-first by a service ' + 48 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 49 | ); 50 | }); 51 | } else { 52 | // Is not local host. Just register service worker 53 | registerValidSW(swUrl); 54 | } 55 | }); 56 | } 57 | } 58 | 59 | function registerValidSW(swUrl: string) { 60 | navigator.serviceWorker 61 | .register(swUrl) 62 | .then(registration => { 63 | registration.onupdatefound = () => { 64 | const installingWorker = registration.installing; 65 | if (installingWorker) { 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the old content will have been purged and 70 | // the fresh content will have been added to the cache. 71 | // It's the perfect time to display a 'New content is 72 | // available; please refresh.' message in your web app. 73 | console.log('New content is available; please refresh.'); 74 | } else { 75 | // At this point, everything has been precached. 76 | // It's the perfect time to display a 77 | // 'Content is cached for offline use.' message. 78 | console.log('Content is cached for offline use.'); 79 | } 80 | } 81 | }; 82 | } 83 | }; 84 | }) 85 | .catch(error => { 86 | console.error('Error during service worker registration:', error); 87 | }); 88 | } 89 | 90 | function checkValidServiceWorker(swUrl: string) { 91 | // Check if the service worker can be found. If it can't reload the page. 92 | fetch(swUrl) 93 | .then(response => { 94 | // Ensure service worker exists, and that we really are getting a JS file. 95 | if ( 96 | response.status === 404 || 97 | response.headers.get('content-type')!.indexOf('javascript') === -1 98 | ) { 99 | // No service worker found. Probably a different app. Reload the page. 100 | navigator.serviceWorker.ready.then(registration => { 101 | registration.unregister().then(() => { 102 | window.location.reload(); 103 | }); 104 | }); 105 | } else { 106 | // Service worker found. Proceed as normal. 107 | registerValidSW(swUrl); 108 | } 109 | }) 110 | .catch(() => { 111 | console.log( 112 | 'No internet connection found. App is running in offline mode.' 113 | ); 114 | }); 115 | } 116 | 117 | export function unregister() { 118 | if ('serviceWorker' in navigator) { 119 | navigator.serviceWorker.ready.then(registration => { 120 | registration.unregister(); 121 | }); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/stores/aboutStore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | observable, 3 | runInAction, 4 | } from 'mobx'; 5 | import { 6 | aboutService, 7 | } from '../apis/index.service'; 8 | import { 9 | IAbout, 10 | } from '../types/about'; 11 | 12 | import { 13 | setToast 14 | } from '@tools/tools'; 15 | 16 | class AboutStore { 17 | @observable public abouts: IAbout[] = []; 18 | 19 | public getAbouts = async () => { 20 | try { 21 | const res = await aboutService.getAbouts(); 22 | runInAction(() => { 23 | this.abouts = res.data; 24 | }); 25 | } catch (e) { 26 | setToast('获取关于失败'); 27 | } 28 | }; 29 | } 30 | 31 | const aboutStore = new AboutStore(); 32 | 33 | export default aboutStore; -------------------------------------------------------------------------------- /src/stores/cvStore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | observable, 3 | runInAction, 4 | } from 'mobx'; 5 | import { 6 | cvService, 7 | } from '../apis/index.service'; 8 | import { 9 | IUser, 10 | IWorkExperience, 11 | IProgramExperience, 12 | } from '../types/cv'; 13 | 14 | import { 15 | setToast 16 | } from '@tools/tools'; 17 | 18 | class CVStore { 19 | @observable public user: IUser = { 20 | avatar: '', 21 | city: '', 22 | position: '', 23 | self_introduction: '', 24 | user_name: '', 25 | }; 26 | @observable public workExperience: IWorkExperience[] = []; 27 | @observable public programExperience: IProgramExperience[] = []; 28 | 29 | public getUser = async () => { 30 | try { 31 | const res = await cvService.getUser(); 32 | runInAction(() => { 33 | this.user = res.data; 34 | }); 35 | } catch (e) { 36 | setToast('获取个人信息失败'); 37 | } 38 | }; 39 | 40 | public getWorkExperience = async () => { 41 | try { 42 | const res = await cvService.getWorkExperience(); 43 | runInAction(() => { 44 | this.workExperience = res.data; 45 | }); 46 | } catch (e) { 47 | setToast('获取工作经历失败'); 48 | } 49 | }; 50 | 51 | public getProgramExperience = async () => { 52 | try { 53 | const res = await cvService.getProgramExperience(); 54 | runInAction(() => { 55 | this.programExperience = res.data; 56 | }); 57 | } catch (e) { 58 | setToast('获取项目经历失败'); 59 | } 60 | }; 61 | } 62 | 63 | const cvStore = new CVStore(); 64 | 65 | export default cvStore; -------------------------------------------------------------------------------- /src/stores/homeStore.ts: -------------------------------------------------------------------------------- 1 | import { observable, runInAction } from 'mobx'; 2 | 3 | import { setToast } from '@tools/tools'; 4 | 5 | import { homeService } from '../apis/index.service'; 6 | 7 | import { webpSuffix } from '../constants/constants'; 8 | 9 | import { IProject } from '../types/home'; 10 | 11 | class HomeStore { 12 | @observable public announcement: string = ''; 13 | @observable public motto: string = ''; 14 | @observable public projects: IProject[] = []; 15 | @observable public coverUrl: string = ''; 16 | 17 | public getAnnouncement = async () => { 18 | try { 19 | const res = await homeService.getAnnouncement(); 20 | runInAction(() => { 21 | this.announcement = res.data.content; 22 | }); 23 | } catch (e) { 24 | setToast('获取 Announcement 失败'); 25 | } 26 | }; 27 | 28 | public getMotto = async () => { 29 | try { 30 | const res = await homeService.getMotto(); 31 | runInAction(() => { 32 | this.motto = res.data.content; 33 | }); 34 | } catch (e) { 35 | setToast('获取 Motto 失败'); 36 | } 37 | }; 38 | 39 | public getProject = async () => { 40 | try { 41 | const res = await homeService.getProject(); 42 | runInAction(() => { 43 | this.projects = res.data; 44 | }); 45 | } catch (e) { 46 | setToast('获取 Projects 失败'); 47 | } 48 | }; 49 | 50 | public loadBgImg = (imageUrl: string) => { 51 | const isWebp = window.localStorage.getItem('isWebp') === 'true'; 52 | const backgroundDOM = document.getElementById('background'); 53 | const background = new Image(); 54 | background.src = isWebp ? `${imageUrl}${webpSuffix}` : imageUrl; 55 | background.onload = () => { 56 | if (backgroundDOM) { 57 | backgroundDOM.style.cssText = `opacity: 1; background-image: url(${ 58 | background.src 59 | })`; 60 | } 61 | }; 62 | }; 63 | 64 | public getCover = async (position: string) => { 65 | let curId = window.localStorage.cover_id; 66 | if (!curId) { 67 | curId = 0; 68 | } 69 | try { 70 | const res = await homeService.getCover(curId, position); 71 | runInAction(() => { 72 | this.coverUrl = res.data.url; 73 | window.localStorage.setItem('cover_id', res.data._id); 74 | }); 75 | if (this.coverUrl) { 76 | this.loadBgImg(this.coverUrl); 77 | } 78 | } catch (e) { 79 | setToast('获取 Cover 失败'); 80 | } 81 | }; 82 | } 83 | 84 | const homeStore = new HomeStore(); 85 | 86 | export default homeStore; 87 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import layoutsStore from './layoutsStore'; 2 | import articleStore from './articleStore'; 3 | import musicStore from './musicStore'; 4 | import homeStore from './homeStore'; 5 | import cvStore from './cvStore'; 6 | import aboutStore from './aboutStore'; 7 | 8 | const stores = { 9 | layoutsStore, 10 | articleStore, 11 | musicStore, 12 | homeStore, 13 | cvStore, 14 | aboutStore, 15 | }; 16 | 17 | export default stores; -------------------------------------------------------------------------------- /src/stores/layoutsStore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | observable, 3 | runInAction 4 | } from 'mobx'; 5 | 6 | import { 7 | layoutsService 8 | } from '../apis/index.service'; 9 | 10 | import { 11 | IAPlayer, 12 | IGlobalStatus, 13 | } from '../types/layout'; 14 | 15 | import APlayer from 'aplayer'; 16 | 17 | import { 18 | setToast 19 | } from '@tools/tools'; 20 | 21 | class LayoutsStore { 22 | @observable public players: IAPlayer[] = []; 23 | @observable public globalStatus: IGlobalStatus = { 24 | full_site_gray: false, 25 | __v: 0, 26 | _id: '', 27 | }; 28 | 29 | public getPlayers = async () => { 30 | try { 31 | const res = await layoutsService.getPlayers(); 32 | runInAction(() => { 33 | res.data.map(item => { 34 | this.players.push({ 35 | name: item.title, 36 | artist: item.artist, 37 | url: item.music_file_url, 38 | cover: item.cover, 39 | lrc: item.lrc, 40 | }) 41 | }) 42 | const ap = new APlayer({ 43 | container: document.querySelector('#player'), 44 | fixed: true, 45 | lrcType: 1, 46 | audio: layoutsStore!.players, 47 | }); 48 | ap.lrc.show(); 49 | }); 50 | } catch (error) { 51 | setToast('获取播放器失败'); 52 | } 53 | } 54 | 55 | public getGlobalStatus = async () => { 56 | try { 57 | const res = await layoutsService.getGlobalStatus(); 58 | runInAction(() => { 59 | this.globalStatus = res.data; 60 | }); 61 | } catch (e) { 62 | setToast('获取全局状态失败'); 63 | } 64 | }; 65 | } 66 | 67 | const layoutsStore = new LayoutsStore(); 68 | 69 | export default layoutsStore; -------------------------------------------------------------------------------- /src/stores/musicStore.ts: -------------------------------------------------------------------------------- 1 | import { observable, runInAction } from 'mobx'; 2 | 3 | import { musicService } from '../apis/index.service'; 4 | 5 | import { setToast } from '@tools/tools'; 6 | 7 | import { ILiveTours, IFeaturedRecords, IYanceyMusic } from '../types/music'; 8 | 9 | class MusicStore { 10 | @observable public liveTours: ILiveTours[] = []; 11 | @observable public featuredRecords: IFeaturedRecords[] = []; 12 | @observable public yanceyMusic: IYanceyMusic[] = []; 13 | @observable public isLiveToursLoading = false; 14 | @observable public isYanceyMusicLoading = false; 15 | @observable public isFeaturedRecordLoading = false; 16 | 17 | public getLiveTours = async () => { 18 | this.isLiveToursLoading = true; 19 | try { 20 | const res = await musicService.getLiveTours(); 21 | runInAction(() => { 22 | this.liveTours = res.data; 23 | }); 24 | } catch (error) { 25 | setToast('获取演唱会现场图失败'); 26 | } finally { 27 | this.isLiveToursLoading = false; 28 | } 29 | }; 30 | 31 | public getFeaturedRecords = async () => { 32 | this.isFeaturedRecordLoading = true; 33 | try { 34 | const res = await musicService.getFeaturedRecords(); 35 | runInAction(() => { 36 | this.featuredRecords = res.data; 37 | }); 38 | } catch (error) { 39 | setToast('获取精选唱片失败'); 40 | } finally { 41 | this.isFeaturedRecordLoading = false; 42 | } 43 | }; 44 | 45 | public getYanceyMusic = async () => { 46 | this.isYanceyMusicLoading = true; 47 | try { 48 | const res = await musicService.getYanceyMusic(); 49 | runInAction(() => { 50 | this.yanceyMusic = res.data; 51 | }); 52 | } catch (error) { 53 | setToast('获取我的作品失败'); 54 | } finally { 55 | this.isYanceyMusicLoading = false; 56 | } 57 | }; 58 | } 59 | 60 | const musicStore = new MusicStore(); 61 | 62 | export default musicStore; 63 | -------------------------------------------------------------------------------- /src/test/tools.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | formatJSONDate 3 | } from '../tools/tools'; 4 | 5 | test('should get right date', () => { 6 | expect(formatJSONDate('2019-03-10T04:15:40.629Z')).toBe('2019-03-10 12:15:40') 7 | }) -------------------------------------------------------------------------------- /src/tools/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { 2 | AxiosError, 3 | AxiosResponse, 4 | AxiosRequestConfig, 5 | } from 'axios'; 6 | 7 | import baseURL from '../baseUrl'; 8 | 9 | const CancelToken = axios.CancelToken; 10 | 11 | // config timeout 12 | axios.defaults.timeout = 30 * 1000; 13 | 14 | // config cookie 15 | // axios.defaults.withCredentials = true; 16 | 17 | // config request header 18 | axios.defaults.headers.post['Content-Type'] = 'application/json'; 19 | 20 | // config base url 21 | axios.defaults.baseURL = baseURL.prod; 22 | 23 | const pending: any[] = []; 24 | const removePending = (config: any) => { 25 | for (const p in pending) { 26 | if (pending[p].u === config.url + '&' + config.method) { 27 | pending[p].f(); 28 | pending.splice(parseInt(p, 10), 1); 29 | } 30 | } 31 | } 32 | 33 | 34 | // config request interceptors 35 | axios.interceptors.request.use( 36 | (config) => { 37 | removePending(config); 38 | config.cancelToken = new CancelToken((c) => { 39 | pending.push({ 40 | u: config.url + '&' + config.method, 41 | f: c 42 | }); 43 | }); 44 | return config; 45 | }, 46 | (err) => Promise.reject(err), 47 | ); 48 | // config response interceptors 49 | axios.interceptors.response.use( 50 | (response: AxiosResponse) => response, 51 | (error: AxiosError) => { 52 | if (error && error.response) { 53 | switch (error.response.status) { 54 | case 400: 55 | error.message = '400 Bad Request'; 56 | break; 57 | case 401: 58 | error.message = '401 Unauthorized'; 59 | window.location.href = '/login'; 60 | break; 61 | case 403: 62 | error.message = '403 Forbidden'; 63 | break; 64 | case 404: 65 | error.message = '404 Not Found'; 66 | break; 67 | case 500: 68 | error.message = '500 Internal Server Error'; 69 | break; 70 | case 502: 71 | error.message = '502 Bad Gateway'; 72 | break; 73 | case 504: 74 | error.message = '504 Internal Server Error'; 75 | break; 76 | default: 77 | error.message = `Unkown error and the status code is ${error.response.status}`; 78 | } 79 | } else { 80 | error.message = 'Unkown error'; 81 | } 82 | return Promise.reject(error.message); 83 | }); 84 | 85 | // GET 86 | export function GET(url: string, params: object | null, errMsg: string | null): Promise < AxiosResponse > { 87 | return new Promise((resolve, reject) => { 88 | axios 89 | .get(url, { 90 | params, 91 | }) 92 | .then((res) => { 93 | resolve(res); 94 | }) 95 | .catch((err) => { 96 | err = errMsg ? errMsg : err 97 | // 可以定义Toast组件输出err 98 | reject(err); 99 | }); 100 | }); 101 | } 102 | 103 | // POST 104 | export function POST(url: string, params: object | null, config ? : AxiosRequestConfig): Promise < AxiosResponse > { 105 | return new Promise((resolve, reject) => { 106 | axios 107 | .post(url, params, config) 108 | .then( 109 | (res) => { 110 | resolve(res); 111 | }, 112 | (err) => { 113 | reject(err); 114 | }, 115 | ) 116 | .catch((err) => { 117 | reject(err); 118 | }); 119 | }); 120 | } 121 | 122 | // PUT 123 | export function PUT(url: string, params: object | null, errMsg: string | null): Promise < AxiosResponse > { 124 | return new Promise((resolve, reject) => { 125 | axios 126 | .put(url, params) 127 | .then((res) => { 128 | resolve(res); 129 | }) 130 | .catch((err) => { 131 | err = errMsg ? errMsg : err; 132 | reject(err); 133 | }); 134 | }); 135 | } 136 | 137 | // DELETE 138 | export function DELETE(url: string, params: object | null): Promise < AxiosResponse > { 139 | return new Promise((resolve, reject) => { 140 | axios 141 | .delete(url, { 142 | data: params, 143 | }) 144 | .then((res) => { 145 | resolve(res); 146 | }) 147 | .catch((err) => { 148 | reject(err); 149 | }); 150 | }); 151 | } -------------------------------------------------------------------------------- /src/tools/tools.ts: -------------------------------------------------------------------------------- 1 | const months = [ 2 | 'January', 3 | 'February', 4 | 'March', 5 | 'April', 6 | 'May', 7 | 'June', 8 | 'July', 9 | 'August', 10 | 'September', 11 | 'October', 12 | 'November', 13 | 'December', 14 | ]; 15 | 16 | import { toast } from 'react-toastify'; 17 | 18 | // 2018-11-11T07:53:15.403Z => 2018-11-11 15:53:15 19 | export const formatJSONDate = (jsonDate: string): string => { 20 | return new Date(+new Date(new Date(jsonDate).toJSON()) + 8 * 3600 * 1000) 21 | .toISOString() 22 | .replace(/T/g, ' ') 23 | .replace(/\.[\d]{3}Z/, ''); 24 | }; 25 | 26 | // 2018-11-11 15:53:15 => November 11, 2018 27 | export const formatCommonDate = (date: string): string => { 28 | const dataList = date.split(' ')[0].split('-'); 29 | return `${months[parseInt(dataList[1], 10) - 1]} ${dataList[2]}, ${ 30 | dataList[0] 31 | }`; 32 | }; 33 | 34 | export const monthToEN = (monthNum: number) => { 35 | const monthList = [ 36 | 'Jan', 37 | 'Feb', 38 | 'Mar', 39 | 'Apr', 40 | 'May', 41 | 'Jun', 42 | 'Jul', 43 | 'Aug', 44 | 'Sep', 45 | 'Oct', 46 | 'Nov', 47 | 'Dec', 48 | ]; 49 | return monthList[monthNum - 1]; 50 | }; 51 | 52 | export const sortBy = (parent: string, child: string) => (a: any, b: any) => 53 | a[parent][child] < b[parent][child] 54 | ? 1 55 | : a[parent][child] > b[parent][child] 56 | ? -1 57 | : 0; 58 | 59 | export const memoized = (fn: any) => { 60 | const cache = {}; 61 | return (arg: any) => cache[arg] || (cache[arg] = fn(arg)); 62 | }; 63 | 64 | export const once = (fn: any) => { 65 | let done = false; 66 | // tslint:disable-next-line:only-arrow-functions 67 | return function() { 68 | return done ? false : ((done = true), fn.apply(null, arguments)); 69 | }; 70 | }; 71 | 72 | export const checkWebp = () => { 73 | return ( 74 | document 75 | .createElement('canvas') 76 | .toDataURL('image/webp') 77 | .indexOf('data:image/webp') === 0 78 | ); 79 | }; 80 | 81 | export const judgeLanguage = () => { 82 | // ja-JP zh-CN en-US 83 | return navigator.language; 84 | }; 85 | 86 | export const judgeClient = () => { 87 | const userAgent = navigator.userAgent; 88 | let client = ''; 89 | if (/(MicroMessenger)/i.test(userAgent)) { 90 | client = 'Wechat'; 91 | } else if (/(iPhone|iPad|iPod|iOS)/i.test(userAgent)) { 92 | client = 'iOS'; 93 | } else if (/(Android)/i.test(userAgent)) { 94 | client = 'Android'; 95 | } else if (/(Macintosh)/i.test(userAgent)) { 96 | client = 'Mac'; 97 | } else if (/(Linux)/i.test(userAgent)) { 98 | client = 'Linux'; 99 | } else if (/(Windows)/i.test(userAgent)) { 100 | client = 'Windows'; 101 | } 102 | return client; 103 | }; 104 | 105 | export const initLivere = () => { 106 | // tslint:disable-next-line:prefer-const 107 | let LivereTower; 108 | // tslint:disable-next-line:only-arrow-functions 109 | (function(d, s) { 110 | // tslint:disable-next-line:one-variable-per-declaration 111 | let j, 112 | // tslint:disable-next-line:prefer-const 113 | e = d.getElementsByTagName(s)[0]; 114 | 115 | if (typeof LivereTower === 'function') { 116 | return; 117 | } 118 | 119 | j = d.createElement(s); 120 | j.src = 'https://cdn-city.livere.com/js/embed.dist.js'; 121 | j.async = true; 122 | if (e.parentNode) { 123 | (e.parentNode as HTMLDivElement).insertBefore(j, e); 124 | } 125 | })(document, 'script'); 126 | }; 127 | 128 | export const setToast = (text: string) => { 129 | return toast.error(`💔 ${text}`, { 130 | position: 'top-center', 131 | autoClose: 3000, 132 | hideProgressBar: true, 133 | closeOnClick: true, 134 | className: 'toasting', 135 | }); 136 | }; 137 | 138 | export const noop = () => {}; 139 | 140 | export const devToolsWarning = () => { 141 | document.addEventListener('DOMContentLoaded', () => { 142 | if (window.console || 'console' in window) { 143 | // tslint:disable-next-line:no-console 144 | console.log(` 145 | __ __ _ _ _____ ________ __ ____ _ ____ _____ 146 | \\ \\ / //\\ | \\ | |/ ____| ____\\ \\ / / | _ \\| | / __ \\ / ____| 147 | \\ \\_/ // \\ | \\| | | | |__ \\ \\_/ / | |_) | | | | | | | __ 148 | \\ // /\\ \\ | . \` | | | __| \\ / | _ <| | | | | | | |_ | 149 | | |/ ____ \\| |\\ | |____| |____ | | | |_) | |___| |__| | |__| | 150 | |_/_/ \\_\\_| \\_|\\_____|______| |_| |____/|______\\____/ \\_____| 151 | `); 152 | } 153 | }); 154 | }; 155 | -------------------------------------------------------------------------------- /src/types/about.ts: -------------------------------------------------------------------------------- 1 | export interface IAbout { 2 | cover: string; 3 | introduction: string; 4 | release_date: string; 5 | title: string; 6 | __v: number; 7 | _id: string; 8 | } 9 | export interface AboutStoreType { 10 | abouts: IAbout[]; 11 | getAbouts: () => void; 12 | } 13 | 14 | export interface IAboutProps { 15 | aboutStore?: AboutStoreType; 16 | } 17 | -------------------------------------------------------------------------------- /src/types/apps.ts: -------------------------------------------------------------------------------- 1 | export interface IApps { 2 | systemType: string; 3 | } -------------------------------------------------------------------------------- /src/types/article.ts: -------------------------------------------------------------------------------- 1 | export interface ILike { 2 | like_number: number; 3 | liked: boolean; 4 | } 5 | 6 | export interface IIncreasePV { 7 | n: number; 8 | nModified: number; 9 | ok: number; 10 | } 11 | 12 | export interface IArchiveMonth { 13 | day: number; 14 | id: string; 15 | pv_count: number; 16 | title: string; 17 | } 18 | 19 | export interface IHeaderState { 20 | isTop: boolean; 21 | } 22 | export interface ArticleStoreType { 23 | posts: IArticleDetail[]; 24 | hots: IArticleDetail[]; 25 | tags: string[]; 26 | archives: IArchive[]; 27 | total: number; 28 | showSearch: boolean; 29 | isLiked: boolean; 30 | likeNum: number; 31 | curIp: string; 32 | isDetailLoading: false; 33 | isSummaryLoading: false; 34 | isLinkCardLoading: false; 35 | detail: IDetail; 36 | toggleShowSearch: () => void; 37 | onPageChange: () => void; 38 | onSearchChange: (e: any) => void; 39 | getPostsByPage: (page: number) => void; 40 | getPostsByTitle: (title: string) => void; 41 | getAllTags: () => void; 42 | getPostsByTag: (tag?: string) => void; 43 | getHots: () => void; 44 | getArchives: () => void; 45 | getPostById: (id: string) => void; 46 | handleLikes: () => void; 47 | getLikes: (id: string, ip: string) => void; 48 | getIp: () => void; 49 | increasePV: (id: string) => void; 50 | } 51 | 52 | export interface IArticleProps { 53 | articleStore?: ArticleStoreType; 54 | location?: any; 55 | } 56 | 57 | export interface IArticleDetail { 58 | _id: string; 59 | header_cover: string; 60 | title: string; 61 | summary: string; 62 | content: string; 63 | publish_date: string; 64 | last_modified_date: string; 65 | tags: string[]; 66 | like_count: string[]; 67 | pv_count: number; 68 | } 69 | 70 | export interface IArchive { 71 | _id: { 72 | year: number; 73 | }; 74 | data: { 75 | month: number; 76 | data: IArchiveMonth[]; 77 | }; 78 | } 79 | 80 | export interface IPrevNext { 81 | header_cover: string; 82 | id: string; 83 | title: string; 84 | } 85 | 86 | export interface IDetail { 87 | curArticle: IArticleDetail; 88 | nextArticle: IPrevNext; 89 | previousArticle: IPrevNext; 90 | } 91 | -------------------------------------------------------------------------------- /src/types/common.ts: -------------------------------------------------------------------------------- 1 | export interface ITitleProps { 2 | title: string; 3 | } -------------------------------------------------------------------------------- /src/types/cv.ts: -------------------------------------------------------------------------------- 1 | export interface IWorkExperience { 2 | enterprise_name: string; 3 | in_service: string[]; 4 | position: string; 5 | work_content: string; 6 | work_technology_stack: string[]; 7 | __v: number; 8 | _id: string; 9 | } 10 | 11 | export interface IProgramExperience { 12 | program_content: string; 13 | program_name: string; 14 | program_technology_stack: string[]; 15 | program_url: string; 16 | __v: number; 17 | _id: string; 18 | } 19 | 20 | export interface ICardProps { 21 | type: string; 22 | name: string; 23 | position: string; 24 | inService: string[]; 25 | programLink: string; 26 | detail: string; 27 | techStack: string[]; 28 | } 29 | export interface CVStoreType { 30 | user: IUser; 31 | workExperience: IWorkExperience[]; 32 | programExperience: IProgramExperience[]; 33 | getUser: () => void; 34 | getWorkExperience: () => void; 35 | getProgramExperience: () => void; 36 | } 37 | 38 | export interface ICVProps { 39 | cvStore?: CVStoreType; 40 | } 41 | 42 | export interface IUser { 43 | avatar: string; 44 | city: string; 45 | position: string; 46 | self_introduction: string; 47 | user_name: string; 48 | __v?: number; 49 | _id?: string; 50 | } 51 | -------------------------------------------------------------------------------- /src/types/home.ts: -------------------------------------------------------------------------------- 1 | import { ArticleStoreType } from './article'; 2 | 3 | export interface IAnnouncement { 4 | __v: number; 5 | _id: string; 6 | content: string; 7 | upload_date: string; 8 | } 9 | 10 | export interface IMotto { 11 | __v: number; 12 | _id: string; 13 | content: string; 14 | upload_date: string; 15 | } 16 | 17 | export interface IProject { 18 | introduction: string; 19 | poster: string; 20 | title: string; 21 | upload_date: string; 22 | url: string; 23 | __v: number; 24 | _id: string; 25 | } 26 | 27 | export interface ICover { 28 | name: string; 29 | show: boolean; 30 | upload_date: string; 31 | url: string; 32 | __v: number; 33 | _id: string; 34 | } 35 | 36 | export interface IHomeStore { 37 | announcement: string; 38 | motto: string; 39 | projects: IProject[]; 40 | coverUrl: string; 41 | getAnnouncement: () => void; 42 | getMotto: () => void; 43 | getProject: () => void; 44 | loadBgImg: () => void; 45 | getCover: (position?: string) => void; 46 | } 47 | 48 | export interface IHomeProps { 49 | homeStore?: IHomeStore; 50 | articleStore?: ArticleStoreType; 51 | } 52 | -------------------------------------------------------------------------------- /src/types/layout.ts: -------------------------------------------------------------------------------- 1 | export interface IPlayer { 2 | __v: number; 3 | _id: string; 4 | title: string; 5 | artist: string; 6 | show: boolean; 7 | music_file_url: string; 8 | lrc: string; 9 | cover: string; 10 | upload_date: string; 11 | } 12 | 13 | export interface IAPlayer { 14 | name: string; 15 | artist: string; 16 | url: string; 17 | cover: string; 18 | lrc: string; 19 | } 20 | 21 | export interface IGlobalStatus { 22 | full_site_gray: boolean; 23 | __v: number; 24 | _id: string; 25 | } 26 | interface LayoutsStoreType { 27 | players: IPlayer[]; 28 | globalStatus: IGlobalStatus; 29 | getPlayers: () => void; 30 | getGlobalStatus: () => void; 31 | } 32 | 33 | export interface ILayoutsProps { 34 | layoutsStore?: LayoutsStoreType; 35 | } 36 | -------------------------------------------------------------------------------- /src/types/music.ts: -------------------------------------------------------------------------------- 1 | import { ArticleStoreType } from './article'; 2 | 3 | export interface ILiveTours { 4 | poster: string; 5 | title: string; 6 | upload_date: string; 7 | __v: number; 8 | _id: string; 9 | } 10 | 11 | export interface IFeaturedRecords { 12 | album_name: string; 13 | artist: string; 14 | buy_url: string; 15 | cover: string; 16 | release_date: string; 17 | __v: number; 18 | _id: string; 19 | } 20 | 21 | export interface IYanceyMusic { 22 | title: string; 23 | soundCloud_url: string; 24 | cover: string; 25 | release_date: string; 26 | __v: number; 27 | _id: string; 28 | } 29 | 30 | export interface ICardProps { 31 | type: string; 32 | url: string; 33 | title: string; 34 | date: string; 35 | cover: string; 36 | } 37 | 38 | interface MusicStoreType { 39 | liveTours: ILiveTours[]; 40 | featuredRecords: IFeaturedRecords[]; 41 | yanceyMusic: IYanceyMusic[]; 42 | isLiveToursLoading: boolean; 43 | isYanceyMusicLoading: boolean; 44 | isFeaturedRecordLoading: boolean; 45 | getLiveTours: () => void; 46 | getFeaturedRecords: () => void; 47 | getYanceyMusic: () => void; 48 | } 49 | 50 | export interface IMusicProps { 51 | musicStore?: MusicStoreType; 52 | articleStore?: ArticleStoreType; 53 | } 54 | -------------------------------------------------------------------------------- /src/types/presistent.ts: -------------------------------------------------------------------------------- 1 | export default interface IPersistent { 2 | isWebp?: boolean; 3 | } -------------------------------------------------------------------------------- /src/types/widget.ts: -------------------------------------------------------------------------------- 1 | export interface ICookieConfirmDialogStates { 2 | flag: boolean; 3 | } 4 | 5 | export interface IDownloadAppDialogStates { 6 | flag: boolean; 7 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "es5", 6 | "lib": ["es6", "dom"], 7 | "baseUrl": ".", 8 | "paths": { 9 | "@constants/*": ["./src/constants/*"], 10 | "@types/*": ["./src/types/*"], 11 | "@components/*": ["./src/components/*"], 12 | "@tools/*": ["./src/tools/*"] 13 | }, 14 | "sourceMap": true, 15 | "allowJs": true, 16 | "jsx": "preserve", 17 | "moduleResolution": "node", 18 | "rootDir": "src", 19 | "forceConsistentCasingInFileNames": true, 20 | "noImplicitReturns": true, 21 | "noImplicitThis": false, 22 | "noImplicitAny": false, 23 | "importHelpers": true, 24 | "strictNullChecks": true, 25 | "suppressImplicitAnyIndexErrors": true, 26 | "noUnusedLocals": true, 27 | "skipLibCheck": false, 28 | "esModuleInterop": true, 29 | "allowSyntheticDefaultImports": true, 30 | "strict": true, 31 | "resolveJsonModule": true, 32 | "isolatedModules": true, 33 | "noEmit": true, 34 | "experimentalDecorators": true 35 | }, 36 | "include": ["src"], 37 | "exclude": [ 38 | "build", 39 | "scripts", 40 | "node_modules", 41 | "acceptance-tests", 42 | "webpack", 43 | "jest", 44 | "src/setupTests.ts", 45 | "config" 46 | ], 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": ["tslint:recommended", "tslint-config-prettier"], 4 | "linterOptions": { 5 | "exclude": [ 6 | "config/**/*.js", 7 | "config/*.js1", 8 | "node_modules/**/*.ts", 9 | "coverage/lcov-report/*.js" 10 | ] 11 | }, 12 | "rules": { 13 | "indent": [true, "spaces", 2], 14 | "quotemark": [true, "single"], 15 | "interface-name": false, 16 | "ordered-imports": false, 17 | "no-consecutive-blank-lines": false, 18 | "object-literal-sort-keys": false 19 | } 20 | } 21 | --------------------------------------------------------------------------------