├── .eslintignore ├── .npmrc ├── .commitlintrc.yml ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── logo ├── logo.jpg ├── favicon.ico ├── logo192.png ├── logo512.png ├── logo-with-text.png └── README.md ├── src ├── shared │ ├── history.ts │ ├── globalStyles.ts │ ├── utils.ts │ └── constants.ts ├── containers │ ├── Home │ │ ├── Motto │ │ │ ├── types.ts │ │ │ ├── typeDefs.ts │ │ │ └── components │ │ │ │ └── MottoModal.tsx │ │ ├── Cover │ │ │ ├── types.ts │ │ │ └── typeDefs.ts │ │ ├── OpenSource │ │ │ ├── types.ts │ │ │ ├── typeDefs.ts │ │ │ └── OpenSource.tsx │ │ └── Announcement │ │ │ ├── types.ts │ │ │ ├── typeDefs.ts │ │ │ └── components │ │ │ └── AnnouncementModal.tsx │ ├── Music │ │ ├── LiveTour │ │ │ ├── types.ts │ │ │ ├── typeDefs.ts │ │ │ └── LiveTour.tsx │ │ ├── YanceyMusic │ │ │ ├── types.ts │ │ │ ├── typeDefs.ts │ │ │ └── YanceyMusic.tsx │ │ ├── BestAlbum │ │ │ ├── types.ts │ │ │ ├── typeDefs.ts │ │ │ └── BestAlbum.tsx │ │ └── Player │ │ │ ├── styles.ts │ │ │ ├── types.ts │ │ │ └── typeDefs.ts │ ├── Settings │ │ ├── Security │ │ │ ├── components │ │ │ │ ├── ChangePassword │ │ │ │ │ └── changePassword.module.scss │ │ │ │ ├── TwoFactors │ │ │ │ │ ├── twoFactors.module.scss │ │ │ │ │ └── TwoFactors.tsx │ │ │ │ ├── SecurtyIntro │ │ │ │ │ └── SecurtyIntro.tsx │ │ │ │ ├── RecoveryCodes │ │ │ │ │ ├── recoveryCode.module.scss │ │ │ │ │ └── RecoveryCodes.tsx │ │ │ │ └── TOTP │ │ │ │ │ └── totp.module.scss │ │ │ ├── typeDefs.ts │ │ │ └── Security.tsx │ │ ├── Profile │ │ │ ├── typeDefs.tsx │ │ │ └── styles.ts │ │ ├── components │ │ │ ├── SettingWrapper │ │ │ │ └── SettingWrapper.tsx │ │ │ ├── SettingItemWrapper │ │ │ │ ├── settingItemWrapper.module.scss │ │ │ │ └── SettingItemWrapper.tsx │ │ │ └── SettingsHeader │ │ │ │ └── SettingsHeader.tsx │ │ ├── GlobalConfig │ │ │ ├── types.ts │ │ │ ├── typeDefs.ts │ │ │ ├── styles.ts │ │ │ ├── components │ │ │ │ ├── GrayTheme.tsx │ │ │ │ ├── CVPicker.tsx │ │ │ │ └── ReleasePicker.tsx │ │ │ └── GlobalConfig.tsx │ │ └── Account │ │ │ ├── typeDefs.tsx │ │ │ ├── styles.ts │ │ │ ├── components │ │ │ ├── UpdateEmail.tsx │ │ │ ├── UpdateUserName.tsx │ │ │ └── DeleteAccount.tsx │ │ │ └── Account.tsx │ ├── Agenda │ │ ├── tools.ts │ │ ├── types.ts │ │ ├── components │ │ │ ├── CustomOpenButton.tsx │ │ │ ├── CustomTodayButton.tsx │ │ │ ├── CustomNavigationButton.tsx │ │ │ ├── ExternalViewSwitcher.tsx │ │ │ └── Schedule.tsx │ │ ├── styles.ts │ │ ├── typeDefs.ts │ │ └── Agenda.tsx │ ├── Events │ │ ├── tools.ts │ │ ├── types.ts │ │ ├── components │ │ │ ├── CustomOpenButton.tsx │ │ │ ├── CustomTodayButton.tsx │ │ │ ├── CustomNavigationButton.tsx │ │ │ ├── ExternalViewSwitcher.tsx │ │ │ └── Schedule.tsx │ │ ├── styles.ts │ │ ├── typeDefs.ts │ │ └── Agenda.tsx │ ├── DashBoard │ │ ├── styles.ts │ │ ├── types.ts │ │ ├── typeDefs.ts │ │ ├── components │ │ │ ├── StatusCardSkeleton.tsx │ │ │ ├── CPUChart.tsx │ │ │ ├── TagClouds.tsx │ │ │ ├── DiskChart.tsx │ │ │ ├── NetWorkChart.tsx │ │ │ ├── UsageStatusSkeleton.tsx │ │ │ ├── PostRankListSkeleton.tsx │ │ │ ├── ToggleChart.tsx │ │ │ ├── BandwagonServiceStatus.tsx │ │ │ ├── PostStatistics.tsx │ │ │ ├── PostRankList.tsx │ │ │ └── StatusCard.tsx │ │ └── chartjsConfig.ts │ └── Post │ │ ├── editors │ │ ├── editorEmbededPlugin.ts │ │ ├── editorIO.ts │ │ └── enhanceEditor.ts │ │ ├── components │ │ └── UploaderModal.tsx │ │ ├── algolia │ │ └── algoliaSearch.ts │ │ ├── styles.ts │ │ ├── types.ts │ │ ├── typeDefs.ts │ │ └── PostList.tsx ├── setupTests.ts ├── pages │ ├── Layouts │ │ ├── components │ │ │ ├── Mains │ │ │ │ ├── styles.ts │ │ │ │ └── Mains.tsx │ │ │ ├── Header │ │ │ │ └── styles.ts │ │ │ ├── Drawer │ │ │ │ └── components │ │ │ │ │ ├── ChildItem.tsx │ │ │ │ │ └── ParentItem.tsx │ │ │ └── Footer │ │ │ │ └── Footer.tsx │ │ ├── styles.ts │ │ └── Layouts.tsx │ └── Auth │ │ ├── utils.ts │ │ ├── typeDefs.ts │ │ └── Auth.module.scss ├── components │ ├── Uploader │ │ ├── types.ts │ │ └── styles.ts │ ├── Transition │ │ └── Transition.tsx │ ├── SkeletonIterator │ │ └── SkeletonIterator.tsx │ ├── NotFound │ │ ├── styles.ts │ │ └── NotFound.tsx │ ├── Loading │ │ ├── Loading.tsx │ │ └── TwitterLoading.tsx │ ├── ConfirmModal │ │ └── ConfirmModal.tsx │ ├── Toast │ │ └── Toast.tsx │ ├── ImagePopup │ │ └── ImagePopup.tsx │ ├── ConfirmPoper │ │ └── ConfirmPoper.tsx │ ├── TableWrapper │ │ └── TableWrapper.tsx │ └── Move │ │ └── Move.tsx ├── graphql │ ├── graphqlFragment.ts │ └── apolloClient.ts ├── assets │ └── global.scss ├── hooks │ ├── useOpenModal.ts │ └── useScript.ts ├── reportWebVitals.ts ├── react-app-env.d.ts ├── index.tsx └── service-worker.ts ├── .huskyrc.json ├── .prettierrc.json ├── .markdownlintrc ├── CONTRIBUTING.md ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── SECURITY.md ├── .gitignore ├── .babelrc ├── .env.production.example ├── .github ├── dependabot.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── github-actions.yml │ └── codeql-analysis.yml ├── tsconfig.json ├── .env.development └── LICENSE /.eslintignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.yarnpkg.com/ -------------------------------------------------------------------------------- /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - '@commitlint/config-conventional' 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /logo/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/blog-cms-v2/HEAD/logo/logo.jpg -------------------------------------------------------------------------------- /logo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/blog-cms-v2/HEAD/logo/favicon.ico -------------------------------------------------------------------------------- /logo/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/blog-cms-v2/HEAD/logo/logo192.png -------------------------------------------------------------------------------- /logo/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/blog-cms-v2/HEAD/logo/logo512.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/blog-cms-v2/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/blog-cms-v2/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/blog-cms-v2/HEAD/public/logo512.png -------------------------------------------------------------------------------- /logo/logo-with-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/blog-cms-v2/HEAD/logo/logo-with-text.png -------------------------------------------------------------------------------- /src/shared/history.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history' 2 | 3 | export default createBrowserHistory() 4 | -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 4 | "pre-commit": "lint-staged" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "bracketSpacing": true, 5 | "proseWrap": "preserve", 6 | "semi": false, 7 | "printWidth": 80 8 | } 9 | -------------------------------------------------------------------------------- /.markdownlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "MD033": { 3 | "allowed_elements": [ 4 | "div", 5 | "img", 6 | "a", 7 | ] 8 | }, 9 | "MD013": false 10 | } -------------------------------------------------------------------------------- /src/containers/Home/Motto/types.ts: -------------------------------------------------------------------------------- 1 | export interface IMotto { 2 | _id: string 3 | weight: number 4 | content: string 5 | createdAt: string 6 | updatedAt: string 7 | } 8 | 9 | export interface Query { 10 | getMottos: IMotto[] 11 | } 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Blog CMS v2 2 | 3 | Want to contribute to the Blog CMS v2? 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. 6 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom' 6 | -------------------------------------------------------------------------------- /src/pages/Layouts/components/Mains/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core/styles' 2 | 3 | const useStyles = makeStyles({ 4 | main: { 5 | display: 'flex', 6 | flex: 1, 7 | padding: '0 24px 24px', 8 | }, 9 | }) 10 | 11 | export default useStyles 12 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the Lighthouse project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Yancey Leo 7 | Blog Environment Group 8 | -------------------------------------------------------------------------------- /src/containers/Music/LiveTour/types.ts: -------------------------------------------------------------------------------- 1 | export interface ILiveTour { 2 | _id: string 3 | title: string 4 | showTime: string 5 | posterUrl: string 6 | createdAt: string 7 | updatedAt: string 8 | } 9 | 10 | export interface Query { 11 | getLiveTours: ILiveTour[] 12 | } 13 | -------------------------------------------------------------------------------- /src/containers/Home/Cover/types.ts: -------------------------------------------------------------------------------- 1 | export interface ICover { 2 | _id: string 3 | title: string 4 | coverUrl: string 5 | isPublic: boolean 6 | weight: number 7 | createdAt: string 8 | updatedAt: string 9 | } 10 | 11 | export interface Query { 12 | getCovers: ICover[] 13 | } 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | [Blog Environment Group](https://github.com/Yancey-Blog) has adopted a Code of Conduct that we expect project participants to adhere to. 4 | Please [read the full text](https://code.fb.com/codeofconduct/) so that you can understand what actions will and will not be tolerated. 5 | -------------------------------------------------------------------------------- /src/containers/Home/OpenSource/types.ts: -------------------------------------------------------------------------------- 1 | export interface IOpenSource { 2 | _id: string 3 | title: string 4 | description: string 5 | url: string 6 | posterUrl: string 7 | createdAt: string 8 | updatedAt: string 9 | } 10 | 11 | export interface Query { 12 | getOpenSources: IOpenSource[] 13 | } 14 | -------------------------------------------------------------------------------- /src/containers/Settings/Security/components/ChangePassword/changePassword.module.scss: -------------------------------------------------------------------------------- 1 | .input { 2 | display: block; 3 | margin-bottom: 14px; 4 | input { 5 | width: 450px; 6 | } 7 | } 8 | 9 | .tip { 10 | margin-top: 20px; 11 | margin-bottom: 24px; 12 | font-size: 12px; 13 | color: #5f6368; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Uploader/types.ts: -------------------------------------------------------------------------------- 1 | export interface UploaderResponse { 2 | name: string 3 | url: string 4 | } 5 | 6 | export interface Props { 7 | type?: 'avatar' | 'simple' 8 | variant?: 'elevation' | 'outlined' | undefined 9 | accept?: string 10 | defaultFile?: string 11 | needMarginLeft?: boolean 12 | className?: any 13 | onChange: Function 14 | } 15 | -------------------------------------------------------------------------------- /src/containers/Settings/Profile/typeDefs.tsx: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | export const UPDATE_USER = gql` 4 | mutation UpdateUser($input: UpdateUserInput!) { 5 | updateUser(input: $input) { 6 | _id 7 | name 8 | location 9 | organization 10 | website 11 | bio 12 | avatarUrl 13 | } 14 | } 15 | ` 16 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | If you discover a security issue in Blog CMS v2, please report it by sending an email to [developer@yanceyleo.com](mailto:developer@yanceyleo.com). 4 | 5 | This will allow us to assess the risk, and make a fix available before we add a bug report to the GitHub repository. 6 | 7 | Thanks for helping make Blog CMS v2 safe for everyone! 8 | -------------------------------------------------------------------------------- /src/containers/Music/YanceyMusic/types.ts: -------------------------------------------------------------------------------- 1 | export interface IYanceyMusic { 2 | readonly _id: string 3 | readonly title: string 4 | readonly soundCloudUrl: string 5 | readonly posterUrl: string 6 | readonly releaseDate: string 7 | readonly createdAt: string 8 | readonly updatedAt: string 9 | } 10 | 11 | export interface Query { 12 | getYanceyMusic: IYanceyMusic[] 13 | } 14 | -------------------------------------------------------------------------------- /src/containers/Music/BestAlbum/types.ts: -------------------------------------------------------------------------------- 1 | export interface IBestAlbum { 2 | readonly _id: string 3 | readonly title: string 4 | readonly artist: string 5 | readonly coverUrl: string 6 | readonly mvUrl: string 7 | readonly releaseDate: string 8 | readonly createdAt: string 9 | readonly updatedAt: string 10 | } 11 | 12 | export interface Query { 13 | getBestAlbums: IBestAlbum[] 14 | } 15 | -------------------------------------------------------------------------------- /src/containers/Music/Player/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 2 | 3 | const useStyles = makeStyles((theme: Theme) => 4 | createStyles({ 5 | lrcTxt: { 6 | margin: 0, 7 | padding: '16px', 8 | fontFamily: 'inherit', 9 | }, 10 | 11 | btnUploaderGroup: { 12 | margin: '40px 0', 13 | }, 14 | }), 15 | ) 16 | 17 | export default useStyles 18 | -------------------------------------------------------------------------------- /src/graphql/graphqlFragment.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | export const BATCH_DELETE_FRAGMENT = gql` 4 | fragment BatchDeleteFragment on BatchDeleteModel { 5 | n 6 | ok 7 | deletedCount 8 | ids 9 | } 10 | ` 11 | 12 | export const BATCH_UPDATE_FRAGMENT = gql` 13 | fragment BatchUpdateFragment on BatchUpdateModel { 14 | n 15 | ok 16 | nModified 17 | ids 18 | } 19 | ` 20 | -------------------------------------------------------------------------------- /src/assets/global.scss: -------------------------------------------------------------------------------- 1 | figure, 2 | ul, 3 | li, 4 | p { 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | a { 10 | color: #1a73e8; 11 | text-decoration: none; 12 | &:hover { 13 | text-decoration: underline; 14 | } 15 | } 16 | 17 | .tui-emebed-icon { 18 | position: relative; 19 | top: -2px; 20 | font-size: 14px !important; 21 | font-weight: bold !important; 22 | background: none !important; 23 | color: #000 !important; 24 | } 25 | -------------------------------------------------------------------------------- /src/containers/Music/Player/types.ts: -------------------------------------------------------------------------------- 1 | export interface IPlayer { 2 | readonly _id: string 3 | readonly title: string 4 | readonly artist: string 5 | readonly lrc: string 6 | readonly coverUrl: string 7 | readonly musicFileUrl: string 8 | readonly isPublic: boolean 9 | readonly weight: number 10 | readonly createdAt: Date 11 | readonly updatedAt: Date 12 | } 13 | 14 | export interface Query { 15 | getPlayers: IPlayer[] 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /dist 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | .env.production 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | .eslintcache -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app"], 3 | "plugins": [ 4 | [ 5 | "babel-plugin-transform-imports", 6 | { 7 | "@material-ui/core": { 8 | "transform": "@material-ui/core/esm/${member}", 9 | "preventFullImport": true 10 | }, 11 | "@material-ui/icons": { 12 | "transform": "@material-ui/icons/esm/${member}", 13 | "preventFullImport": true 14 | } 15 | } 16 | ] 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Transition/Transition.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, Ref, ReactElement } from 'react' 2 | import { TransitionProps } from '@material-ui/core/transitions' 3 | import { Slide } from '@material-ui/core' 4 | 5 | const Transition = forwardRef(function Transition( 6 | props: TransitionProps & { children?: ReactElement }, 7 | ref: Ref, 8 | ) { 9 | return 10 | }) 11 | 12 | export default Transition 13 | -------------------------------------------------------------------------------- /src/containers/Agenda/tools.ts: -------------------------------------------------------------------------------- 1 | import { Dict, IAgenda } from './types' 2 | 3 | export const formatChangedData = (o: Dict) => { 4 | const id = Object.keys(o)[0] 5 | return { 6 | id, 7 | ...o[id], 8 | } 9 | } 10 | 11 | export const dateStringToDate = (agendaList: IAgenda[]) => 12 | agendaList.map((agenda) => ({ 13 | ...agenda, 14 | id: agenda._id, 15 | startDate: new Date(agenda.startDate), 16 | endDate: new Date(agenda.endDate), 17 | })) 18 | -------------------------------------------------------------------------------- /src/containers/Events/tools.ts: -------------------------------------------------------------------------------- 1 | import { Dict, IAgenda } from './types' 2 | 3 | export const formatChangedData = (o: Dict) => { 4 | const id = Object.keys(o)[0] 5 | return { 6 | id, 7 | ...o[id], 8 | } 9 | } 10 | 11 | export const dateStringToDate = (agendaList: IAgenda[]) => 12 | agendaList.map((agenda) => ({ 13 | ...agenda, 14 | id: agenda._id, 15 | startDate: new Date(agenda.startDate), 16 | endDate: new Date(agenda.endDate), 17 | })) 18 | -------------------------------------------------------------------------------- /src/hooks/useOpenModal.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export interface Open { 4 | isOpen: boolean 5 | id?: string 6 | } 7 | 8 | const useOpenModal = () => { 9 | const [open, setOpen] = useState({ isOpen: false }) 10 | 11 | const handleOpen = (id?: string) => { 12 | const params: Open = { isOpen: !open.isOpen } 13 | params.id = id ? id : '' 14 | setOpen(params) 15 | } 16 | 17 | return { open, handleOpen } 18 | } 19 | 20 | export default useOpenModal 21 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals' 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry) 7 | getFID(onPerfEntry) 8 | getFCP(onPerfEntry) 9 | getLCP(onPerfEntry) 10 | getTTFB(onPerfEntry) 11 | }) 12 | } 13 | } 14 | 15 | export default reportWebVitals 16 | -------------------------------------------------------------------------------- /.env.production.example: -------------------------------------------------------------------------------- 1 | # IMPORTANT 2 | # Create the `.env.production` file at first 3 | 4 | REACT_APP_BEG_SERVICE_DOMAIN= 5 | REACT_APP_UPLOADER_SERVICE_DOMAIN= 6 | REACT_APP_ALGOLIA_APPLICATION_ID= 7 | REACT_APP_ALGOLIA_ADMIN_API_KEY= 8 | REACT_APP_ALGOLIA_SEARCH_INDEX= 9 | REACT_APP_RECAPTCHA_KEY= 10 | -------------------------------------------------------------------------------- /src/containers/DashBoard/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 2 | 3 | const useStyles = makeStyles((theme: Theme) => 4 | createStyles({ 5 | dashboradWrapper: { 6 | width: '100%', 7 | }, 8 | 9 | group: { 10 | display: 'grid', 11 | gridTemplateColumns: '2fr 1fr', 12 | gridColumnGap: 24, 13 | gridTemplateRows: '375px 375px 375px', 14 | gridRowGap: 24, 15 | marginBottom: 24, 16 | }, 17 | }), 18 | ) 19 | 20 | export default useStyles 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' 9 | directory: '/' 10 | schedule: 11 | interval: 'weekly' 12 | target-branch: "develop" 13 | commit-message: 14 | prefix: 'deps' 15 | -------------------------------------------------------------------------------- /src/components/SkeletonIterator/SkeletonIterator.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ComponentType } from 'react' 2 | import { randomSeries } from 'yancey-js-util' 3 | 4 | interface Props { 5 | count: number 6 | skeletonComponent: ComponentType 7 | } 8 | 9 | const SkeletonIterator: FC = ({ 10 | count, 11 | skeletonComponent: Skeleton, 12 | }) => { 13 | return ( 14 | <> 15 | {Array.from({ length: count }, () => randomSeries(6)).map((val) => ( 16 | 17 | ))} 18 | 19 | ) 20 | } 21 | 22 | export default SkeletonIterator 23 | -------------------------------------------------------------------------------- /src/containers/Settings/components/SettingWrapper/SettingWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 3 | 4 | const useStyles = makeStyles((theme: Theme) => 5 | createStyles({ 6 | settingWrapper: { 7 | display: 'block', 8 | width: '100%', 9 | }, 10 | }), 11 | ) 12 | 13 | const SettingWrapper: FC = ({ children }) => { 14 | const classes = useStyles() 15 | 16 | return
{children}
17 | } 18 | 19 | export default SettingWrapper 20 | -------------------------------------------------------------------------------- /src/containers/Settings/GlobalConfig/types.ts: -------------------------------------------------------------------------------- 1 | import { IPostItem } from 'src/containers/Post/types' 2 | 3 | export interface PostFilterProps { 4 | id: string 5 | isFetching: boolean 6 | isSubmitting: boolean 7 | fetchPosts: Function 8 | updateGlobalSettingById: Function 9 | posts: IPostItem[] 10 | } 11 | 12 | export interface IGlobalSetting { 13 | _id: string 14 | releasePostId: string 15 | cvPostId: string 16 | isGrayTheme: boolean 17 | createdAt: string 18 | updatedAt: string 19 | } 20 | 21 | export interface Query { 22 | getGlobalSetting: IGlobalSetting 23 | } 24 | -------------------------------------------------------------------------------- /src/containers/Agenda/types.ts: -------------------------------------------------------------------------------- 1 | export interface IAgenda { 2 | _id: string 3 | title: string 4 | startDate: Date 5 | endDate: Date 6 | allDay: boolean 7 | notes: string | null 8 | rRule: string | null 9 | exDate: string | null 10 | createdAt: string 11 | updatedAt: string 12 | } 13 | 14 | export interface Query { 15 | getAgenda: IAgenda[] 16 | } 17 | 18 | export interface ScheduleProps { 19 | dataSource: IAgenda[] 20 | createAgenda: Function 21 | updateAgendaById: Function 22 | deleteAgendaById: Function 23 | } 24 | 25 | export interface Dict { 26 | [x: string]: any 27 | } 28 | -------------------------------------------------------------------------------- /src/containers/Events/types.ts: -------------------------------------------------------------------------------- 1 | export interface IAgenda { 2 | _id: string 3 | title: string 4 | startDate: Date 5 | endDate: Date 6 | allDay: boolean 7 | notes: string | null 8 | rRule: string | null 9 | exDate: string | null 10 | createdAt: string 11 | updatedAt: string 12 | } 13 | 14 | export interface Query { 15 | getAgenda: IAgenda[] 16 | } 17 | 18 | export interface ScheduleProps { 19 | dataSource: IAgenda[] 20 | createAgenda: Function 21 | updateAgendaById: Function 22 | deleteAgendaById: Function 23 | } 24 | 25 | export interface Dict { 26 | [x: string]: any 27 | } 28 | -------------------------------------------------------------------------------- /src/containers/Settings/Account/typeDefs.tsx: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | export const UPDATE_USERNAME = gql` 4 | mutation UpdateUserName($username: String!) { 5 | updateUserName(username: $username) { 6 | _id 7 | username 8 | } 9 | } 10 | ` 11 | 12 | export const UPDATE_EMAIL = gql` 13 | mutation UpdateEmail($email: String!) { 14 | updateEmail(email: $email) { 15 | _id 16 | email 17 | } 18 | } 19 | ` 20 | 21 | export const DELETE_ACCOUNT = gql` 22 | mutation DeleteAccount { 23 | deleteAccount { 24 | _id 25 | } 26 | } 27 | ` 28 | -------------------------------------------------------------------------------- /src/containers/DashBoard/types.ts: -------------------------------------------------------------------------------- 1 | export interface IBandwagonServiceInfo { 2 | data_counter: number 3 | plan_monthly_data: number 4 | plan_disk: number 5 | ve_used_disk_space_b: number 6 | plan_ram: number 7 | mem_available_kb: number 8 | swap_total_kb: number 9 | swap_available_kb: number 10 | } 11 | 12 | export interface IBandwagonUsageStatus { 13 | timestamp: string 14 | network_in_bytes: string 15 | network_out_bytes: string 16 | disk_read_bytes: string 17 | disk_write_bytes: string 18 | cpu_usage: string 19 | } 20 | 21 | export enum PostRankListType { 22 | PV, 23 | LIKE, 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/Auth/utils.ts: -------------------------------------------------------------------------------- 1 | import { AZURE_BLOB_PATH } from 'src/shared/constants' 2 | 3 | export const getBackgroundUrl = () => { 4 | const backgrounds = [ 5 | 'login-bg-light.jpg', 6 | 'login-bg-dark.jpg', 7 | 'login-bg-deep-dark.png', 8 | ] 9 | const hour = new Date().getHours() 10 | let backgroundUrl = `${AZURE_BLOB_PATH}/` 11 | 12 | if (hour >= 6 && hour <= 17) { 13 | backgroundUrl += backgrounds[0] 14 | } else if (hour >= 18 && hour <= 22) { 15 | backgroundUrl += backgrounds[1] 16 | } else { 17 | backgroundUrl += backgrounds[2] 18 | } 19 | 20 | return backgroundUrl 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": "." 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Yancey Blog CMS", 3 | "name": "The CMS for Yancey Blog", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/containers/Settings/Security/components/TwoFactors/twoFactors.module.scss: -------------------------------------------------------------------------------- 1 | .arrowIcon { 2 | width: 16px; 3 | height: 16px; 4 | } 5 | 6 | .listGroup { 7 | margin: 24px -24px -32px; 8 | 9 | :global(.MuiListItem-root) { 10 | padding: 16px 0; 11 | } 12 | } 13 | 14 | .title { 15 | margin-left: 24px; 16 | width: 16%; 17 | } 18 | 19 | .isUseTOTP { 20 | display: flex; 21 | align-items: center; 22 | font-size: 14px; 23 | color: #1a73e8; 24 | 25 | svg { 26 | width: 20px; 27 | margin-right: 8px !important; 28 | } 29 | } 30 | 31 | .phone { 32 | font-size: 14px; 33 | color: #5f6368; 34 | } 35 | -------------------------------------------------------------------------------- /src/containers/Agenda/components/CustomOpenButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Button } from '@material-ui/core' 3 | import { DateNavigator } from '@devexpress/dx-react-scheduler-material-ui' 4 | import useStyles from '../styles' 5 | 6 | const CustomOpenButton: FC = ({ 7 | text, 8 | onVisibilityToggle, 9 | }) => { 10 | const classes = useStyles() 11 | 12 | return ( 13 | 20 | ) 21 | } 22 | 23 | export default CustomOpenButton 24 | -------------------------------------------------------------------------------- /src/containers/Events/components/CustomOpenButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Button } from '@material-ui/core' 3 | import { DateNavigator } from '@devexpress/dx-react-scheduler-material-ui' 4 | import useStyles from '../styles' 5 | 6 | const CustomOpenButton: FC = ({ 7 | text, 8 | onVisibilityToggle, 9 | }) => { 10 | const classes = useStyles() 11 | 12 | return ( 13 | 20 | ) 21 | } 22 | 23 | export default CustomOpenButton 24 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module 'react/jsx-runtime' { 4 | export default any 5 | } 6 | 7 | declare namespace NodeJS { 8 | export interface ProcessEnv { 9 | REACT_APP_BEG_SERVICE_DOMAIN: string 10 | REACT_APP_UPLOADER_SERVICE_DOMAIN: string 11 | REACT_APP_ALGOLIA_APPLICATION_ID: string 12 | REACT_APP_ALGOLIA_ADMIN_API_KEY: string 13 | REACT_APP_ALGOLIA_SEARCH_INDEX: string 14 | REACT_APP_RECAPTCHA_KEY: string 15 | PORT: string 16 | } 17 | } 18 | 19 | declare interface Window { 20 | grecaptcha: { 21 | ready: Function 22 | execute: Promise 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # priority 2 | # 3 | # yarn start: .env.development.local > .env.development > .env.local > .env 4 | # yarn build: .env.production.local > .env.production > .env.local > .env 5 | # yarn test: .env.test.local > .env.test > .env 6 | 7 | REACT_APP_BEG_SERVICE_DOMAIN=http://localhost:3002/beg/graphql 8 | REACT_APP_UPLOADER_SERVICE_DOMAIN=http://localhost:3003/uploader 9 | REACT_APP_ALGOLIA_APPLICATION_ID= 10 | REACT_APP_ALGOLIA_ADMIN_API_KEY= 11 | REACT_APP_ALGOLIA_SEARCH_INDEX= 12 | REACT_APP_RECAPTCHA_KEY= 13 | PORT=3001 14 | -------------------------------------------------------------------------------- /src/containers/Agenda/components/CustomTodayButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Button } from '@material-ui/core' 3 | import { TodayButton } from '@devexpress/dx-react-scheduler-material-ui' 4 | import useStyles from '../styles' 5 | 6 | const CustomTodayButton: FC = ({ setCurrentDate }) => { 7 | const classes = useStyles() 8 | 9 | return ( 10 | 19 | ) 20 | } 21 | 22 | export default CustomTodayButton 23 | -------------------------------------------------------------------------------- /src/containers/Events/components/CustomTodayButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Button } from '@material-ui/core' 3 | import { TodayButton } from '@devexpress/dx-react-scheduler-material-ui' 4 | import useStyles from '../styles' 5 | 6 | const CustomTodayButton: FC = ({ setCurrentDate }) => { 7 | const classes = useStyles() 8 | 9 | return ( 10 | 19 | ) 20 | } 21 | 22 | export default CustomTodayButton 23 | -------------------------------------------------------------------------------- /src/containers/Post/editors/editorEmbededPlugin.ts: -------------------------------------------------------------------------------- 1 | import ToastUIEditor from '@toast-ui/editor' 2 | import { randomSeries } from 'yancey-js-util' 3 | 4 | const renderEmbeded = (wrapperId: string, iframeEl: string) => { 5 | const el = document.querySelector(`#${wrapperId}`) 6 | if (el) { 7 | el.innerHTML = iframeEl 8 | } 9 | } 10 | 11 | const addEmbededEl = () => { 12 | ToastUIEditor.codeBlockManager.setReplacer('embeded', (iframeEl: string) => { 13 | const wrapperId = `embeded_${randomSeries(6)}` 14 | setTimeout(renderEmbeded.bind(null, wrapperId, iframeEl), 0) 15 | return `
` 16 | }) 17 | } 18 | 19 | export default addEmbededEl 20 | -------------------------------------------------------------------------------- /src/containers/Settings/Profile/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 2 | 3 | const useStyles = makeStyles((theme: Theme) => 4 | createStyles({ 5 | input: { 6 | marginBottom: theme.spacing(2.5), 7 | flex: 1, 8 | }, 9 | 10 | profileContainer: { 11 | display: 'grid', 12 | gridTemplateColumns: '2fr 1fr', 13 | }, 14 | 15 | customUploader: { 16 | marginLeft: 60, 17 | width: 180, 18 | height: 180, 19 | borderRadius: '50%', 20 | 21 | '& img': { 22 | width: '100%', 23 | height: '100%', 24 | }, 25 | }, 26 | }), 27 | ) 28 | 29 | export default useStyles 30 | -------------------------------------------------------------------------------- /src/shared/globalStyles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 2 | 3 | const useStyles = makeStyles((theme: Theme) => 4 | createStyles({ 5 | addIconFab: { 6 | boxShadow: 'none!important', 7 | background: 'none!important', 8 | color: 'rgba(0, 0, 0, 0.54)', 9 | }, 10 | 11 | uploaderGroup: { 12 | display: 'flex', 13 | alignItems: 'center', 14 | marginTop: theme.spacing(2.5), 15 | }, 16 | 17 | textFieldSpace: { marginBottom: theme.spacing(2.5) }, 18 | 19 | editIcon: { 20 | cursor: 'pointer', 21 | marginRight: theme.spacing(1), 22 | }, 23 | }), 24 | ) 25 | 26 | export default useStyles 27 | -------------------------------------------------------------------------------- /src/components/NotFound/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core/styles' 2 | 3 | const useStyles = makeStyles({ 4 | notFound: { 5 | display: 'flex', 6 | flexDirection: 'column', 7 | justifyContent: 'center', 8 | alignItems: 'center', 9 | width: '100%', 10 | }, 11 | header: { 12 | margin: 0, 13 | fontSize: 36, 14 | color: '#263238', 15 | }, 16 | 17 | tips: { 18 | fontSize: 14, 19 | color: '#546e7a', 20 | }, 21 | 22 | image: { 23 | margin: '64px 0', 24 | width: 560, 25 | 26 | '& img': { 27 | width: '100%', 28 | height: '100%', 29 | objectFit: 'cover', 30 | }, 31 | }, 32 | }) 33 | 34 | export default useStyles 35 | -------------------------------------------------------------------------------- /src/containers/DashBoard/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | export const GET_BANWAGON_SERVICE_INFO = gql` 4 | query GetBanwagonServiceInfo { 5 | getBanwagonServiceInfo { 6 | data_counter 7 | plan_monthly_data 8 | plan_disk 9 | ve_used_disk_space_b 10 | plan_ram 11 | mem_available_kb 12 | swap_total_kb 13 | swap_available_kb 14 | } 15 | } 16 | ` 17 | 18 | export const GET_BANWAGON_USAGE_STATS = gql` 19 | query GetBanwagonUsageStats { 20 | getBanwagonUsageStats { 21 | timestamp 22 | network_in_bytes 23 | network_out_bytes 24 | disk_read_bytes 25 | disk_write_bytes 26 | cpu_usage 27 | } 28 | } 29 | ` 30 | -------------------------------------------------------------------------------- /src/containers/Settings/GlobalConfig/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | const GLOBAL_SETTING_FRAGMENT = gql` 4 | fragment GlobalSettingFragment on GlobalSettingModel { 5 | _id 6 | releasePostId 7 | cvPostId 8 | isGrayTheme 9 | } 10 | ` 11 | 12 | export const GLOBAL_SETTING = gql` 13 | query GetGlobalSetting { 14 | getGlobalSetting { 15 | ...GlobalSettingFragment 16 | } 17 | } 18 | ${GLOBAL_SETTING_FRAGMENT} 19 | ` 20 | 21 | export const UPDATE_GLOBAL_SETTING_BY_ID = gql` 22 | mutation UpdateGlobalSettingById($input: UpdateGlobalSettingInput!) { 23 | updateGlobalSettingById(input: $input) { 24 | ...GlobalSettingFragment 25 | } 26 | } 27 | ${GLOBAL_SETTING_FRAGMENT} 28 | ` 29 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Yancey Blog CMS 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/containers/Settings/GlobalConfig/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 2 | 3 | const useStyles = makeStyles((theme: Theme) => 4 | createStyles({ 5 | card: { marginTop: 24, padding: 12, width: 450 }, 6 | 7 | input: { 8 | width: 450, 9 | }, 10 | 11 | searchBtn: { 12 | marginLeft: 24, 13 | verticalAlign: 'bottom', 14 | }, 15 | 16 | btnGroup: { 17 | marginTop: 12, 18 | textAlign: 'right', 19 | }, 20 | 21 | checkedId: { 22 | marginBottom: 24, 23 | padding: 12, 24 | width: 450, 25 | fontSize: 16, 26 | color: '#5f6368', 27 | border: '1px dotted #5f6368', 28 | }, 29 | }), 30 | ) 31 | 32 | export default useStyles 33 | -------------------------------------------------------------------------------- /src/containers/Settings/components/SettingItemWrapper/settingItemWrapper.module.scss: -------------------------------------------------------------------------------- 1 | .paper { 2 | position: relative; 3 | margin: 24px auto 0; 4 | padding: 24px; 5 | width: 864px; 6 | border-radius: 16px; 7 | background: #ffffff; 8 | box-shadow: rgb(145 158 171 / 24%) 0px 0px 2px 0px, 9 | rgb(145 158 171 / 24%) 0px 16px 32px -4px; 10 | } 11 | 12 | .header { 13 | display: flex; 14 | justify-content: space-between; 15 | h2 { 16 | margin: 0; 17 | font-size: 22px; 18 | font-weight: 400; 19 | color: #202124; 20 | } 21 | 22 | .subHeader { 23 | margin-bottom: 64px; 24 | } 25 | } 26 | 27 | .img { 28 | height: 112px; 29 | object-fit: cover; 30 | 31 | img { 32 | height: 100%; 33 | width: auto; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/containers/Settings/Security/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | export const CREATE_TOTP = gql` 4 | mutation CreateTOTP { 5 | createTOTP { 6 | qrcode 7 | key 8 | } 9 | } 10 | ` 11 | 12 | export const CREATE_RECOVERY_CODES = gql` 13 | mutation CreateRecoveryCodes { 14 | createRecoveryCodes { 15 | recoveryCodes 16 | } 17 | } 18 | ` 19 | 20 | export const VALIDATE_TOTP = gql` 21 | mutation ValidateTOTP($input: ValidateTOTPInput!) { 22 | validateTOTP(input: $input) { 23 | _id 24 | isTOTP 25 | } 26 | } 27 | ` 28 | 29 | export const CHANGE_PASSWORD = gql` 30 | mutation ChangePassword($input: ChangePasswordInput!) { 31 | changePassword(input: $input) { 32 | _id 33 | } 34 | } 35 | ` 36 | -------------------------------------------------------------------------------- /src/containers/Settings/components/SettingItemWrapper/SettingItemWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import styles from './settingItemWrapper.module.scss' 3 | 4 | interface Props { 5 | title: string 6 | imageUrl?: string 7 | } 8 | 9 | const SettingItemWrapper: FC = ({ children, title, imageUrl }) => { 10 | return ( 11 |
12 |
13 |

{title}

14 | {imageUrl ? ( 15 |
16 | {title} 17 |
18 | ) : null} 19 |
20 | 21 | {children} 22 |
23 | ) 24 | } 25 | 26 | export default SettingItemWrapper 27 | -------------------------------------------------------------------------------- /src/components/Loading/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import CircularProgress from '@material-ui/core/CircularProgress' 4 | 5 | const useStyles = makeStyles({ 6 | mask: { 7 | display: 'flex', 8 | justifyContent: 'center', 9 | alignItems: 'center', 10 | position: 'absolute', 11 | left: 0, 12 | top: 0, 13 | width: '100%', 14 | height: '100%', 15 | background: 'rgba(255, 255, 255, 0.4)', 16 | borderRadius: '4px', 17 | zIndex: 9999, 18 | }, 19 | }) 20 | 21 | const Loading: FC = () => { 22 | const classes = useStyles() 23 | 24 | return ( 25 |
26 | 27 |
28 | ) 29 | } 30 | 31 | export default Loading 32 | -------------------------------------------------------------------------------- /src/containers/Settings/Account/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 2 | 3 | const useStyles = makeStyles((theme: Theme) => 4 | createStyles({ 5 | tip: { 6 | top: 72, 7 | color: '#5f6368', 8 | width: 460, 9 | position: 'absolute', 10 | fontSize: 14, 11 | }, 12 | 13 | checkboxLabel: { 14 | fontSize: 14, 15 | color: 'rgba(0,0,0,0.65)', 16 | }, 17 | 18 | sureToDeleteAccount: { 19 | display: 'block', 20 | position: 'relative', 21 | left: -12, 22 | margin: '36px 0 12px', 23 | }, 24 | 25 | input: { 26 | display: 'block', 27 | marginBottom: theme.spacing(2.5), 28 | width: 450, 29 | }, 30 | }), 31 | ) 32 | 33 | export default useStyles 34 | -------------------------------------------------------------------------------- /src/pages/Auth/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | export const USER_FRAGMENT = gql` 4 | fragment UserFragment on UserModel { 5 | _id 6 | authorization 7 | username 8 | email 9 | role 10 | name 11 | location 12 | organization 13 | website 14 | bio 15 | avatarUrl 16 | isTOTP 17 | createdAt 18 | createdAt 19 | updatedAt 20 | } 21 | ` 22 | 23 | export const LOGIN = gql` 24 | query Login($input: LoginInput!) { 25 | login(input: $input) { 26 | ...UserFragment 27 | } 28 | } 29 | ${USER_FRAGMENT} 30 | ` 31 | 32 | export const REGISTER = gql` 33 | mutation Register($input: RegisterInput!) { 34 | register(input: $input) { 35 | ...UserFragment 36 | } 37 | } 38 | ${USER_FRAGMENT} 39 | ` 40 | -------------------------------------------------------------------------------- /src/containers/Agenda/components/CustomNavigationButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import classNames from 'classnames' 3 | import { Button } from '@material-ui/core' 4 | import { DateNavigator } from '@devexpress/dx-react-scheduler-material-ui' 5 | import useStyles from '../styles' 6 | 7 | const CustomNavigationButton: FC = ({ 8 | type, 9 | onClick, 10 | }) => { 11 | const classes = useStyles() 12 | 13 | return ( 14 | 23 | ) 24 | } 25 | 26 | export default CustomNavigationButton 27 | -------------------------------------------------------------------------------- /src/containers/Events/components/CustomNavigationButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import classNames from 'classnames' 3 | import { Button } from '@material-ui/core' 4 | import { DateNavigator } from '@devexpress/dx-react-scheduler-material-ui' 5 | import useStyles from '../styles' 6 | 7 | const CustomNavigationButton: FC = ({ 8 | type, 9 | onClick, 10 | }) => { 11 | const classes = useStyles() 12 | 13 | return ( 14 | 23 | ) 24 | } 25 | 26 | export default CustomNavigationButton 27 | -------------------------------------------------------------------------------- /src/containers/Settings/Security/Security.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import ChangePassword from './components/ChangePassword/ChangePassword' 3 | import TwoFactors from './components/TwoFactors/TwoFactors' 4 | import SecurtyIntro from './components/SecurtyIntro/SecurtyIntro' 5 | import SettingsHeader from '../components/SettingsHeader/SettingsHeader' 6 | import SettingWrapper from '../components/SettingWrapper/SettingWrapper' 7 | 8 | const Security: FC = () => { 9 | return ( 10 | 11 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default Security 23 | -------------------------------------------------------------------------------- /.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. If you haven't already, complete the CLA. 20 | -------------------------------------------------------------------------------- /src/containers/Post/editors/editorIO.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react' 2 | import { Editor } from '@toast-ui/react-editor' 3 | 4 | export const getMarkdown = (editorRef: RefObject) => { 5 | if (editorRef.current) { 6 | return editorRef.current.getInstance().getMarkdown() 7 | } 8 | 9 | return '' 10 | } 11 | 12 | // TODO: Temporarily block pre tag 13 | export const getHTML = (editorRef: RefObject) => { 14 | if (editorRef.current) { 15 | return editorRef.current 16 | .getInstance() 17 | .getHtml() 18 | .replace(/]*>([\s\S]*?)<\/pre>/gi, '') 19 | } 20 | 21 | return '' 22 | } 23 | 24 | export const setMarkdown = (editorRef: RefObject, content: string) => { 25 | if (editorRef.current) { 26 | return editorRef.current.getInstance().setMarkdown(content) 27 | } 28 | 29 | return '' 30 | } 31 | -------------------------------------------------------------------------------- /src/containers/Agenda/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core/styles' 2 | 3 | const useStyles = makeStyles({ 4 | header: { 5 | display: 'flex', 6 | alignItems: 'center', 7 | justifyContent: 'space-between', 8 | padding: '0 24px', 9 | }, 10 | viewSwitcher: { 11 | position: 'absolute', 12 | top: '18px', 13 | right: '24px', 14 | zIndex: 1, 15 | }, 16 | customPaper: { 17 | position: 'relative', 18 | }, 19 | navigationButtonSpace: { 20 | margin: '0 4px', 21 | }, 22 | customTitle: { 23 | position: 'absolute', 24 | left: '50%', 25 | top: '34px', 26 | transform: 'translate3d(-50%, -50%, 0)', 27 | fontSize: '34px', 28 | fontWeight: 300, 29 | textTransform: 'capitalize', 30 | }, 31 | customBtn: { 32 | borderRadius: '24px', 33 | textTransform: 'capitalize', 34 | }, 35 | }) 36 | 37 | export default useStyles 38 | -------------------------------------------------------------------------------- /src/containers/Events/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core/styles' 2 | 3 | const useStyles = makeStyles({ 4 | header: { 5 | display: 'flex', 6 | alignItems: 'center', 7 | justifyContent: 'space-between', 8 | padding: '0 24px', 9 | }, 10 | viewSwitcher: { 11 | position: 'absolute', 12 | top: '18px', 13 | right: '24px', 14 | zIndex: 1, 15 | }, 16 | customPaper: { 17 | position: 'relative', 18 | }, 19 | navigationButtonSpace: { 20 | margin: '0 4px', 21 | }, 22 | customTitle: { 23 | position: 'absolute', 24 | left: '50%', 25 | top: '34px', 26 | transform: 'translate3d(-50%, -50%, 0)', 27 | fontSize: '34px', 28 | fontWeight: 300, 29 | textTransform: 'capitalize', 30 | }, 31 | customBtn: { 32 | borderRadius: '24px', 33 | textTransform: 'capitalize', 34 | }, 35 | }) 36 | 37 | export default useStyles 38 | -------------------------------------------------------------------------------- /src/pages/Layouts/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core/styles' 2 | import { DRAWER_WIDTH, FOLDER_DRAWER_WIDTH } from 'src/shared/constants' 3 | 4 | export const transition = (props: 'margin-left' | 'transform') => 5 | `${props} 300ms cubic-bezier(0.4, 0, 0.6, 1) 0ms` 6 | 7 | const useStyles = makeStyles({ 8 | layouts: { 9 | display: 'flex', 10 | overflowX: 'hidden', 11 | }, 12 | 13 | expand: { 14 | marginLeft: `${DRAWER_WIDTH}px`, 15 | transition: transition('margin-left'), 16 | }, 17 | 18 | shrink: { 19 | marginLeft: `${FOLDER_DRAWER_WIDTH}px`, 20 | transition: transition('margin-left'), 21 | }, 22 | 23 | mainWrapper: { 24 | display: 'flex', 25 | flexDirection: 'column', 26 | minWidth: `calc(100% - ${DRAWER_WIDTH}px)`, 27 | width: `calc(100% - ${FOLDER_DRAWER_WIDTH}px)`, 28 | minHeight: '100vh', 29 | }, 30 | }) 31 | 32 | export default useStyles 33 | -------------------------------------------------------------------------------- /src/containers/Agenda/components/ExternalViewSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Button, ButtonGroup } from '@material-ui/core' 3 | import { VIEW_DATE } from 'src/shared/constants' 4 | import useStyles from '../styles' 5 | 6 | interface IExternalViewSwitcher { 7 | onChange: (val: string) => void 8 | } 9 | 10 | const ExternalViewSwitcher: FC = ({ onChange }) => { 11 | const classes = useStyles() 12 | 13 | return ( 14 | 20 | {VIEW_DATE.map((val) => ( 21 | 28 | ))} 29 | 30 | ) 31 | } 32 | 33 | export default ExternalViewSwitcher 34 | -------------------------------------------------------------------------------- /src/containers/Events/components/ExternalViewSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Button, ButtonGroup } from '@material-ui/core' 3 | import { VIEW_DATE } from 'src/shared/constants' 4 | import useStyles from '../styles' 5 | 6 | interface IExternalViewSwitcher { 7 | onChange: (val: string) => void 8 | } 9 | 10 | const ExternalViewSwitcher: FC = ({ onChange }) => { 11 | const classes = useStyles() 12 | 13 | return ( 14 | 20 | {VIEW_DATE.map((val) => ( 21 | 28 | ))} 29 | 30 | ) 31 | } 32 | 33 | export default ExternalViewSwitcher 34 | -------------------------------------------------------------------------------- /src/containers/Settings/components/SettingsHeader/SettingsHeader.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | 4 | interface Props { 5 | title: string 6 | subTitle: string 7 | } 8 | 9 | const useStyles = makeStyles({ 10 | header: { textAlign: 'center' }, 11 | 12 | title: { 13 | marginTop: 0, 14 | marginBottom: '8px', 15 | fontSize: '28px', 16 | fontWeight: 500, 17 | lineHeight: 1.3, 18 | color: '#202124', 19 | }, 20 | 21 | subTitle: { 22 | marginBottom: '36px', 23 | fontSize: '16px', 24 | color: '#5f6368', 25 | }, 26 | }) 27 | 28 | const SettingsHeader: FC = ({ title, subTitle }) => { 29 | const classes = useStyles() 30 | 31 | return ( 32 |
33 |

{title}

34 |

{subTitle}

35 |
36 | ) 37 | } 38 | 39 | export default SettingsHeader 40 | -------------------------------------------------------------------------------- /src/containers/Settings/Security/components/SecurtyIntro/SecurtyIntro.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import { AZURE_BLOB_PATH } from 'src/shared/constants' 4 | import SettingItemWrapper from '../../../components/SettingItemWrapper/SettingItemWrapper' 5 | 6 | const useStyles = makeStyles({ 7 | tip: { 8 | position: 'absolute', 9 | top: '72px', 10 | width: '460px', 11 | fontSize: '14px', 12 | color: '#5f6368', 13 | }, 14 | }) 15 | 16 | const SecurtyIntro: FC = () => { 17 | const classes = useStyles() 18 | 19 | return ( 20 | 24 |

25 | The Security Checkup gives you personalized recommendations to secure 26 | your account. 27 |

28 |
29 | ) 30 | } 31 | 32 | export default SecurtyIntro 33 | -------------------------------------------------------------------------------- /src/pages/Layouts/Layouts.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from 'react' 2 | import classNames from 'classnames' 3 | import useStyles from './styles' 4 | import Header from './components/Header/Header' 5 | import Drawer from './components/Drawer/Drawer' 6 | import Mains from './components/Mains/Mains' 7 | import Footer from './components/Footer/Footer' 8 | 9 | const Layouts: FC = () => { 10 | const [open, setOpen] = useState(true) 11 | 12 | const classes = useStyles() 13 | 14 | function handleDrawerChange() { 15 | setOpen(!open) 16 | } 17 | 18 | return ( 19 |
20 | 21 |
27 |
28 | 29 |
30 |
31 |
32 | ) 33 | } 34 | 35 | export default Layouts 36 | -------------------------------------------------------------------------------- /.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 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 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 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /src/containers/DashBoard/components/StatusCardSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Card } from '@material-ui/core' 3 | import { Skeleton } from '@material-ui/lab' 4 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 5 | 6 | const useStyles = makeStyles((theme: Theme) => 7 | createStyles({ 8 | card: { 9 | padding: 16, 10 | boxShadow: 11 | 'rgb(145 158 171 / 24%) 0px 0px 2px 0px, rgb(145 158 171 / 24%) 0px 16px 32px -4px', 12 | borderRadius: 16, 13 | }, 14 | 15 | skeleton: { 16 | margin: '8px 0 36px', 17 | }, 18 | }), 19 | ) 20 | 21 | const StatusCardSkeleton: FC = () => { 22 | const classes = useStyles() 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | 33 | export default StatusCardSkeleton 34 | -------------------------------------------------------------------------------- /src/components/ConfirmModal/ConfirmModal.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { 3 | Button, 4 | DialogActions, 5 | DialogTitle, 6 | Dialog, 7 | DialogContent, 8 | DialogContentText, 9 | } from '@material-ui/core' 10 | 11 | interface IConfirmModal { 12 | onSubmit: (ids: string[]) => void 13 | } 14 | 15 | const ConfirmModal: FC = ({ routeState, onSubmit, goBack }) => ( 16 | 17 | Are you sure delete those items? 18 | 19 | 20 | Let Google help apps determine location. This means sending anonymous 21 | location data to Google, even when no apps are running. 22 | 23 | 24 | 25 | 28 | 31 | 32 | 33 | ) 34 | 35 | export default ConfirmModal 36 | -------------------------------------------------------------------------------- /src/containers/DashBoard/components/CPUChart.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from 'react' 2 | import { Line } from 'react-chartjs-2' 3 | import chartConfig from '../chartjsConfig' 4 | import { IBandwagonUsageStatus } from '../types' 5 | import ToggleChart from './ToggleChart' 6 | import UsageStatusSkeleton from './UsageStatusSkeleton' 7 | 8 | interface Props { 9 | usageStatus: IBandwagonUsageStatus[] 10 | isFetchingUsageStatus: boolean 11 | } 12 | 13 | const CPUChart: FC = ({ usageStatus, isFetchingUsageStatus }) => { 14 | const [cpuLimit, setCPULimit] = useState(12) 15 | 16 | return ( 17 | <> 18 | {isFetchingUsageStatus ? ( 19 | 20 | ) : ( 21 | setCPULimit(value)}> 22 | 27 | 28 | )} 29 | 30 | ) 31 | } 32 | 33 | export default CPUChart 34 | -------------------------------------------------------------------------------- /src/containers/Agenda/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | const AGENDA_FRAGMENT = gql` 4 | fragment AgendaFragment on AgendaModel { 5 | _id 6 | title 7 | startDate 8 | endDate 9 | allDay 10 | notes 11 | rRule 12 | exDate 13 | } 14 | ` 15 | 16 | export const CREATE_ONE_AGENDA = gql` 17 | mutation CreateAgenda($input: CreateAgendaInput!) { 18 | createAgenda(input: $input) { 19 | ...AgendaFragment 20 | } 21 | } 22 | ${AGENDA_FRAGMENT} 23 | ` 24 | 25 | export const UPDATE_ONE_AGENDA = gql` 26 | mutation UpdateAgendaById($input: UpdateAgendaInput!) { 27 | updateAgendaById(input: $input) { 28 | ...AgendaFragment 29 | } 30 | } 31 | ${AGENDA_FRAGMENT} 32 | ` 33 | 34 | export const AGENDAS = gql` 35 | query GetAgenda { 36 | getAgenda { 37 | ...AgendaFragment 38 | } 39 | } 40 | ${AGENDA_FRAGMENT} 41 | ` 42 | 43 | export const DELETE_ONE_AGENDA = gql` 44 | mutation DeleteAgendaById($id: ID!) { 45 | deleteAgendaById(id: $id) { 46 | ...AgendaFragment 47 | } 48 | } 49 | ${AGENDA_FRAGMENT} 50 | ` 51 | -------------------------------------------------------------------------------- /src/containers/Events/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | const AGENDA_FRAGMENT = gql` 4 | fragment AgendaFragment on AgendaModel { 5 | _id 6 | title 7 | startDate 8 | endDate 9 | allDay 10 | notes 11 | rRule 12 | exDate 13 | } 14 | ` 15 | 16 | export const CREATE_ONE_AGENDA = gql` 17 | mutation CreateAgenda($input: CreateAgendaInput!) { 18 | createAgenda(input: $input) { 19 | ...AgendaFragment 20 | } 21 | } 22 | ${AGENDA_FRAGMENT} 23 | ` 24 | 25 | export const UPDATE_ONE_AGENDA = gql` 26 | mutation UpdateAgendaById($input: UpdateAgendaInput!) { 27 | updateAgendaById(input: $input) { 28 | ...AgendaFragment 29 | } 30 | } 31 | ${AGENDA_FRAGMENT} 32 | ` 33 | 34 | export const AGENDAS = gql` 35 | query GetAgenda { 36 | getAgenda { 37 | ...AgendaFragment 38 | } 39 | } 40 | ${AGENDA_FRAGMENT} 41 | ` 42 | 43 | export const DELETE_ONE_AGENDA = gql` 44 | mutation DeleteAgendaById($id: ID!) { 45 | deleteAgendaById(id: $id) { 46 | ...AgendaFragment 47 | } 48 | } 49 | ${AGENDA_FRAGMENT} 50 | ` 51 | -------------------------------------------------------------------------------- /src/pages/Layouts/components/Mains/Mains.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Route, Switch } from 'react-router-dom' 3 | import loadable from '@loadable/component' 4 | import { mapRoutes } from 'src/routes' 5 | import Loading from 'src/components/Loading/InstagramLoading' 6 | import NotFound from 'src/components/NotFound/NotFound' 7 | import useStyles from './styles' 8 | 9 | const routeList = mapRoutes() 10 | 11 | const Mains: FC = () => { 12 | const classes = useStyles() 13 | 14 | return ( 15 |
16 | 17 | {routeList.map((route) => ( 18 | import(`src/containers/${route.component}`), 24 | { 25 | fallback: , 26 | }, 27 | )} 28 | /> 29 | ))} 30 | 31 | 32 | 33 | 34 |
35 | ) 36 | } 37 | 38 | export default Mains 39 | -------------------------------------------------------------------------------- /src/components/NotFound/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { useHistory } from 'react-router-dom' 3 | import { Button } from '@material-ui/core' 4 | import { AZURE_BLOB_PATH } from 'src/shared/constants' 5 | import useStyles from './styles' 6 | 7 | const NotFound: FC = () => { 8 | const classes = useStyles() 9 | const history = useHistory() 10 | 11 | const toHomePage = () => { 12 | history.push('/') 13 | } 14 | 15 | return ( 16 |
17 |

18 | 404: The page you are looking for isn’t here 19 |

20 |

21 | You either tried some shady route or you came here by mistake. Whichever 22 | it is, try using the navigation. 23 |

24 |
25 | 404-logo 26 |
27 | 30 |
31 | ) 32 | } 33 | 34 | export default NotFound 35 | -------------------------------------------------------------------------------- /src/hooks/useScript.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | export const useScriptUrl = ( 4 | url: string, 5 | isAsync = true, 6 | htmlEl?: HTMLElement, 7 | ) => { 8 | useEffect(() => { 9 | const $scriptEl = document.createElement('script') 10 | 11 | $scriptEl.src = url 12 | if (isAsync) $scriptEl.async = true 13 | 14 | htmlEl 15 | ? htmlEl.appendChild($scriptEl) 16 | : document.body.appendChild($scriptEl) 17 | return () => { 18 | htmlEl 19 | ? htmlEl.removeChild($scriptEl) 20 | : document.body.removeChild($scriptEl) 21 | } 22 | }, [htmlEl, url, isAsync]) 23 | } 24 | 25 | export const useScript = (content: any, htmlEl?: HTMLElement) => { 26 | useEffect(() => { 27 | const $scriptEl = document.createElement('script') 28 | $scriptEl.innerHTML = content 29 | 30 | htmlEl 31 | ? htmlEl.appendChild($scriptEl) 32 | : document.body.appendChild($scriptEl) 33 | return () => { 34 | htmlEl 35 | ? htmlEl.removeChild($scriptEl) 36 | : document.body.removeChild($scriptEl) 37 | } 38 | }, [htmlEl, content]) 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Uploader/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 2 | 3 | const useStyles = makeStyles((theme: Theme) => 4 | createStyles({ 5 | avatarUploader: { 6 | display: 'inline-flex', 7 | alignItems: 'center', 8 | justifyContent: 'center', 9 | position: 'relative', 10 | width: '128px', 11 | height: '128px', 12 | }, 13 | 14 | addBtn: { 15 | width: '48px', 16 | height: '48px', 17 | }, 18 | 19 | customInput: { 20 | position: 'absolute', 21 | width: '100%', 22 | height: '100%', 23 | top: 0, 24 | left: 0, 25 | opacity: 0, 26 | cursor: 'pointer', 27 | }, 28 | 29 | img: { 30 | width: '114px', 31 | height: '114px', 32 | objectFit: 'cover', 33 | }, 34 | 35 | simpleUploader: { 36 | marginLeft: '24px', 37 | }, 38 | 39 | simpleContent: { 40 | position: 'absolute', 41 | marginTop: '10px', 42 | }, 43 | 44 | customLoadingCircle: { 45 | position: 'absolute', 46 | }, 47 | }), 48 | ) 49 | 50 | export default useStyles 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Yancey Inc. and its affiliates. 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. -------------------------------------------------------------------------------- /logo/README.md: -------------------------------------------------------------------------------- 1 | # The Yancey Blog Logo 2 | 3 | This is the only official Yancey Blog Logo. 4 | Don't use any other logos to represent Yancey Blog. 5 | 6 | It comes in several flavors. 7 | 8 | ## Just the Logo 9 | 10 | Yancey Blog Logo 11 | 12 | Download as [PNG](./logo.jpg) 13 | 14 | ## Logo with Text 15 | 16 | Yancey Blog Logo with text 17 | 18 | Download as [PNG](./logo-with-text.png). 19 | 20 | ## Modifications 21 | 22 | Whenever possible, we ask you to use the originals provided on this page. 23 | 24 | If for some reason you must change how the title is rendered and can't use the prerendered version we provide, we ask that you use the free [Lato Black](http://www.latofonts.com/lato-free-fonts/) font and ensure there is enough space between the logo and the title. 25 | 26 | When in doubt, use the original logos. 27 | 28 | ## Credits 29 | 30 | The Yancey Blog logo was designed by [Yancey Leo](https://yanceyleo.com/). 31 | 32 | ## License 33 | 34 | The Redux logo is licensed under CC0, waiving all copyright. 35 | [Read the license.](../LICENSE-logo.md) 36 | -------------------------------------------------------------------------------- /src/pages/Layouts/components/Header/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core/styles' 2 | 3 | const useStyles = makeStyles({ 4 | header: { 5 | display: 'flex', 6 | alignItems: 'center', 7 | justifyContent: 'space-between', 8 | flexDirection: 'row', 9 | padding: '12px 24px 48px', 10 | background: 'transparent', 11 | boxShadow: 'none', 12 | }, 13 | fabIcon: { 14 | color: '#999', 15 | backgroundColor: '#fff', 16 | boxShadow: `0 2px 2px 0 rgba(153, 153, 153, 0.14), 17 | 0 3px 1px -2px rgba(153, 153, 153, 0.2), 18 | 0 1px 5px 0 rgba(153, 153, 153, 0.12)`, 19 | '&:hover': { backgroundColor: '#fff' }, 20 | }, 21 | left: { 22 | display: 'flex', 23 | alignItems: 'center', 24 | }, 25 | 26 | title: { 27 | marginLeft: '24px', 28 | color: '#000', 29 | }, 30 | 31 | marginRight: { 32 | marginRight: '24px', 33 | }, 34 | 35 | anchor: { 36 | color: '#000', 37 | 38 | '&:hover': { 39 | textDecoration: 'none', 40 | }, 41 | }, 42 | 43 | menu: { 44 | '& .MuiListItemIcon-root': { 45 | minWidth: 42, 46 | }, 47 | }, 48 | }) 49 | 50 | export default useStyles 51 | -------------------------------------------------------------------------------- /src/containers/DashBoard/components/TagClouds.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Paper, Chip } from '@material-ui/core' 3 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 4 | 5 | interface Props { 6 | tags: string[] 7 | loading: boolean 8 | } 9 | 10 | const useStyles = makeStyles((theme: Theme) => 11 | createStyles({ 12 | paper: { 13 | padding: 16, 14 | overflowY: 'scroll', 15 | boxShadow: 16 | 'rgb(145 158 171 / 24%) 0px 0px 2px 0px, rgb(145 158 171 / 24%) 0px 16px 32px -4px', 17 | borderRadius: 16, 18 | }, 19 | 20 | chip: { 21 | margin: 8, 22 | }, 23 | 24 | header: { 25 | marginBottom: 16, 26 | fontSize: 16, 27 | fontWeight: 600, 28 | }, 29 | }), 30 | ) 31 | 32 | const TagClouds: FC = ({ tags }) => { 33 | const classes = useStyles() 34 | 35 | return ( 36 | 37 |
Tag Clouds
38 | {tags.map((tag) => ( 39 | 40 | ))} 41 |
42 | ) 43 | } 44 | 45 | export default TagClouds 46 | -------------------------------------------------------------------------------- /src/components/Toast/Toast.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { useSnackbar, VariantType, WithSnackbarProps } from 'notistack' 3 | 4 | interface Props { 5 | setUseSnackbarRef: (showSnackbar: WithSnackbarProps) => void 6 | } 7 | 8 | const InnerSnackbarUtilsConfigurator: FC = (props: Props) => { 9 | props.setUseSnackbarRef(useSnackbar()) 10 | return null 11 | } 12 | 13 | let useSnackbarRef: WithSnackbarProps 14 | const setUseSnackbarRef = (useSnackbarRefProp: WithSnackbarProps) => { 15 | useSnackbarRef = useSnackbarRefProp 16 | } 17 | 18 | export const SnackbarUtilsConfigurator = () => { 19 | return ( 20 | 21 | ) 22 | } 23 | 24 | const toast = { 25 | success(msg: string) { 26 | this.toast(msg, 'success') 27 | }, 28 | warning(msg: string) { 29 | this.toast(msg, 'warning') 30 | }, 31 | info(msg: string) { 32 | this.toast(msg, 'info') 33 | }, 34 | error(msg: string) { 35 | this.toast(msg, 'error') 36 | }, 37 | toast(msg: string, variant: VariantType = 'default') { 38 | useSnackbarRef.enqueueSnackbar(msg, { variant }) 39 | }, 40 | } 41 | 42 | export default toast 43 | -------------------------------------------------------------------------------- /src/containers/Settings/Security/components/RecoveryCodes/recoveryCode.module.scss: -------------------------------------------------------------------------------- 1 | .recoveryCode { 2 | color: #3c4043; 3 | 4 | :global(.MuiDialog-paper) { 5 | width: 456px; 6 | } 7 | 8 | :global(.MuiDialogTitle-root) { 9 | padding-bottom: 0; 10 | } 11 | 12 | :global(.MuiDialogActions-root) { 13 | margin-top: 20px; 14 | } 15 | 16 | ul { 17 | list-style: none; 18 | } 19 | } 20 | 21 | .tips { 22 | color: #3c4043; 23 | font-size: 14px; 24 | } 25 | 26 | .listContainer { 27 | position: relative; 28 | margin: 24px 0; 29 | padding: 16px 0; 30 | text-align: center; 31 | border: 1px dotted #dadce0; 32 | } 33 | 34 | .recoveryCodesGroup { 35 | display: grid; 36 | grid-template-columns: repeat(2, 1fr); 37 | } 38 | 39 | .recoveryCodesItem { 40 | font-size: 20px; 41 | color: #5f6368; 42 | } 43 | 44 | .square { 45 | position: relative; 46 | top: -1px; 47 | display: inline-block; 48 | margin-right: 10px; 49 | width: 12px; 50 | height: 12px; 51 | border: 1px solid; 52 | } 53 | 54 | .tipGroup { 55 | margin-top: 20px; 56 | } 57 | 58 | .loading { 59 | display: flex; 60 | justify-content: center; 61 | align-items: center; 62 | height: 180px; 63 | } 64 | -------------------------------------------------------------------------------- /src/containers/DashBoard/components/DiskChart.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from 'react' 2 | import { Bar } from 'react-chartjs-2' 3 | import chartConfig from '../chartjsConfig' 4 | import { IBandwagonUsageStatus } from '../types' 5 | import ToggleChart from './ToggleChart' 6 | import UsageStatusSkeleton from './UsageStatusSkeleton' 7 | 8 | interface Props { 9 | usageStatus: IBandwagonUsageStatus[] 10 | isFetchingUsageStatus: boolean 11 | } 12 | 13 | const DiskChart: FC = ({ usageStatus, isFetchingUsageStatus }) => { 14 | const [diskLimit, setDiskLimit] = useState(12) 15 | 16 | return ( 17 | <> 18 | {isFetchingUsageStatus ? ( 19 | 20 | ) : ( 21 | setDiskLimit(value)} 23 | > 24 | 34 | 35 | )} 36 | 37 | ) 38 | } 39 | 40 | export default DiskChart 41 | -------------------------------------------------------------------------------- /src/containers/DashBoard/components/NetWorkChart.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from 'react' 2 | import { Bar } from 'react-chartjs-2' 3 | import chartConfig from '../chartjsConfig' 4 | import { IBandwagonUsageStatus } from '../types' 5 | import ToggleChart from './ToggleChart' 6 | import UsageStatusSkeleton from './UsageStatusSkeleton' 7 | 8 | interface Props { 9 | usageStatus: IBandwagonUsageStatus[] 10 | isFetchingUsageStatus: boolean 11 | } 12 | 13 | const NetWorkChart: FC = ({ usageStatus, isFetchingUsageStatus }) => { 14 | const [networkLimit, setNetworkLimit] = useState(12) 15 | 16 | return ( 17 | <> 18 | {isFetchingUsageStatus ? ( 19 | 20 | ) : ( 21 | setNetworkLimit(value)} 23 | > 24 | 34 | 35 | )} 36 | 37 | ) 38 | } 39 | 40 | export default NetWorkChart 41 | -------------------------------------------------------------------------------- /src/pages/Layouts/components/Drawer/components/ChildItem.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import classNames from 'classnames' 3 | import { Link } from '@material-ui/icons' 4 | import { getInitials } from 'src/shared/utils' 5 | import { RouteChildren } from 'src/routes' 6 | import useStyles from '../styles' 7 | 8 | interface ChildItemProps { 9 | open: boolean 10 | childRoute: RouteChildren 11 | } 12 | 13 | const ChildItem: FC = ({ 14 | open, 15 | childRoute: { name, isExternalLink }, 16 | }) => { 17 | const classes = useStyles() 18 | 19 | return ( 20 |
25 | 30 | {getInitials(name)} 31 | 32 |
37 | {name} 38 | {isExternalLink && } 39 |
40 |
41 | ) 42 | } 43 | 44 | export default ChildItem 45 | -------------------------------------------------------------------------------- /src/containers/Home/Announcement/types.ts: -------------------------------------------------------------------------------- 1 | import { Open } from 'src/hooks/useOpenModal' 2 | 3 | export interface IAnnouncement { 4 | _id: string 5 | weight: number 6 | content: string 7 | createdAt: string 8 | updatedAt: string 9 | } 10 | 11 | export interface Query { 12 | getAnnouncements: IAnnouncement[] 13 | } 14 | 15 | export interface CreateAnnouncementMutation { 16 | createAnnouncement: IAnnouncement 17 | } 18 | 19 | export interface CreateAnnouncementVars { 20 | input: { 21 | content: string 22 | } 23 | } 24 | 25 | export interface DeleteAnnouncementByIdMutation { 26 | deleteAnnouncementById: IAnnouncement 27 | } 28 | 29 | export interface DeleteAnnouncementByIdVars { 30 | id: string 31 | } 32 | 33 | export interface AnnouncementTableProps { 34 | dataSource: IAnnouncement[] 35 | isFetching: boolean 36 | isDeleting: boolean 37 | isExchanging: boolean 38 | isBatchDeleting: boolean 39 | createAnnouncement: Function 40 | updateAnnouncementById: Function 41 | deleteAnnouncementById: Function 42 | deleteAnnouncements: Function 43 | exchangePosition: Function 44 | } 45 | 46 | export interface AnnouncementModalProps { 47 | open: Open 48 | handleOpen: Function 49 | createAnnouncement: Function 50 | updateAnnouncementById: Function 51 | } 52 | -------------------------------------------------------------------------------- /src/shared/utils.ts: -------------------------------------------------------------------------------- 1 | import qs from 'query-string' 2 | import history from './history' 3 | 4 | interface Dict { 5 | [index: string]: any 6 | } 7 | 8 | export const getInitials = (txt: string) => 9 | txt 10 | .split(' ') 11 | .map((val: string) => val[0]) 12 | .join('') 13 | 14 | export const getType = (type: T) => 15 | Object.prototype.toString.call(type).slice(8, -1).toLowerCase() 16 | 17 | export const goBack = () => history.goBack() 18 | 19 | export const parseSearch = (search: string) => 20 | qs.parse(search, { parseBooleans: true }) 21 | 22 | export const stringfySearch = (searchObj: Dict) => qs.stringify(searchObj) 23 | 24 | export const noop = () => {} 25 | 26 | export const isNumber = (type: T) => getType(type) === 'number' 27 | 28 | export const isString = (type: T) => getType(type) === 'string' 29 | 30 | export const isBoolean = (type: T) => getType(type) === 'boolean' 31 | 32 | export const isArray = (type: T) => Array.isArray(type) 33 | 34 | export const logout = () => { 35 | window.localStorage.clear() 36 | history.replace('/login') 37 | } 38 | 39 | export const getURLPathName = (url: string) => 40 | decodeURI(new URL(url).pathname.slice(1)) 41 | 42 | export const generateFile = (data: string, type = 'text/plain') => { 43 | return URL.createObjectURL(new Blob([data], { type })) 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/github-actions.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions for Blog CMS v2 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | - name: Use Node.js 12.x 12 | uses: actions/setup-node@v2 13 | with: 14 | node-version: '12.x' 15 | 16 | - name: Get yarn cache directory path 17 | id: yarn-cache-dir-path 18 | run: echo "::set-output name=dir::$(yarn cache dir)" 19 | 20 | - uses: actions/cache@v2 21 | id: yarn-cache 22 | with: 23 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 24 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 25 | restore-keys: | 26 | ${{ runner.os }}-yarn- 27 | - name: Install dependencies 28 | run: yarn 29 | 30 | - name: Pre compilation 31 | run: yarn build 32 | 33 | deployment: 34 | runs-on: ubuntu-latest 35 | needs: test 36 | if: startsWith(github.ref, 'refs/tags/v') 37 | steps: 38 | - name: Deploy to server 39 | uses: appleboy/ssh-action@v0.1.4 40 | with: 41 | host: ${{ secrets.HOST }} 42 | username: ${{ secrets.USERNAME }} 43 | password: ${{ secrets.PASSWORD }} 44 | script: sh ./blog-cms-v2-deploy.sh -------------------------------------------------------------------------------- /src/containers/DashBoard/components/UsageStatusSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Card } from '@material-ui/core' 3 | import { Skeleton } from '@material-ui/lab' 4 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 5 | 6 | const useStyles = makeStyles((theme: Theme) => 7 | createStyles({ 8 | card: { 9 | padding: 16, 10 | boxShadow: 11 | 'rgb(145 158 171 / 24%) 0px 0px 2px 0px, rgb(145 158 171 / 24%) 0px 16px 32px -4px', 12 | borderRadius: 16, 13 | }, 14 | 15 | skeleton: { 16 | margin: '8px 0 36px', 17 | }, 18 | 19 | firstSkeleton: { 20 | float: 'right', 21 | }, 22 | 23 | secondSkeleton: { 24 | margin: '0 auto 12px', 25 | }, 26 | }), 27 | ) 28 | 29 | const UsageStatusSkeleton: FC = () => { 30 | const classes = useStyles() 31 | 32 | return ( 33 | 34 | 40 | 46 | 47 | 48 | ) 49 | } 50 | 51 | export default UsageStatusSkeleton 52 | -------------------------------------------------------------------------------- /src/containers/Post/components/UploaderModal.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { 3 | DialogActions, 4 | DialogTitle, 5 | Dialog, 6 | DialogContent, 7 | Button, 8 | } from '@material-ui/core' 9 | import { makeStyles } from '@material-ui/core/styles' 10 | import Uploader from '../../../components/Uploader/Uploader' 11 | 12 | interface Props { 13 | open: boolean 14 | onOk: Function 15 | onClose: Function 16 | onChange: Function 17 | } 18 | 19 | const useStyles = makeStyles({ 20 | uploaderModalContent: { 21 | margin: '24px auto', 22 | }, 23 | }) 24 | 25 | const UploaderModal: FC = ({ open, onOk, onClose, onChange }) => { 26 | const classes = useStyles() 27 | 28 | const handleOk = () => { 29 | onClose(false) 30 | onOk() 31 | } 32 | return ( 33 | onClose(false)}> 34 | Insert image to markdown editor. 35 | 36 | 37 | 38 | 39 | 42 | 45 | 46 | 47 | ) 48 | } 49 | 50 | export default UploaderModal 51 | -------------------------------------------------------------------------------- /src/containers/Post/algolia/algoliaSearch.ts: -------------------------------------------------------------------------------- 1 | import algoliasearch from 'algoliasearch' 2 | import SnackbarUtils from 'src/components/Toast/Toast' 3 | 4 | const { 5 | REACT_APP_ALGOLIA_APPLICATION_ID, 6 | REACT_APP_ALGOLIA_ADMIN_API_KEY, 7 | REACT_APP_ALGOLIA_SEARCH_INDEX, 8 | } = process.env 9 | 10 | const client = algoliasearch( 11 | REACT_APP_ALGOLIA_APPLICATION_ID, 12 | REACT_APP_ALGOLIA_ADMIN_API_KEY, 13 | ) 14 | const index = client.initIndex(REACT_APP_ALGOLIA_SEARCH_INDEX) 15 | 16 | export const sendPostToAlgolia = async ( 17 | objectID: string, 18 | name: string, 19 | description: string, 20 | content: string, 21 | imageUrl: string, 22 | labels: string[], 23 | ) => { 24 | try { 25 | await index.saveObject( 26 | { 27 | objectID, 28 | name, 29 | description, 30 | content, 31 | imageUrl, 32 | labels, 33 | }, 34 | { autoGenerateObjectIDIfNotExist: true }, 35 | ) 36 | } catch (e) { 37 | SnackbarUtils.error(e.message) 38 | } 39 | } 40 | 41 | export const deletePostOnAlgolia = async (objectID: string) => { 42 | try { 43 | await index.deleteObject(objectID) 44 | } catch (e) { 45 | SnackbarUtils.error(e.message) 46 | } 47 | } 48 | 49 | export const deletePostsOnAlgolia = async (objectIDs: string[]) => { 50 | try { 51 | await index.deleteObjects(objectIDs) 52 | } catch (e) { 53 | SnackbarUtils.error(e.message) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/containers/Music/LiveTour/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | import { BATCH_DELETE_FRAGMENT } from 'src/graphql/graphqlFragment' 3 | 4 | const LIVE_TOUR_FRAGMENT = gql` 5 | fragment LiveTourFragment on LiveTourModel { 6 | _id 7 | title 8 | posterUrl 9 | showTime 10 | createdAt 11 | updatedAt 12 | } 13 | ` 14 | 15 | export const CREATE_ONE_LIVE_TOUR = gql` 16 | mutation CreateLiveTour($input: CreateLiveTourInput!) { 17 | createLiveTour(input: $input) { 18 | ...LiveTourFragment 19 | } 20 | } 21 | ${LIVE_TOUR_FRAGMENT} 22 | ` 23 | 24 | export const UPDATE_ONE_LIVE_TOUR = gql` 25 | mutation UpdateLiveTourById($input: UpdateLiveTourInput!) { 26 | updateLiveTourById(input: $input) { 27 | ...LiveTourFragment 28 | } 29 | } 30 | ${LIVE_TOUR_FRAGMENT} 31 | ` 32 | 33 | export const LIVE_TOURS = gql` 34 | query GetLiveTours { 35 | getLiveTours { 36 | ...LiveTourFragment 37 | } 38 | } 39 | ${LIVE_TOUR_FRAGMENT} 40 | ` 41 | 42 | export const DELETE_ONE_LIVE_TOUR = gql` 43 | mutation DeleteLiveTourById($id: ID!) { 44 | deleteLiveTourById(id: $id) { 45 | ...LiveTourFragment 46 | } 47 | } 48 | ${LIVE_TOUR_FRAGMENT} 49 | ` 50 | 51 | export const BATCH_DELETE_LIVE_TOUR = gql` 52 | mutation DeleteLiveTours($ids: [ID!]!) { 53 | deleteLiveTours(ids: $ids) { 54 | ...BatchDeleteFragment 55 | } 56 | } 57 | ${BATCH_DELETE_FRAGMENT} 58 | ` 59 | -------------------------------------------------------------------------------- /src/pages/Layouts/components/Drawer/components/ParentItem.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import classNames from 'classnames' 3 | import { Link } from '@material-ui/icons' 4 | import { noop } from 'src/shared/utils' 5 | import { Route } from 'src/routes' 6 | import useStyles from '../styles' 7 | 8 | interface ParentItemProps { 9 | open: boolean 10 | route: Route 11 | handleFoldNameChange?: (name: string) => void 12 | } 13 | 14 | const ParentItem: FC = ({ 15 | open, 16 | route: { name, icon, isExternalLink }, 17 | handleFoldNameChange, 18 | }) => { 19 | const classes = useStyles() 20 | 21 | return ( 22 |
handleFoldNameChange(name) : noop} 27 | > 28 | 33 | {icon} 34 | 35 |
40 | {name} 41 | {isExternalLink && } 42 | {handleFoldNameChange && } 43 |
44 |
45 | ) 46 | } 47 | 48 | export default ParentItem 49 | -------------------------------------------------------------------------------- /src/containers/Post/styles.ts: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 2 | 3 | const useStyles = makeStyles((theme: Theme) => 4 | createStyles({ 5 | editorWrapper: { 6 | marginTop: '8px', 7 | width: '100%', 8 | }, 9 | 10 | header: { 11 | display: 'grid', 12 | gridTemplateColumns: 'repeat(2, 1fr)', 13 | }, 14 | 15 | publishTools: { 16 | display: 'flex', 17 | justifyContent: 'flex-end', 18 | alignItems: 'center', 19 | }, 20 | 21 | summary: { width: '50%', margin: '24px 0 48px' }, 22 | 23 | summaryTxtFiled: { 24 | marginBottom: '24px', 25 | }, 26 | 27 | btn: { marginLeft: theme.spacing(1), marginBottom: theme.spacing(1) }, 28 | 29 | pagination: { 30 | display: 'flex', 31 | justifyContent: 'flex-end', 32 | marginTop: '24px', 33 | }, 34 | 35 | uploadImageIcon: { 36 | position: 'relative', 37 | top: '-4px', 38 | }, 39 | 40 | search: { 41 | position: 'absolute', 42 | right: '24px', 43 | top: '83px', 44 | padding: '2px 4px', 45 | display: 'flex', 46 | alignItems: 'center', 47 | width: 400, 48 | zIndex: 1101, 49 | }, 50 | input: { 51 | marginLeft: theme.spacing(1), 52 | flex: 1, 53 | }, 54 | iconButton: { 55 | padding: 10, 56 | }, 57 | divider: { 58 | height: 28, 59 | margin: 4, 60 | }, 61 | }), 62 | ) 63 | 64 | export default useStyles 65 | -------------------------------------------------------------------------------- /src/containers/Music/BestAlbum/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | import { BATCH_DELETE_FRAGMENT } from 'src/graphql/graphqlFragment' 3 | 4 | const BEST_ALBUM_FRAGMENT = gql` 5 | fragment BestAlbumFragment on BestAlbumModel { 6 | _id 7 | title 8 | artist 9 | coverUrl 10 | mvUrl 11 | releaseDate 12 | createdAt 13 | updatedAt 14 | } 15 | ` 16 | 17 | export const CREATE_ONE_BEST_ALBUM = gql` 18 | mutation CreateBestAlbum($input: CreateBestAlbumInput!) { 19 | createBestAlbum(input: $input) { 20 | ...BestAlbumFragment 21 | } 22 | } 23 | ${BEST_ALBUM_FRAGMENT} 24 | ` 25 | 26 | export const UPDATE_ONE_BEST_ALBUM = gql` 27 | mutation UpdateBestAlbumById($input: UpdateBestAlbumInput!) { 28 | updateBestAlbumById(input: $input) { 29 | ...BestAlbumFragment 30 | } 31 | } 32 | ${BEST_ALBUM_FRAGMENT} 33 | ` 34 | 35 | export const BEST_ALBUMS = gql` 36 | query GetBestAlbums { 37 | getBestAlbums { 38 | ...BestAlbumFragment 39 | } 40 | } 41 | ${BEST_ALBUM_FRAGMENT} 42 | ` 43 | 44 | export const DELETE_ONE_BEST_ALBUM = gql` 45 | mutation DeleteBestAlbumById($id: ID!) { 46 | deleteBestAlbumById(id: $id) { 47 | ...BestAlbumFragment 48 | } 49 | } 50 | ${BEST_ALBUM_FRAGMENT} 51 | ` 52 | 53 | export const BATCH_DELETE_BEST_ALBUMS = gql` 54 | mutation DeleteBestAlbums($ids: [ID!]!) { 55 | deleteBestAlbums(ids: $ids) { 56 | ...BatchDeleteFragment 57 | } 58 | } 59 | ${BATCH_DELETE_FRAGMENT} 60 | ` 61 | -------------------------------------------------------------------------------- /src/containers/Home/OpenSource/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | import { BATCH_DELETE_FRAGMENT } from 'src/graphql/graphqlFragment' 3 | 4 | const OPEN_SOURCE_FRAGMENT = gql` 5 | fragment OpenSourceFragment on OpenSourceModel { 6 | _id 7 | title 8 | description 9 | url 10 | posterUrl 11 | createdAt 12 | updatedAt 13 | } 14 | ` 15 | 16 | export const CREATE_ONE_OPEN_SOURCE = gql` 17 | mutation CreateOpenSource($input: CreateOpenSourceInput!) { 18 | createOpenSource(input: $input) { 19 | ...OpenSourceFragment 20 | } 21 | } 22 | ${OPEN_SOURCE_FRAGMENT} 23 | ` 24 | 25 | export const UPDATE_ONE_OPEN_SOURCE = gql` 26 | mutation UpdateOpenSourceById($input: UpdateOpenSourceInput!) { 27 | updateOpenSourceById(input: $input) { 28 | ...OpenSourceFragment 29 | } 30 | } 31 | ${OPEN_SOURCE_FRAGMENT} 32 | ` 33 | 34 | export const OPEN_SOURCES = gql` 35 | query GetOpenSources { 36 | getOpenSources { 37 | ...OpenSourceFragment 38 | } 39 | } 40 | ${OPEN_SOURCE_FRAGMENT} 41 | ` 42 | 43 | export const DELETE_ONE_OPEN_SOURCE = gql` 44 | mutation DeleteOpenSourceById($id: ID!) { 45 | deleteOpenSourceById(id: $id) { 46 | ...OpenSourceFragment 47 | } 48 | } 49 | ${OPEN_SOURCE_FRAGMENT} 50 | ` 51 | 52 | export const BATCH_DELETE_OPEN_SOURCE = gql` 53 | mutation DeleteOpenSources($ids: [ID!]!) { 54 | deleteOpenSources(ids: $ids) { 55 | ...BatchDeleteFragment 56 | } 57 | } 58 | ${BATCH_DELETE_FRAGMENT} 59 | ` 60 | -------------------------------------------------------------------------------- /src/containers/Music/YanceyMusic/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | import { BATCH_DELETE_FRAGMENT } from 'src/graphql/graphqlFragment' 3 | 4 | const YANCEY_MUSIC_FRAGMENT = gql` 5 | fragment YanceyMusicFragment on YanceyMusicModel { 6 | _id 7 | title 8 | soundCloudUrl 9 | posterUrl 10 | releaseDate 11 | createdAt 12 | updatedAt 13 | } 14 | ` 15 | 16 | export const CREATE_ONE_YANCEY_MUSIC = gql` 17 | mutation CreateYanceyMusic($input: CreateYanceyMusicInput!) { 18 | createYanceyMusic(input: $input) { 19 | ...YanceyMusicFragment 20 | } 21 | } 22 | ${YANCEY_MUSIC_FRAGMENT} 23 | ` 24 | 25 | export const UPDATE_ONE_YANCEY_MUSIC = gql` 26 | mutation UpdateYanceyMusicById($input: UpdateYanceyMusicInput!) { 27 | updateYanceyMusicById(input: $input) { 28 | ...YanceyMusicFragment 29 | } 30 | } 31 | ${YANCEY_MUSIC_FRAGMENT} 32 | ` 33 | 34 | export const YANCEY_MUSIC = gql` 35 | query GetYanceyMusic { 36 | getYanceyMusic { 37 | ...YanceyMusicFragment 38 | } 39 | } 40 | ${YANCEY_MUSIC_FRAGMENT} 41 | ` 42 | 43 | export const DELETE_ONE_YANCEY_MUSIC = gql` 44 | mutation DeleteYanceyMusicById($id: ID!) { 45 | deleteYanceyMusicById(id: $id) { 46 | ...YanceyMusicFragment 47 | } 48 | } 49 | ${YANCEY_MUSIC_FRAGMENT} 50 | ` 51 | 52 | export const BATCH_DELETE_YANCEY_MUSIC = gql` 53 | mutation DeleteYanceyMusic($ids: [ID!]!) { 54 | deleteYanceyMusic(ids: $ids) { 55 | ...BatchDeleteFragment 56 | } 57 | } 58 | ${BATCH_DELETE_FRAGMENT} 59 | ` 60 | -------------------------------------------------------------------------------- /src/components/ImagePopup/ImagePopup.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import PopupState, { bindTrigger, bindPopover } from 'material-ui-popup-state' 3 | import { Popover } from '@material-ui/core' 4 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 5 | import { 6 | POPOVER_ANCHOR_ORIGIN, 7 | POPOVER_TRANSFORM_ORIGIN, 8 | } from 'src/shared/constants' 9 | 10 | interface Props { 11 | imgUrl: string 12 | imgName: string 13 | } 14 | 15 | const useStyles = makeStyles((theme: Theme) => 16 | createStyles({ 17 | thumb: { 18 | width: 150, 19 | cursor: 'pointer', 20 | }, 21 | full: { 22 | display: 'block', 23 | width: 400, 24 | }, 25 | }), 26 | ) 27 | 28 | const ImagePopup: FC = ({ imgUrl, imgName }) => { 29 | const classes = useStyles() 30 | 31 | return ( 32 | 33 | {(popupState) => ( 34 |
35 | {imgName} 41 | 47 | {imgName} 48 | 49 |
50 | )} 51 |
52 | ) 53 | } 54 | 55 | export default ImagePopup 56 | -------------------------------------------------------------------------------- /src/containers/Home/Motto/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | import { BATCH_DELETE_FRAGMENT } from 'src/graphql/graphqlFragment' 3 | 4 | const MOTTO_FRAGMENT = gql` 5 | fragment MottoFragment on MottoModel { 6 | _id 7 | content 8 | weight 9 | createdAt 10 | updatedAt 11 | } 12 | ` 13 | 14 | export const CREATE_ONE_MOTTO = gql` 15 | mutation CreateMotto($input: CreateMottoInput!) { 16 | createMotto(input: $input) { 17 | ...MottoFragment 18 | } 19 | } 20 | ${MOTTO_FRAGMENT} 21 | ` 22 | 23 | export const UPDATE_ONE_MOTTO = gql` 24 | mutation UpdateMottoById($input: UpdateMottoInput!) { 25 | updateMottoById(input: $input) { 26 | ...MottoFragment 27 | } 28 | } 29 | ${MOTTO_FRAGMENT} 30 | ` 31 | 32 | export const EXCHANGE_POSITION = gql` 33 | mutation ExchangePositionMotto($input: ExchangePositionInput!) { 34 | exchangePositionMotto(input: $input) { 35 | ...MottoFragment 36 | } 37 | } 38 | ${MOTTO_FRAGMENT} 39 | ` 40 | 41 | export const MOTTOS = gql` 42 | query GetMottos { 43 | getMottos { 44 | ...MottoFragment 45 | } 46 | } 47 | ${MOTTO_FRAGMENT} 48 | ` 49 | 50 | export const DELETE_ONE_MOTTO = gql` 51 | mutation DeleteMottoById($id: ID!) { 52 | deleteMottoById(id: $id) { 53 | ...MottoFragment 54 | } 55 | } 56 | ${MOTTO_FRAGMENT} 57 | ` 58 | 59 | export const BATCH_DELETE_MOTTO = gql` 60 | mutation DeleteMottos($ids: [ID!]!) { 61 | deleteMottos(ids: $ids) { 62 | ...BatchDeleteFragment 63 | } 64 | } 65 | ${BATCH_DELETE_FRAGMENT} 66 | ` 67 | -------------------------------------------------------------------------------- /src/containers/Music/Player/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | import { BATCH_DELETE_FRAGMENT } from 'src/graphql/graphqlFragment' 3 | 4 | const PLAYER_FRAGMENT = gql` 5 | fragment PlayerFragment on PlayerModel { 6 | _id 7 | title 8 | artist 9 | lrc 10 | coverUrl 11 | musicFileUrl 12 | isPublic 13 | weight 14 | createdAt 15 | updatedAt 16 | } 17 | ` 18 | 19 | export const CREATE_ONE_PLAYER = gql` 20 | mutation CreatePlayer($input: CreatePlayerInput!) { 21 | createPlayer(input: $input) { 22 | ...PlayerFragment 23 | } 24 | } 25 | ${PLAYER_FRAGMENT} 26 | ` 27 | 28 | export const UPDATE_ONE_PLAYER = gql` 29 | mutation UpdatePlayerById($input: UpdatePlayerInput!) { 30 | updatePlayerById(input: $input) { 31 | ...PlayerFragment 32 | } 33 | } 34 | ${PLAYER_FRAGMENT} 35 | ` 36 | 37 | export const EXCHANGE_POSITION = gql` 38 | mutation ExchangePositionPlayer($input: ExchangePositionInput!) { 39 | exchangePositionPlayer(input: $input) { 40 | ...PlayerFragment 41 | } 42 | } 43 | ${PLAYER_FRAGMENT} 44 | ` 45 | 46 | export const PLAYERS = gql` 47 | query GetPlayers { 48 | getPlayers { 49 | ...PlayerFragment 50 | } 51 | } 52 | ${PLAYER_FRAGMENT} 53 | ` 54 | 55 | export const DELETE_ONE_PLAYER = gql` 56 | mutation DeletePlayerById($id: ID!) { 57 | deletePlayerById(id: $id) { 58 | ...PlayerFragment 59 | } 60 | } 61 | ${PLAYER_FRAGMENT} 62 | ` 63 | 64 | export const BATCH_DELETE_PLAYER = gql` 65 | mutation DeletePlayers($ids: [ID!]!) { 66 | deletePlayers(ids: $ids) { 67 | ...BatchDeleteFragment 68 | } 69 | } 70 | ${BATCH_DELETE_FRAGMENT} 71 | ` 72 | -------------------------------------------------------------------------------- /src/containers/Settings/GlobalConfig/components/GrayTheme.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ChangeEvent } from 'react' 2 | import { FormControlLabel, Switch } from '@material-ui/core' 3 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 4 | import SettingItemWrapper from '../../components/SettingItemWrapper/SettingItemWrapper' 5 | 6 | interface Props { 7 | id: string 8 | isSubmitting: boolean 9 | isGrayTheme: boolean 10 | updateGlobalSettingById: Function 11 | } 12 | 13 | const useStyles = makeStyles((theme: Theme) => 14 | createStyles({ 15 | label: { 16 | marginLeft: 0, 17 | }, 18 | }), 19 | ) 20 | 21 | const GrayTheme: FC = ({ 22 | id, 23 | isGrayTheme, 24 | isSubmitting, 25 | updateGlobalSettingById, 26 | }) => { 27 | const classes = useStyles() 28 | 29 | const handleSwitchChange = async (e: ChangeEvent) => { 30 | await updateGlobalSettingById({ 31 | variables: { input: { isGrayTheme: e.target.checked, id } }, 32 | optimisticResponse: { 33 | __typename: 'Mutation', 34 | updateGlobalSettingById: { 35 | id, 36 | __typename: 'GlobalSettingModel', 37 | isGrayTheme: e.target.checked, 38 | }, 39 | }, 40 | }) 41 | } 42 | 43 | return ( 44 | 45 | 54 | } 55 | label="Is Gray Theme?" 56 | labelPlacement="start" 57 | /> 58 | 59 | ) 60 | } 61 | 62 | export default GrayTheme 63 | -------------------------------------------------------------------------------- /src/containers/Home/Announcement/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | import { BATCH_DELETE_FRAGMENT } from 'src/graphql/graphqlFragment' 3 | 4 | const ANNOUNCEMENT_FRAGMENT = gql` 5 | fragment AnnouncementFragment on AnnouncementModel { 6 | _id 7 | content 8 | weight 9 | createdAt 10 | updatedAt 11 | } 12 | ` 13 | 14 | export const CREATE_ONE_ANNOUNCEMENT = gql` 15 | mutation CreateAnnouncement($input: CreateAnnouncementInput!) { 16 | createAnnouncement(input: $input) { 17 | ...AnnouncementFragment 18 | } 19 | } 20 | ${ANNOUNCEMENT_FRAGMENT} 21 | ` 22 | 23 | export const UPDATE_ONE_ANNOUNCEMENT = gql` 24 | mutation UpdateAnnouncementById($input: UpdateAnnouncementInput!) { 25 | updateAnnouncementById(input: $input) { 26 | ...AnnouncementFragment 27 | } 28 | } 29 | ${ANNOUNCEMENT_FRAGMENT} 30 | ` 31 | 32 | export const EXCHANGE_POSITION = gql` 33 | mutation ExchangePositionAnnouncement($input: ExchangePositionInput!) { 34 | exchangePositionAnnouncement(input: $input) { 35 | ...AnnouncementFragment 36 | } 37 | } 38 | ${ANNOUNCEMENT_FRAGMENT} 39 | ` 40 | 41 | export const ANNOUNCEMENTS = gql` 42 | query GetAnnouncements { 43 | getAnnouncements { 44 | ...AnnouncementFragment 45 | } 46 | } 47 | ${ANNOUNCEMENT_FRAGMENT} 48 | ` 49 | 50 | export const DELETE_ONE_ANNOUNCEMENT = gql` 51 | mutation DeleteAnnouncementById($id: ID!) { 52 | deleteAnnouncementById(id: $id) { 53 | ...AnnouncementFragment 54 | } 55 | } 56 | ${ANNOUNCEMENT_FRAGMENT} 57 | ` 58 | 59 | export const BATCH_DELETE_ANNOUNCEMENT = gql` 60 | mutation DeleteAnnouncements($ids: [ID!]!) { 61 | deleteAnnouncements(ids: $ids) { 62 | ...BatchDeleteFragment 63 | } 64 | } 65 | ${BATCH_DELETE_FRAGMENT} 66 | ` 67 | -------------------------------------------------------------------------------- /src/components/ConfirmPoper/ConfirmPoper.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import PopupState, { bindTrigger, bindPopover } from 'material-ui-popup-state' 3 | import { 4 | Button, 5 | Popover, 6 | Paper, 7 | DialogActions, 8 | DialogTitle, 9 | } from '@material-ui/core' 10 | import { 11 | POPOVER_ANCHOR_ORIGIN, 12 | POPOVER_TRANSFORM_ORIGIN, 13 | } from 'src/shared/constants' 14 | 15 | interface Props { 16 | title?: string 17 | onOk: () => void 18 | } 19 | 20 | const ConfirmPoper: FC = ({ children, onOk, title }) => { 21 | return ( 22 | 23 | {(popupState) => ( 24 | <> 25 |
26 | {children} 27 |
28 | 34 | 35 | 36 | {title ? title : 'Are you sure you want to delete?'} 37 | 38 | 39 | 42 | 51 | 52 | 53 | 54 | 55 | )} 56 |
57 | ) 58 | } 59 | 60 | export default ConfirmPoper 61 | -------------------------------------------------------------------------------- /src/containers/Home/Cover/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | import { 3 | BATCH_DELETE_FRAGMENT, 4 | BATCH_UPDATE_FRAGMENT, 5 | } from 'src/graphql/graphqlFragment' 6 | 7 | const COVER_FRAGMENT = gql` 8 | fragment CoverFragment on CoverModel { 9 | _id 10 | title 11 | coverUrl 12 | weight 13 | isPublic 14 | createdAt 15 | updatedAt 16 | } 17 | ` 18 | 19 | export const CREATE_ONE_COVER = gql` 20 | mutation CreateCover($input: CreateCoverInput!) { 21 | createCover(input: $input) { 22 | ...CoverFragment 23 | } 24 | } 25 | ${COVER_FRAGMENT} 26 | ` 27 | 28 | export const UPDATE_ONE_COVER = gql` 29 | mutation UpdateCoverById($input: UpdateCoverInput!) { 30 | updateCoverById(input: $input) { 31 | ...CoverFragment 32 | } 33 | } 34 | ${COVER_FRAGMENT} 35 | ` 36 | 37 | export const EXCHANGE_POSITION = gql` 38 | mutation ExchangePositionCover($input: ExchangePositionInput!) { 39 | exchangePositionCover(input: $input) { 40 | ...CoverFragment 41 | } 42 | } 43 | ${COVER_FRAGMENT} 44 | ` 45 | 46 | export const COVERS = gql` 47 | query GetCovers { 48 | getCovers { 49 | ...CoverFragment 50 | } 51 | } 52 | ${COVER_FRAGMENT} 53 | ` 54 | 55 | export const DELETE_ONE_COVER = gql` 56 | mutation DeleteCoverById($id: ID!) { 57 | deleteCoverById(id: $id) { 58 | ...CoverFragment 59 | } 60 | } 61 | ${COVER_FRAGMENT} 62 | ` 63 | 64 | export const BATCH_DELETE_COVERS = gql` 65 | mutation DeleteCovers($ids: [ID!]!) { 66 | deleteCovers(ids: $ids) { 67 | ...BatchDeleteFragment 68 | } 69 | } 70 | ${BATCH_DELETE_FRAGMENT} 71 | ` 72 | 73 | export const BATCH_PUBLIC_COVERS = gql` 74 | mutation DeleteCovers($ids: [ID!]!) { 75 | publicCovers(ids: $ids) { 76 | ...BatchUpdateFragment 77 | } 78 | } 79 | ${BATCH_UPDATE_FRAGMENT} 80 | ` 81 | -------------------------------------------------------------------------------- /src/containers/Settings/Account/components/UpdateEmail.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import * as Yup from 'yup' 3 | import { useFormik } from 'formik' 4 | import { Button, TextField } from '@material-ui/core' 5 | import { AZURE_BLOB_PATH } from 'src/shared/constants' 6 | import SettingItemWrapper from '../../components/SettingItemWrapper/SettingItemWrapper' 7 | import useStyles from '../styles' 8 | 9 | interface Props { 10 | email: string 11 | updateEmail: Function 12 | } 13 | 14 | const validationSchema = Yup.object().shape({ 15 | email: Yup.string().email().required(), 16 | }) 17 | 18 | const UpdateEmail: FC = ({ email, updateEmail }) => { 19 | const classes = useStyles() 20 | 21 | const initialValues = { 22 | email, 23 | } 24 | 25 | const { handleSubmit, getFieldProps, isSubmitting, errors, values } = 26 | useFormik({ 27 | initialValues, 28 | validationSchema, 29 | onSubmit: async (values) => { 30 | await updateEmail({ 31 | variables: { email: values.email }, 32 | }) 33 | }, 34 | }) 35 | 36 | return ( 37 | 41 |
42 | 51 | 59 | 60 |
61 | ) 62 | } 63 | 64 | export default UpdateEmail 65 | -------------------------------------------------------------------------------- /src/containers/Agenda/Agenda.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { useQuery, useMutation } from '@apollo/client' 3 | import { 4 | AGENDAS, 5 | CREATE_ONE_AGENDA, 6 | UPDATE_ONE_AGENDA, 7 | DELETE_ONE_AGENDA, 8 | } from './typeDefs' 9 | import { IAgenda, Query } from './types' 10 | import Schedule from './components/Schedule' 11 | import { dateStringToDate } from './tools' 12 | 13 | const Agenda: FC = () => { 14 | const { data } = useQuery(AGENDAS, { 15 | notifyOnNetworkStatusChange: true, 16 | }) 17 | 18 | const [createAgenda] = useMutation(CREATE_ONE_AGENDA, { 19 | update(proxy, { data: { createAgenda } }) { 20 | const data = proxy.readQuery({ query: AGENDAS }) 21 | 22 | if (data) { 23 | proxy.writeQuery({ 24 | query: AGENDAS, 25 | data: { 26 | ...data, 27 | getAgenda: [createAgenda, ...data.getAgenda], 28 | }, 29 | }) 30 | } 31 | }, 32 | 33 | onError() {}, 34 | }) 35 | 36 | const [updateAgendaById] = useMutation(UPDATE_ONE_AGENDA, { 37 | onError() {}, 38 | }) 39 | 40 | const [deleteAgendaById] = useMutation(DELETE_ONE_AGENDA, { 41 | update(proxy, { data: { deleteAgendaById } }) { 42 | const data = proxy.readQuery({ query: AGENDAS }) 43 | 44 | if (data) { 45 | proxy.writeQuery({ 46 | query: AGENDAS, 47 | data: { 48 | getAgenda: data.getAgenda.filter( 49 | (agenda: IAgenda) => agenda._id !== deleteAgendaById._id, 50 | ), 51 | }, 52 | }) 53 | } 54 | }, 55 | onError() {}, 56 | }) 57 | 58 | return ( 59 | 65 | ) 66 | } 67 | 68 | export default Agenda 69 | -------------------------------------------------------------------------------- /src/containers/Events/Agenda.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { useQuery, useMutation } from '@apollo/client' 3 | import { 4 | AGENDAS, 5 | CREATE_ONE_AGENDA, 6 | UPDATE_ONE_AGENDA, 7 | DELETE_ONE_AGENDA, 8 | } from './typeDefs' 9 | import { IAgenda, Query } from './types' 10 | import Schedule from './components/Schedule' 11 | import { dateStringToDate } from './tools' 12 | 13 | const Agenda: FC = () => { 14 | const { data } = useQuery(AGENDAS, { 15 | notifyOnNetworkStatusChange: true, 16 | }) 17 | 18 | const [createAgenda] = useMutation(CREATE_ONE_AGENDA, { 19 | update(proxy, { data: { createAgenda } }) { 20 | const data = proxy.readQuery({ query: AGENDAS }) 21 | 22 | if (data) { 23 | proxy.writeQuery({ 24 | query: AGENDAS, 25 | data: { 26 | ...data, 27 | getAgenda: [createAgenda, ...data.getAgenda], 28 | }, 29 | }) 30 | } 31 | }, 32 | 33 | onError() {}, 34 | }) 35 | 36 | const [updateAgendaById] = useMutation(UPDATE_ONE_AGENDA, { 37 | onError() {}, 38 | }) 39 | 40 | const [deleteAgendaById] = useMutation(DELETE_ONE_AGENDA, { 41 | update(proxy, { data: { deleteAgendaById } }) { 42 | const data = proxy.readQuery({ query: AGENDAS }) 43 | 44 | if (data) { 45 | proxy.writeQuery({ 46 | query: AGENDAS, 47 | data: { 48 | getAgenda: data.getAgenda.filter( 49 | (agenda: IAgenda) => agenda._id !== deleteAgendaById._id, 50 | ), 51 | }, 52 | }) 53 | } 54 | }, 55 | onError() {}, 56 | }) 57 | 58 | return ( 59 | 65 | ) 66 | } 67 | 68 | export default Agenda 69 | -------------------------------------------------------------------------------- /src/containers/Settings/Account/components/UpdateUserName.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import * as Yup from 'yup' 3 | import { useFormik } from 'formik' 4 | import { Button, TextField } from '@material-ui/core' 5 | import { AZURE_BLOB_PATH } from 'src/shared/constants' 6 | import SettingItemWrapper from '../../components/SettingItemWrapper/SettingItemWrapper' 7 | import useStyles from '../styles' 8 | 9 | interface Props { 10 | username: string 11 | updateUserName: Function 12 | } 13 | 14 | const validationSchema = Yup.object().shape({ 15 | username: Yup.string().required(), 16 | }) 17 | 18 | const UpdateUserName: FC = ({ username, updateUserName }) => { 19 | const classes = useStyles() 20 | 21 | const initialValues = { 22 | username, 23 | } 24 | 25 | const { handleSubmit, getFieldProps, isSubmitting, errors, values } = 26 | useFormik({ 27 | initialValues, 28 | validationSchema, 29 | onSubmit: async (values) => { 30 | await updateUserName({ 31 | variables: { username: values.username }, 32 | }) 33 | }, 34 | }) 35 | 36 | return ( 37 | 41 |
42 | 51 | 59 | 60 |
61 | ) 62 | } 63 | 64 | export default UpdateUserName 65 | -------------------------------------------------------------------------------- /src/containers/Settings/Account/components/DeleteAccount.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from 'react' 2 | import { Button, Checkbox, FormControlLabel } from '@material-ui/core' 3 | import { AZURE_BLOB_PATH } from 'src/shared/constants' 4 | import SettingItemWrapper from '../../components/SettingItemWrapper/SettingItemWrapper' 5 | import useStyles from '../styles' 6 | 7 | interface Props { 8 | isDeletingAccount: boolean 9 | deleteAccount: Function 10 | } 11 | 12 | const DeleteAccount: FC = ({ isDeletingAccount, deleteAccount }) => { 13 | const classes = useStyles() 14 | 15 | const [checked, setChecked] = useState(false) 16 | 17 | return ( 18 | 22 |

23 | You’re trying to delete your Account, which provides access to various 24 | Yancey Blog services. You’ll no longer be able to use any of those 25 | services, and your account and data will be lost. 26 |

27 | 28 | setChecked(e.target.checked)} 35 | /> 36 | } 37 | label={ 38 | 39 | Yes, I want to permanently delete this Yancey Blog CMS Account and 40 | all its data. 41 | 42 | } 43 | labelPlacement="end" 44 | /> 45 | 46 | 54 |
55 | ) 56 | } 57 | 58 | export default DeleteAccount 59 | -------------------------------------------------------------------------------- /src/containers/Post/editors/enhanceEditor.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react' 2 | import { Editor } from '@toast-ui/react-editor' 3 | import { UploaderResponse } from 'src/components/Uploader/types' 4 | import toast from 'src/components/Toast/Toast' 5 | 6 | export const insertImage = ( 7 | editorRef: RefObject, 8 | image: UploaderResponse, 9 | ) => { 10 | if (editorRef.current) { 11 | const instance = editorRef.current.getInstance() 12 | instance.insertText(`\n\n![${image.name}](${image.url})`) 13 | } 14 | } 15 | 16 | export const insertEmbeded = (editorRef: RefObject) => { 17 | if (editorRef.current) { 18 | const instance = editorRef.current.getInstance() 19 | instance.insertText('```embeded\n\n```') 20 | } 21 | } 22 | 23 | export const enhanceUpload = ( 24 | editorRef: RefObject, 25 | setOpen: Function, 26 | ) => { 27 | if (editorRef.current) { 28 | const instance = editorRef.current.getInstance() 29 | const toolbar = instance.getUI().getToolbar() 30 | 31 | //@ts-ignore 32 | instance.eventManager.addEventType('uploadImg') 33 | 34 | //@ts-ignore 35 | instance.eventManager.listen('uploadImg', () => { 36 | setOpen(true) 37 | }) 38 | 39 | //@ts-ignore 40 | instance.eventManager.addEventType('insertEmbeded') 41 | 42 | //@ts-ignore 43 | instance.eventManager.listen('insertEmbeded', () => { 44 | insertEmbeded(editorRef) 45 | }) 46 | 47 | toolbar.insertItem(16, { 48 | type: 'button', 49 | options: { 50 | className: 'tui-image', 51 | event: 'uploadImg', 52 | tooltip: 'Insert Image', 53 | }, 54 | }) 55 | 56 | toolbar.insertItem(21, { 57 | type: 'button', 58 | options: { 59 | className: 'tui-emebed-icon', 60 | event: 'insertEmbeded', 61 | tooltip: 'Insert Embeded Block', 62 | text: 'EB', 63 | }, 64 | }) 65 | } 66 | } 67 | 68 | export const enhancePasteUpload = (editorRef: RefObject) => { 69 | // TODO: 70 | toast.error('暂不支持复制上传图片') 71 | } 72 | -------------------------------------------------------------------------------- /src/components/TableWrapper/TableWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Save } from '@material-ui/icons' 3 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 4 | 5 | const useStyles = makeStyles((theme: Theme) => 6 | createStyles({ 7 | tableWrapper: { 8 | position: 'relative', 9 | width: '100%', 10 | marginTop: '4px', 11 | }, 12 | 13 | tableIconContainer: { 14 | display: 'flex', 15 | justifyContent: 'center', 16 | alignItems: 'center', 17 | position: 'absolute', 18 | left: '16px', 19 | width: '62px', 20 | height: '65px', 21 | borderRadius: '3px', 22 | background: 'linear-gradient(60deg, #ec407a, #d81b60)', 23 | boxShadow: `0 4px 20px 0 rgba(0, 0, 0, 0.14), 24 | 0 7px 10px -5px rgba(233, 30, 99, 0.4)`, 25 | }, 26 | 27 | tableHeader: { 28 | display: 'flex', 29 | justifyContent: 'flex-start', 30 | alignItems: 'center', 31 | position: 'relative', 32 | top: '16px', 33 | zIndex: 120, 34 | }, 35 | 36 | tableIcon: { 37 | width: '20px !important', 38 | height: '20px', 39 | color: '#ffffff', 40 | }, 41 | 42 | tableTitle: { 43 | position: 'relative', 44 | top: '18px', 45 | margin: 0, 46 | marginLeft: '94px', 47 | fontSize: '18px', 48 | fontWeight: 300, 49 | color: '#3c4858', 50 | }, 51 | }), 52 | ) 53 | 54 | interface Props { 55 | tableName: string 56 | icon: string 57 | } 58 | 59 | const TableWrapper: FC = ({ children, tableName, icon }) => { 60 | const classes = useStyles() 61 | 62 | return ( 63 |
64 |
65 | 66 | 67 | 68 |

{tableName}

69 |
70 | {children} 71 |
72 | ) 73 | } 74 | 75 | export default TableWrapper 76 | -------------------------------------------------------------------------------- /src/containers/DashBoard/components/PostRankListSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Card } from '@material-ui/core' 3 | import { Skeleton } from '@material-ui/lab' 4 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 5 | import SkeletonIterator from 'src/components/SkeletonIterator/SkeletonIterator' 6 | 7 | const useStyles = makeStyles((theme: Theme) => 8 | createStyles({ 9 | card: { 10 | padding: 16, 11 | boxShadow: 12 | 'rgb(145 158 171 / 24%) 0px 0px 2px 0px, rgb(145 158 171 / 24%) 0px 16px 32px -4px', 13 | borderRadius: 16, 14 | }, 15 | 16 | header: { 17 | marginBottom: 32, 18 | }, 19 | 20 | skeletonItem: { 21 | display: 'flex', 22 | justifyContent: 'space-between', 23 | alignItems: 'center', 24 | paddingTop: 8, 25 | paddingBottom: 8, 26 | }, 27 | 28 | skeletonMeta: { 29 | display: 'flex', 30 | justifyContent: 'space-between', 31 | alignItems: 'center', 32 | }, 33 | 34 | avatar: { 35 | marginRight: 16, 36 | }, 37 | }), 38 | ) 39 | 40 | const PostRankListSkeleton: FC = () => { 41 | const classes = useStyles() 42 | 43 | const SkeletonItem = () => ( 44 |
45 |
46 | 53 | 54 |
55 | 56 |
57 | ) 58 | 59 | return ( 60 | 61 | 67 | 68 | 69 | ) 70 | } 71 | 72 | export default PostRankListSkeleton 73 | -------------------------------------------------------------------------------- /src/containers/Settings/Security/components/TOTP/totp.module.scss: -------------------------------------------------------------------------------- 1 | .totpDialog { 2 | :global(.MuiDialog-paper) { 3 | width: 480px; 4 | border-radius: 0; 5 | } 6 | } 7 | 8 | .title { 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | height: 175px; 13 | background-color: #4285f4; 14 | } 15 | 16 | .logoImg { 17 | position: relative; 18 | width: 145px; 19 | height: 145px; 20 | 21 | img { 22 | width: 100%; 23 | } 24 | } 25 | 26 | .closeBtn { 27 | position: absolute; 28 | top: 4px; 29 | right: 4px; 30 | color: rgba(255, 255, 255, 0.749); 31 | } 32 | 33 | .header { 34 | padding-top: 12px; 35 | font-size: 20px; 36 | color: #202124; 37 | } 38 | 39 | .qrcodeWrapper { 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | width: 180px; 44 | height: 210px; 45 | margin: 32px auto 24px; 46 | text-align: center; 47 | img { 48 | display: block; 49 | } 50 | } 51 | 52 | .tipGroup { 53 | padding: 0; 54 | } 55 | 56 | .tipItem { 57 | display: block; 58 | position: relative; 59 | padding-left: 20px; 60 | color: #3c4043; 61 | font-size: 14px; 62 | line-height: 1.6; 63 | &::before { 64 | position: absolute; 65 | content: ''; 66 | top: 9px; 67 | left: 0; 68 | width: 4px; 69 | height: 4px; 70 | border-radius: 50%; 71 | background: #000; 72 | } 73 | } 74 | 75 | .totpKey { 76 | margin: 16px 0; 77 | padding: 16px 0; 78 | font-size: 12px; 79 | text-align: center; 80 | border: 1px dotted #ccc; 81 | } 82 | 83 | .keyTxt { 84 | display: block; 85 | cursor: pointer; 86 | transform: scale(0.9); 87 | } 88 | 89 | .bold { 90 | font-weight: bold; 91 | } 92 | 93 | .inputTipHeader { 94 | margin-top: 20px; 95 | font-size: 14px; 96 | color: #3c4043; 97 | } 98 | 99 | .customInput { 100 | margin-top: 10px; 101 | margin-bottom: 48px; 102 | } 103 | 104 | .question { 105 | margin: 20px 0; 106 | font-size: 15px; 107 | color: #5f6368; 108 | } 109 | -------------------------------------------------------------------------------- /src/containers/DashBoard/chartjsConfig.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { IBandwagonUsageStatus } from 'src/containers/DashBoard/types' 3 | 4 | const chartConfig = ( 5 | usageStatus: IBandwagonUsageStatus[], 6 | limit: number, 7 | type1: Exclude, 8 | type2?: Exclude, 9 | ) => ({ 10 | labels: usageStatus 11 | .map(({ timestamp }) => DateTime.fromSeconds(+timestamp).toFormat('HH:mm')) 12 | .slice(-limit), 13 | datasets: [ 14 | { 15 | label: type1.split('_').join(' ').toUpperCase(), 16 | fill: false, 17 | lineTension: 0.4, 18 | backgroundColor: 'rgba(255, 99, 132, .8)', 19 | borderColor: '#FF6384', 20 | borderCapStyle: 'butt', 21 | borderDash: [], 22 | borderDashOffset: 0.0, 23 | borderJoinStyle: 'miter', 24 | pointBorderColor: '#FF6384', 25 | pointBackgroundColor: '#fff', 26 | pointBorderWidth: 1, 27 | pointHoverRadius: 5, 28 | pointHoverBackgroundColor: '#FF6384)', 29 | pointHoverBorderColor: 'rgba(220,220,220,1)', 30 | pointHoverBorderWidth: 2, 31 | pointRadius: 1, 32 | pointHitRadius: 10, 33 | data: usageStatus.map((usageStat) => usageStat[type1]).slice(-limit), 34 | }, 35 | type2 && { 36 | label: type2.split('_').join(' ').toUpperCase(), 37 | fill: false, 38 | lineTension: 0.4, 39 | backgroundColor: 'rgb(54, 162, 235, .8)', 40 | borderColor: '#36A2EB', 41 | borderCapStyle: 'butt', 42 | borderDash: [], 43 | borderDashOffset: 0.0, 44 | borderJoinStyle: 'miter', 45 | pointBorderColor: '#36A2EB', 46 | pointBackgroundColor: '#fff', 47 | pointBorderWidth: 1, 48 | pointHoverRadius: 5, 49 | pointHoverBackgroundColor: '#36A2EB', 50 | pointHoverBorderColor: 'rgba(220,220,220,1)', 51 | pointHoverBorderWidth: 2, 52 | pointRadius: 1, 53 | pointHitRadius: 10, 54 | data: usageStatus.map((usageStat) => usageStat[type2]).slice(-limit), 55 | }, 56 | ].filter(Boolean), 57 | }) 58 | 59 | export default chartConfig 60 | -------------------------------------------------------------------------------- /src/containers/DashBoard/components/ToggleChart.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState, MouseEvent } from 'react' 2 | import { Paper } from '@material-ui/core' 3 | import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab' 4 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 5 | 6 | interface Props { 7 | handleToggleChange: Function 8 | } 9 | 10 | // 24 hours, 12 hours, 1 hour 11 | const duration = [24, 12, 1] 12 | 13 | // One data is provided every five minutes, 14 | // so 12 data are provided every hour. 15 | const countByHour = 12 16 | 17 | const useStyles = makeStyles((theme: Theme) => 18 | createStyles({ 19 | paper: { 20 | position: 'relative', 21 | padding: 16, 22 | boxShadow: 23 | 'rgb(145 158 171 / 24%) 0px 0px 2px 0px, rgb(145 158 171 / 24%) 0px 16px 32px -4px', 24 | borderRadius: 16, 25 | }, 26 | 27 | toggleButtonGroup: { 28 | position: 'absolute', 29 | top: 4, 30 | right: 16, 31 | }, 32 | 33 | toggleBtn: { 34 | padding: '0 4px', 35 | border: 'none', 36 | }, 37 | }), 38 | ) 39 | 40 | const ToggleChart: FC = ({ children, handleToggleChange }) => { 41 | const classes = useStyles() 42 | 43 | const [value, setValue] = useState(countByHour) 44 | 45 | const handleChange = (e: MouseEvent, value: number) => { 46 | handleToggleChange(value) 47 | setValue(value) 48 | } 49 | 50 | return ( 51 | 52 | 60 | {duration.map((val) => ( 61 | 67 | {val}H 68 | 69 | ))} 70 | 71 | {children} 72 | 73 | ) 74 | } 75 | 76 | export default ToggleChart 77 | -------------------------------------------------------------------------------- /src/shared/constants.ts: -------------------------------------------------------------------------------- 1 | import { PopoverOrigin, SnackbarOrigin } from '@material-ui/core' 2 | import { MUIDataTableOptions } from 'mui-datatables' 3 | 4 | export const SNACKBAR_ANCHOR_ORIGIN: SnackbarOrigin = { 5 | vertical: 'top', 6 | horizontal: 'center', 7 | } 8 | 9 | export const GOOGLE_RECAPTCHA_URL = `https://www.google.com/recaptcha/api.js?render=${process.env.REACT_APP_RECAPTCHA_KEY}` 10 | 11 | export const SNACKBAR_MAX_NUM = 1 12 | 13 | export const SNACKBAR_AUTO_HIDE_DURATION = 3000 14 | 15 | export const POPOVER_ANCHOR_ORIGIN: PopoverOrigin = { 16 | vertical: 'bottom', 17 | horizontal: 'left', 18 | } 19 | 20 | export const POPOVER_TRANSFORM_ORIGIN: PopoverOrigin = { 21 | vertical: 'top', 22 | horizontal: 'center', 23 | } 24 | 25 | export const RECOVERY_CODES_FILE_NAME = 'yancey-blog-cms-recovery-codes.txt' 26 | 27 | export const PASSWORD_REGEXP = 28 | /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[^\w\s]).{8,}$/ 29 | 30 | export const MARKDOWN_EDITOR_TOOLBAR_ITEMS = [ 31 | 'heading', 32 | 'bold', 33 | 'italic', 34 | 'strike', 35 | 'divider', 36 | 'hr', 37 | 'quote', 38 | 'divider', 39 | 'ul', 40 | 'ol', 41 | 'task', 42 | 'indent', 43 | 'outdent', 44 | 'divider', 45 | 'table', 46 | 'link', 47 | 'divider', 48 | 'code', 49 | 'codeblock', 50 | ] 51 | 52 | export const DRAWER_WIDTH = 260 53 | 54 | export const FOLDER_DRAWER_WIDTH = 80 55 | 56 | export const VIEW_DATE = ['Day', 'Week', 'Month'] 57 | 58 | export const TABLE_OPTIONS: MUIDataTableOptions = { 59 | filterType: 'textField', 60 | rowsPerPage: 10, 61 | rowsPerPageOptions: [10, 20, 50], 62 | searchPlaceholder: 'Search...', 63 | } 64 | 65 | export const AZURE_BLOB_PATH = 'https://edge.yancey.app/beg' 66 | 67 | export const GOOGLE_AUTHENTICATOR_FOR_IOS = 68 | 'https://itunes.apple.com/us/app/google-authenticator/id388497605' 69 | 70 | export const GOOGLE_AUTHENTICATOR_FOR_ANDROID = 71 | 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2' 72 | 73 | export const YANCEY_BLOG_URL = 'https://www.yanceyleo.com' 74 | 75 | export const YANCEY_GITHUB_URL = 'https://www.github.com/YanceyOfficial' 76 | 77 | export const YANCEY_EMAIL_URL = 'yanceyofficial@gmail.com' 78 | -------------------------------------------------------------------------------- /src/containers/DashBoard/components/BandwagonServiceStatus.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 3 | import { IBandwagonServiceInfo } from '../types' 4 | import StatusCard from './StatusCard' 5 | import StatusCardSkeleton from './StatusCardSkeleton' 6 | 7 | interface Props { 8 | serviceInfo: IBandwagonServiceInfo 9 | isFechingServiceInfo: boolean 10 | } 11 | 12 | const useStyles = makeStyles((theme: Theme) => 13 | createStyles({ 14 | statusCardGrid: { 15 | display: 'grid', 16 | gridTemplateColumns: 'repeat(4, 1fr)', 17 | gridColumnGap: 24, 18 | marginBottom: 24, 19 | }, 20 | }), 21 | ) 22 | 23 | const BandwagonServiceStatus: FC = ({ 24 | serviceInfo, 25 | isFechingServiceInfo, 26 | }) => { 27 | const classes = useStyles() 28 | 29 | const { 30 | data_counter, 31 | plan_monthly_data, 32 | plan_disk, 33 | ve_used_disk_space_b, 34 | plan_ram, 35 | mem_available_kb, 36 | swap_total_kb, 37 | swap_available_kb, 38 | } = serviceInfo 39 | 40 | const statusCards = [ 41 | { 42 | total: plan_monthly_data, 43 | used: data_counter, 44 | unit: 'GB', 45 | title: 'Bandwidth Usage', 46 | }, 47 | { 48 | total: plan_disk, 49 | used: ve_used_disk_space_b, 50 | unit: 'GB', 51 | title: 'Disk Usage', 52 | }, 53 | { 54 | total: plan_ram, 55 | used: plan_ram - mem_available_kb * 1024, 56 | unit: 'MB', 57 | title: 'RAM Usage', 58 | }, 59 | { 60 | total: swap_total_kb * 1024, 61 | used: (swap_total_kb - swap_available_kb) * 1024, 62 | unit: 'MB', 63 | title: 'SWAP Usage', 64 | }, 65 | ] 66 | 67 | return ( 68 |
69 | {isFechingServiceInfo 70 | ? statusCards.map((statusCard) => ( 71 | 72 | )) 73 | : statusCards.map((statusCard) => ( 74 | 81 | ))} 82 |
83 | ) 84 | } 85 | 86 | export default BandwagonServiceStatus 87 | -------------------------------------------------------------------------------- /src/containers/Post/types.ts: -------------------------------------------------------------------------------- 1 | /* Post */ 2 | export interface IPost { 3 | readonly page: number 4 | readonly pageSize: number 5 | readonly total: number 6 | readonly items: IPostItem[] 7 | } 8 | 9 | export interface IPostItem { 10 | readonly _id: string 11 | readonly posterUrl: string 12 | readonly title: string 13 | readonly summary: string 14 | readonly content: string 15 | readonly tags: string[] 16 | readonly lastModifiedDate: string 17 | readonly like: number 18 | readonly pv: number 19 | readonly isPublic: boolean 20 | readonly createdAt: string 21 | readonly updatedAt: string 22 | } 23 | 24 | export interface Query { 25 | getPosts: IPost 26 | } 27 | 28 | export interface CreatePostMutation { 29 | createPost: IPostItem 30 | } 31 | 32 | export interface UpdatePostByIdMutation { 33 | updatePostById: IPostItem 34 | } 35 | 36 | export interface CreatePostVars { 37 | input: { 38 | posterUrl: string 39 | title: string 40 | summary: string 41 | content: string 42 | tags: string[] 43 | lastModifiedDate: string 44 | isPublic: boolean 45 | } 46 | } 47 | 48 | export interface UpdatePostVars { 49 | input: { 50 | id: string 51 | posterUrl: string 52 | title: string 53 | summary: string 54 | content: string 55 | tags: string[] 56 | lastModifiedDate: string 57 | isPublic: boolean 58 | } 59 | } 60 | 61 | export enum SaveType { 62 | DRAFT, 63 | FINALIZE, 64 | } 65 | 66 | /* PostStatistics */ 67 | 68 | export interface IPostStatisticsGroupItem { 69 | readonly _id: string 70 | readonly date: string 71 | readonly count: number 72 | readonly items: Array<{ 73 | readonly postId: string 74 | readonly postName: string 75 | readonly scenes: string 76 | readonly operatedAt: string 77 | }> 78 | } 79 | 80 | export interface IPostStatistics { 81 | readonly _id: string 82 | readonly postId: string 83 | readonly postName: string 84 | readonly scenes: string 85 | readonly createdAt: string 86 | readonly updatedAt: string 87 | } 88 | 89 | export interface PostStatisticsVars { 90 | input: { 91 | postId: string 92 | postName: string 93 | scenes: string 94 | } 95 | } 96 | 97 | export interface CreatePostStatisticsMutation { 98 | createPostStatistics: IPostStatistics 99 | } 100 | -------------------------------------------------------------------------------- /src/containers/Settings/Account/Account.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { useSnackbar } from 'notistack' 3 | import { useMutation } from '@apollo/client' 4 | import { logout } from 'src/shared/utils' 5 | import client from 'src/graphql/apolloClient' 6 | import { UPDATE_USERNAME, UPDATE_EMAIL, DELETE_ACCOUNT } from './typeDefs' 7 | import SettingsHeader from '../components/SettingsHeader/SettingsHeader' 8 | import SettingWrapper from '../components/SettingWrapper/SettingWrapper' 9 | import UpdateUserName from './components/UpdateUserName' 10 | import UpdateEmail from './components/UpdateEmail' 11 | import DeleteAccount from './components/DeleteAccount' 12 | 13 | const Account: FC = () => { 14 | const { enqueueSnackbar } = useSnackbar() 15 | 16 | const { username, email } = 17 | // @ts-ignore 18 | client.cache.data.data[`UserModel:${window.localStorage.getItem('userId')}`] 19 | 20 | const [updateUserName] = useMutation(UPDATE_USERNAME, { 21 | onCompleted(data) { 22 | if (data.updateUserName) { 23 | enqueueSnackbar(`Your username has been updated! Please Re-Login.`, { 24 | variant: 'success', 25 | }) 26 | 27 | logout() 28 | } 29 | }, 30 | }) 31 | 32 | const [updateEmail] = useMutation(UPDATE_EMAIL, { 33 | onCompleted(data) { 34 | if (data.updateEmail) { 35 | enqueueSnackbar(`Your email has been updated! Please Re-Login.`, { 36 | variant: 'success', 37 | }) 38 | logout() 39 | } 40 | }, 41 | }) 42 | 43 | const [deleteAccount, { loading: isDeletingAccount }] = useMutation( 44 | DELETE_ACCOUNT, 45 | { 46 | onCompleted() { 47 | enqueueSnackbar( 48 | `Your account has been deleted successfully! Just fuck off.`, 49 | { 50 | variant: 'success', 51 | }, 52 | ) 53 | 54 | logout() 55 | }, 56 | }, 57 | ) 58 | 59 | return ( 60 | 61 | 65 | 66 | 67 | 68 | 72 | 73 | ) 74 | } 75 | 76 | export default Account 77 | -------------------------------------------------------------------------------- /src/containers/DashBoard/components/PostStatistics.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { DateTime } from 'luxon' 3 | import { formatJSONDate } from 'yancey-js-util' 4 | import classNames from 'classnames' 5 | import ReactTooltip from 'react-tooltip' 6 | import CalendarHeatmap from 'react-calendar-heatmap' 7 | import 'react-calendar-heatmap/dist/styles.css' 8 | import { Paper } from '@material-ui/core' 9 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 10 | import { 11 | IPostStatistics, 12 | IPostStatisticsGroupItem, 13 | } from 'src/containers/Post/types' 14 | 15 | const useStyles = makeStyles((theme: Theme) => 16 | createStyles({ 17 | paper: { 18 | display: 'flex', 19 | justifyContent: 'center', 20 | alignItems: 'center', 21 | boxShadow: 22 | 'rgb(145 158 171 / 24%) 0px 0px 2px 0px, rgb(145 158 171 / 24%) 0px 16px 32px -4px', 23 | borderRadius: 16, 24 | }, 25 | 26 | heatmapPaper: { 27 | padding: '0 16px', 28 | '& svg': { 29 | width: '100%', 30 | height: '100%', 31 | }, 32 | }, 33 | }), 34 | ) 35 | 36 | interface Props { 37 | loading: boolean 38 | data: IPostStatistics[] 39 | } 40 | 41 | const PostStatistics: FC = ({ loading, data }) => { 42 | const classes = useStyles() 43 | 44 | return ( 45 | 46 | ({ 50 | date: postStatisticsItem._id, 51 | ...postStatisticsItem, 52 | }))} 53 | classForValue={(value: IPostStatisticsGroupItem) => { 54 | if (!value) { 55 | return 'color-empty' 56 | } 57 | return `color-gitlab-${value.count}` 58 | }} 59 | tooltipDataAttrs={(value: IPostStatisticsGroupItem) => ({ 60 | 'data-tip': value.date 61 | ? ` 62 | ${value.items 63 | .map( 64 | (item) => 65 | `「${item.postName}」 is ${item.scenes} at ${formatJSONDate( 66 | item.operatedAt, 67 | )}`, 68 | ) 69 | .join('
')} 70 | ` 71 | : `No contributions on the day.`, 72 | })} 73 | showWeekdayLabels 74 | /> 75 | 76 |
77 | ) 78 | } 79 | 80 | export default PostStatistics 81 | -------------------------------------------------------------------------------- /src/components/Loading/TwitterLoading.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | 3 | class TwitterLoading extends 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 TwitterLoading 31 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '29 0 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /src/containers/DashBoard/components/PostRankList.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { 3 | Paper, 4 | List, 5 | ListItem, 6 | ListItemAvatar, 7 | ListItemText, 8 | ListItemSecondaryAction, 9 | IconButton, 10 | Avatar, 11 | } from '@material-ui/core' 12 | import { LooksOne, LooksTwo, Looks3, Looks4, Looks5 } from '@material-ui/icons' 13 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 14 | import { IPostItem } from 'src/containers/Post/types' 15 | import PostRankListSkeleton from './PostRankListSkeleton' 16 | import { PostRankListType } from '../types' 17 | 18 | const useStyles = makeStyles((theme: Theme) => 19 | createStyles({ 20 | paper: { 21 | display: 'grid', 22 | boxShadow: 23 | 'rgb(145 158 171 / 24%) 0px 0px 2px 0px, rgb(145 158 171 / 24%) 0px 16px 32px -4px', 24 | borderRadius: 16, 25 | }, 26 | 27 | list: { 28 | width: '100%', 29 | }, 30 | 31 | header: { 32 | marginTop: 16, 33 | marginLeft: 16, 34 | fontSize: 16, 35 | fontWeight: 600, 36 | }, 37 | }), 38 | ) 39 | 40 | interface Props { 41 | type: PostRankListType 42 | topPosts: IPostItem[] 43 | loading: boolean 44 | } 45 | 46 | const numbersIcon = [ 47 | , 48 | , 49 | , 50 | , 51 | , 52 | ] 53 | 54 | const PostRankList: FC = ({ type, topPosts, loading }) => { 55 | const classes = useStyles() 56 | 57 | const renderType = (post: IPostItem) => { 58 | if (type === PostRankListType.PV) { 59 | return `(PV: ${post.pv})` 60 | } 61 | 62 | return `(Like: ${post.like})` 63 | } 64 | 65 | return ( 66 | 67 | {loading ? ( 68 | 69 | ) : ( 70 | <> 71 |
72 | TOP 5 POSTS BY {type === PostRankListType.PV ? 'PV' : 'LIKE'} 73 |
74 | 75 | 76 | {topPosts.map((post, index) => ( 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | {numbersIcon[index]} 85 | 86 | 87 | 88 | ))} 89 | 90 | 91 | )} 92 |
93 | ) 94 | } 95 | 96 | export default PostRankList 97 | -------------------------------------------------------------------------------- /src/containers/Post/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | import { BATCH_DELETE_FRAGMENT } from 'src/graphql/graphqlFragment' 3 | 4 | const POST_FRAGMENT = gql` 5 | fragment PostFragment on PostItemModel { 6 | _id 7 | posterUrl 8 | title 9 | summary 10 | content 11 | tags 12 | lastModifiedDate 13 | like 14 | pv 15 | isPublic 16 | createdAt 17 | updatedAt 18 | } 19 | ` 20 | 21 | export const CREATE_ONE_POST = gql` 22 | mutation CreatePost($input: CreatePostInput!) { 23 | createPost(input: $input) { 24 | ...PostFragment 25 | } 26 | } 27 | ${POST_FRAGMENT} 28 | ` 29 | 30 | export const UPDATE_ONE_POST = gql` 31 | mutation UpdatePostById($input: UpdatePostInput!) { 32 | updatePostById(input: $input) { 33 | ...PostFragment 34 | } 35 | } 36 | ${POST_FRAGMENT} 37 | ` 38 | 39 | export const POSTS = gql` 40 | query GetPosts($input: PaginationInput!) { 41 | getPosts(input: $input) { 42 | total 43 | page 44 | pageSize 45 | items { 46 | ...PostFragment 47 | } 48 | } 49 | } 50 | ${POST_FRAGMENT} 51 | ` 52 | 53 | export const DELETE_ONE_POST = gql` 54 | mutation DeletePostById($id: ID!) { 55 | deletePostById(id: $id) { 56 | ...PostFragment 57 | } 58 | } 59 | ${POST_FRAGMENT} 60 | ` 61 | 62 | export const BATCH_DELETE_POSTS = gql` 63 | mutation DeletePosts($ids: [ID!]!) { 64 | deletePosts(ids: $ids) { 65 | ...BatchDeleteFragment 66 | } 67 | } 68 | ${BATCH_DELETE_FRAGMENT} 69 | ` 70 | 71 | export const GET_TOP_PV_POSTS = gql` 72 | query GetTopPVPosts($limit: Int!) { 73 | getTopPVPosts(limit: $limit) { 74 | _id 75 | title 76 | posterUrl 77 | pv 78 | } 79 | } 80 | ` 81 | 82 | export const GET_TOP_LIKE_POSTS = gql` 83 | query GetTopLikePosts($limit: Int!) { 84 | getTopLikePosts(limit: $limit) { 85 | _id 86 | title 87 | posterUrl 88 | like 89 | } 90 | } 91 | ` 92 | 93 | export const GET_ALL_TAGS = gql` 94 | query GetAllTags { 95 | getAllTags { 96 | tags 97 | } 98 | } 99 | ` 100 | 101 | export const GET_POST_STATISTICS = gql` 102 | query GetPostStatistics { 103 | getPostStatistics { 104 | _id 105 | count 106 | items { 107 | postId 108 | postName 109 | scenes 110 | operatedAt 111 | } 112 | } 113 | } 114 | ` 115 | 116 | export const CREATE_POST_STATISTICS = gql` 117 | mutation CreatePostStatistics($input: CreatePostStatisticsInput!) { 118 | createPostStatistics(input: $input) { 119 | _id 120 | postId 121 | postName 122 | scenes 123 | createdAt 124 | updatedAt 125 | } 126 | } 127 | ` 128 | -------------------------------------------------------------------------------- /src/containers/Agenda/components/Schedule.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from 'react' 2 | import { Paper } from '@material-ui/core' 3 | import { 4 | ViewState, 5 | EditingState, 6 | ChangeSet, 7 | AppointmentModel, 8 | } from '@devexpress/dx-react-scheduler' 9 | import { 10 | Scheduler, 11 | EditRecurrenceMenu, 12 | DayView, 13 | WeekView, 14 | MonthView, 15 | Toolbar, 16 | DateNavigator, 17 | TodayButton, 18 | ConfirmationDialog, 19 | Appointments, 20 | AppointmentTooltip, 21 | AppointmentForm, 22 | AllDayPanel, 23 | DragDropProvider, 24 | CurrentTimeIndicator, 25 | } from '@devexpress/dx-react-scheduler-material-ui' 26 | import ExternalViewSwitcher from './ExternalViewSwitcher' 27 | import CustomNavigationButton from './CustomNavigationButton' 28 | import CustomTodayButton from './CustomTodayButton' 29 | import CustomOpenButton from './CustomOpenButton' 30 | import useStyles from '../styles' 31 | import { ScheduleProps } from '../types' 32 | import { formatChangedData } from '../tools' 33 | 34 | const Schedule: FC = ({ 35 | dataSource, 36 | createAgenda, 37 | updateAgendaById, 38 | deleteAgendaById, 39 | }) => { 40 | const classes = useStyles() 41 | 42 | const [currentViewName, setCurrentViewName] = useState('Week') 43 | 44 | const commitChanges = ({ added, changed, deleted }: ChangeSet) => { 45 | if (added) { 46 | createAgenda({ variables: { input: added } }) 47 | } 48 | if (changed) { 49 | updateAgendaById({ variables: { input: formatChangedData(changed) } }) 50 | } 51 | if (deleted) { 52 | deleteAgendaById({ variables: { id: deleted } }) 53 | } 54 | } 55 | 56 | return ( 57 | 58 | 59 | setCurrentViewName(val)} 61 | /> 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | true} /> 80 | 85 | 86 | 87 | ) 88 | } 89 | 90 | export default Schedule 91 | -------------------------------------------------------------------------------- /src/containers/Events/components/Schedule.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from 'react' 2 | import { Paper } from '@material-ui/core' 3 | import { 4 | ViewState, 5 | EditingState, 6 | ChangeSet, 7 | AppointmentModel, 8 | } from '@devexpress/dx-react-scheduler' 9 | import { 10 | Scheduler, 11 | EditRecurrenceMenu, 12 | DayView, 13 | WeekView, 14 | MonthView, 15 | Toolbar, 16 | DateNavigator, 17 | TodayButton, 18 | ConfirmationDialog, 19 | Appointments, 20 | AppointmentTooltip, 21 | AppointmentForm, 22 | AllDayPanel, 23 | DragDropProvider, 24 | CurrentTimeIndicator, 25 | } from '@devexpress/dx-react-scheduler-material-ui' 26 | import ExternalViewSwitcher from './ExternalViewSwitcher' 27 | import CustomNavigationButton from './CustomNavigationButton' 28 | import CustomTodayButton from './CustomTodayButton' 29 | import CustomOpenButton from './CustomOpenButton' 30 | import useStyles from '../styles' 31 | import { ScheduleProps } from '../types' 32 | import { formatChangedData } from '../tools' 33 | 34 | const Schedule: FC = ({ 35 | dataSource, 36 | createAgenda, 37 | updateAgendaById, 38 | deleteAgendaById, 39 | }) => { 40 | const classes = useStyles() 41 | 42 | const [currentViewName, setCurrentViewName] = useState('Week') 43 | 44 | const commitChanges = ({ added, changed, deleted }: ChangeSet) => { 45 | if (added) { 46 | createAgenda({ variables: { input: added } }) 47 | } 48 | if (changed) { 49 | updateAgendaById({ variables: { input: formatChangedData(changed) } }) 50 | } 51 | if (deleted) { 52 | deleteAgendaById({ variables: { id: deleted } }) 53 | } 54 | } 55 | 56 | return ( 57 | 58 | 59 | setCurrentViewName(val)} 61 | /> 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | true} /> 80 | 85 | 86 | 87 | ) 88 | } 89 | 90 | export default Schedule 91 | -------------------------------------------------------------------------------- /src/containers/Home/Announcement/components/AnnouncementModal.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect } from 'react' 2 | import * as Yup from 'yup' 3 | import { 4 | Button, 5 | DialogActions, 6 | DialogTitle, 7 | Dialog, 8 | DialogContent, 9 | DialogContentText, 10 | TextField, 11 | } from '@material-ui/core' 12 | import { useFormik } from 'formik' 13 | import client from 'src/graphql/apolloClient' 14 | import { AnnouncementModalProps as Props } from '../types' 15 | 16 | const AnnouncementModal: FC = ({ 17 | open, 18 | handleOpen, 19 | createAnnouncement, 20 | updateAnnouncementById, 21 | }) => { 22 | const { isOpen, id } = open 23 | 24 | const initialValues = { 25 | content: '', 26 | } 27 | 28 | const validationSchema = Yup.object().shape({ 29 | content: Yup.string().required('Content is required.'), 30 | }) 31 | 32 | const { 33 | handleSubmit, 34 | getFieldProps, 35 | setValues, 36 | resetForm, 37 | isSubmitting, 38 | errors, 39 | } = useFormik({ 40 | initialValues, 41 | validationSchema, 42 | onSubmit: async (values) => { 43 | if (id) { 44 | await updateAnnouncementById({ 45 | variables: { input: { ...values, id } }, 46 | }) 47 | } else { 48 | await createAnnouncement({ variables: { input: values } }) 49 | } 50 | 51 | resetForm() 52 | handleOpen() 53 | }, 54 | }) 55 | 56 | useEffect(() => { 57 | resetForm() 58 | 59 | if (id) { 60 | // @ts-ignore 61 | const { content } = client.cache.data.data[`AnnouncementModel:${id}`] 62 | 63 | setValues({ content }) 64 | } 65 | }, [id, resetForm, setValues]) 66 | 67 | return ( 68 | handleOpen()}> 69 | {id ? 'Update' : 'Add'} an Announcement 70 |
71 | 72 | 73 | To {id ? 'update' : 'add'} an Announcement, please enter the 74 | following fields here. We will send data after clicking the submit 75 | button. 76 | 77 | 86 | 87 | 88 | 91 | 94 | 95 |
96 |
97 | ) 98 | } 99 | 100 | export default AnnouncementModal 101 | -------------------------------------------------------------------------------- /src/containers/Home/Motto/components/MottoModal.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect } from 'react' 2 | import * as Yup from 'yup' 3 | import { 4 | Button, 5 | DialogActions, 6 | DialogTitle, 7 | Dialog, 8 | DialogContent, 9 | DialogContentText, 10 | TextField, 11 | } from '@material-ui/core' 12 | import { useFormik } from 'formik' 13 | import client from 'src/graphql/apolloClient' 14 | import { Open } from 'src/hooks/useOpenModal' 15 | 16 | interface Props { 17 | open: Open 18 | handleOpen: Function 19 | createMotto: Function 20 | updateMottoById: Function 21 | } 22 | 23 | const MottoModal: FC = ({ 24 | open, 25 | handleOpen, 26 | createMotto, 27 | updateMottoById, 28 | }) => { 29 | const { isOpen, id } = open 30 | 31 | const initialValues = { 32 | content: '', 33 | } 34 | 35 | const validationSchema = Yup.object().shape({ 36 | content: Yup.string().required('Content is required.'), 37 | }) 38 | 39 | const { 40 | handleSubmit, 41 | getFieldProps, 42 | setValues, 43 | resetForm, 44 | isSubmitting, 45 | errors, 46 | } = useFormik({ 47 | initialValues, 48 | validationSchema, 49 | onSubmit: async (values) => { 50 | if (id) { 51 | await updateMottoById({ 52 | variables: { input: { ...values, id } }, 53 | }) 54 | } else { 55 | await createMotto({ variables: { input: values } }) 56 | } 57 | resetForm() 58 | handleOpen() 59 | }, 60 | }) 61 | 62 | useEffect(() => { 63 | resetForm() 64 | 65 | if (id) { 66 | // @ts-ignore 67 | const { content } = client.cache.data.data[`MottoModel:${id}`] 68 | 69 | setValues({ content }) 70 | } 71 | }, [id, resetForm, setValues]) 72 | 73 | return ( 74 | handleOpen()}> 75 | {id ? 'Update' : 'Add'} a Motto 76 |
77 | 78 | 79 | To {id ? 'update' : 'add'} a Motto, please enter the following 80 | fields here. We will send data after clicking the submit button. 81 | 82 | 91 | 92 | 93 | 96 | 99 | 100 |
101 |
102 | ) 103 | } 104 | 105 | export default MottoModal 106 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Switch, Route, Redirect, Router } from 'react-router-dom' 4 | import loadable from '@loadable/component' 5 | import CssBaseline from '@material-ui/core/CssBaseline' 6 | import { ApolloProvider } from '@apollo/client' 7 | import { MuiPickersUtilsProvider } from '@material-ui/pickers' 8 | import LuxonUtils from '@date-io/luxon' 9 | // @ts-ignore 10 | import { SnackbarProvider } from 'notistack' 11 | import * as serviceWorkerRegistration from './serviceWorkerRegistration' 12 | import { SnackbarUtilsConfigurator } from './components/Toast/Toast' 13 | import Login from './pages/Auth/Login' 14 | import Register from './pages/Auth/Register' 15 | import Loading from './components/Loading/InstagramLoading' 16 | import client from './graphql/apolloClient' 17 | import reportWebVitals from './reportWebVitals' 18 | import history from './shared/history' 19 | import { 20 | SNACKBAR_ANCHOR_ORIGIN, 21 | SNACKBAR_MAX_NUM, 22 | SNACKBAR_AUTO_HIDE_DURATION, 23 | } from './shared/constants' 24 | import './assets/global.scss' 25 | 26 | const Layouts = loadable(() => import('./pages/Layouts/Layouts'), { 27 | fallback: , 28 | }) 29 | 30 | ReactDOM.render( 31 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | window.localStorage.getItem('token') ? ( 53 | 54 | ) : ( 55 | 61 | ) 62 | } 63 | /> 64 | 65 | 66 | 67 | 68 | 69 | , 70 | document.getElementById('root'), 71 | ) 72 | 73 | // If you want your app to work offline and load faster, you can change 74 | // unregister() to register() below. Note this comes with some pitfalls. 75 | // Learn more about service workers: https://cra.link/PWA 76 | serviceWorkerRegistration.unregister() 77 | 78 | // If you want to start measuring performance in your app, pass a function 79 | // to log results (for example: reportWebVitals(console.log)) 80 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 81 | reportWebVitals() 82 | -------------------------------------------------------------------------------- /src/graphql/apolloClient.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, InMemoryCache, from } from '@apollo/client' 2 | import { BatchHttpLink } from '@apollo/client/link/batch-http' 3 | import { onError } from '@apollo/client/link/error' 4 | import { setContext } from '@apollo/client/link/context' 5 | import { persistCache } from 'apollo-cache-persist' 6 | import SnackbarUtils from 'src/components/Toast/Toast' 7 | import { logout } from 'src/shared/utils' 8 | 9 | interface CustomGraphQLError { 10 | timestamp: string 11 | code: string 12 | message: string 13 | } 14 | 15 | const isEnvProduction = process.env.NODE_ENV 16 | const graphqlLink = process.env.REACT_APP_BEG_SERVICE_DOMAIN 17 | 18 | const httpLink = new BatchHttpLink({ 19 | uri: graphqlLink, 20 | }) 21 | 22 | const authLink = setContext((_, { headers }) => { 23 | const token = localStorage.getItem('token') 24 | return { 25 | headers: { 26 | ...headers, 27 | authorization: token ? `Bearer ${token}` : '', 28 | }, 29 | } 30 | }) 31 | 32 | const errorLink = onError(({ graphQLErrors, networkError }) => { 33 | if (graphQLErrors) { 34 | const isUnauthenticated = graphQLErrors.some((graphQLError) => { 35 | // In production environment, the error structure 36 | // was formatted as `CustomGraphQLError`. 37 | const isEnvProductionUnauthenticated = 38 | isEnvProduction && 39 | (graphQLError as unknown as CustomGraphQLError).code === 40 | 'UNAUTHENTICATED' 41 | 42 | // In non-production environment, the error structure 43 | // uses the native `GraphQLError`. 44 | const isUnEnvProductionUnauthenticated = 45 | !isEnvProduction && 46 | graphQLError.extensions && 47 | graphQLError.extensions.code === 'UNAUTHENTICATED' 48 | 49 | return isEnvProductionUnauthenticated || isUnEnvProductionUnauthenticated 50 | }) 51 | 52 | if (isUnauthenticated) { 53 | alert('Your session has expired. Please log in.') 54 | logout() 55 | return 56 | } 57 | 58 | graphQLErrors.forEach((graphQLError) => { 59 | SnackbarUtils.error(`[GraphQL error]: ${graphQLError.message}`) 60 | }) 61 | } 62 | 63 | if (networkError) { 64 | SnackbarUtils.error(`[Network error]: ${networkError.message}`) 65 | } 66 | }) 67 | 68 | // @ts-ignore 69 | const additiveLink = from([errorLink, authLink, httpLink]) 70 | 71 | const cache = new InMemoryCache() 72 | 73 | async function handlePersistCache() { 74 | await persistCache({ 75 | cache, 76 | // @ts-ignore 77 | storage: window.localStorage, 78 | maxSize: false, 79 | }) 80 | } 81 | 82 | handlePersistCache() 83 | 84 | const client = new ApolloClient({ 85 | cache, 86 | resolvers: {}, 87 | link: additiveLink, 88 | defaultOptions: { 89 | watchQuery: { 90 | fetchPolicy: 'cache-and-network', 91 | }, 92 | query: { 93 | fetchPolicy: 'network-only', 94 | errorPolicy: 'all', 95 | }, 96 | mutate: { 97 | errorPolicy: 'all', 98 | }, 99 | }, 100 | }) 101 | 102 | export default client 103 | -------------------------------------------------------------------------------- /src/containers/DashBoard/components/StatusCard.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import classNames from 'classnames' 3 | import { Card, LinearProgress, Divider } from '@material-ui/core' 4 | import { 5 | makeStyles, 6 | Theme, 7 | createStyles, 8 | withStyles, 9 | lighten, 10 | } from '@material-ui/core/styles' 11 | 12 | interface Props { 13 | title: string 14 | used: number 15 | total: number 16 | unit: string 17 | } 18 | 19 | const BorderLinearProgress = withStyles({ 20 | root: { 21 | width: '100%', 22 | height: 5, 23 | backgroundColor: lighten('#eceff1', 0.1), 24 | borderRadius: 5, 25 | }, 26 | bar: { 27 | borderRadius: 5, 28 | backgroundColor: '#3f51b5', 29 | }, 30 | })(LinearProgress) 31 | 32 | const useStyles = makeStyles((theme: Theme) => 33 | createStyles({ 34 | card: { 35 | padding: 16, 36 | boxShadow: 37 | 'rgb(145 158 171 / 24%) 0px 0px 2px 0px, rgb(145 158 171 / 24%) 0px 16px 32px -4px', 38 | borderRadius: 16, 39 | }, 40 | 41 | cardTitle: { 42 | marginTop: 0, 43 | marginBottom: 8, 44 | fontSize: 11, 45 | color: '#546e7a', 46 | textTransform: 'uppercase', 47 | fontWeight: 500, 48 | lineHeight: '13px', 49 | letterSpacing: '0.33px', 50 | }, 51 | 52 | percent: { 53 | marginRight: 20, 54 | fontSize: 24, 55 | fontWeight: 500, 56 | color: '#263238', 57 | }, 58 | 59 | vision: { 60 | display: 'flex', 61 | alignItems: 'center', 62 | }, 63 | 64 | divider: { 65 | margin: '12px 0 16px', 66 | }, 67 | 68 | bottomTxt: { 69 | fontSize: 14, 70 | }, 71 | 72 | margin: { 73 | marginRight: 16, 74 | }, 75 | }), 76 | ) 77 | 78 | const switchUnit = (unit: string, value: number) => { 79 | let res = 0 80 | switch (true) { 81 | case unit === 'GB': 82 | res = value / 1024 / 1024 / 1024 83 | break 84 | case unit === 'MB': 85 | res = value / 1024 / 1024 86 | break 87 | case unit === 'KB': 88 | res = value / 1024 89 | break 90 | default: 91 | res = value 92 | break 93 | } 94 | 95 | return res.toFixed(2) 96 | } 97 | 98 | const StatusCard: FC = ({ title, used, total, unit }) => { 99 | const classes = useStyles() 100 | const percent = (used / total) * 100 101 | 102 | return ( 103 | 104 |

{title}

105 |
106 | {Math.ceil(percent)}% 107 | 108 |
109 | 110 |

111 | 112 | Used: {switchUnit(unit, used)} {unit} 113 | 114 | 115 | Total: {switchUnit(unit, total)} {unit} 116 | 117 |

118 |
119 | ) 120 | } 121 | 122 | export default StatusCard 123 | -------------------------------------------------------------------------------- /src/containers/Settings/GlobalConfig/components/CVPicker.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState, ChangeEvent } from 'react' 2 | import { 3 | Button, 4 | TextField, 5 | ListItem, 6 | ListItemAvatar, 7 | ListItemText, 8 | Avatar, 9 | ListItemIcon, 10 | RadioGroup, 11 | Radio, 12 | Card, 13 | } from '@material-ui/core' 14 | import SettingItemWrapper from '../../components/SettingItemWrapper/SettingItemWrapper' 15 | import { PostFilterProps } from '../types' 16 | import useStyles from '../styles' 17 | 18 | type Props = PostFilterProps & { cvPostId: string } 19 | 20 | const CVPicker: FC = ({ 21 | id, 22 | posts, 23 | isFetching, 24 | isSubmitting, 25 | fetchPosts, 26 | cvPostId, 27 | updateGlobalSettingById, 28 | }) => { 29 | const classes = useStyles() 30 | 31 | const [searchValue, setSearchValue] = useState('') 32 | const handleInputChange = (e: ChangeEvent) => { 33 | setSearchValue(e.target.value) 34 | } 35 | 36 | const [checked, setChecked] = useState('') 37 | const handleRadioChange = (e: ChangeEvent) => { 38 | setChecked(e.target.value) 39 | } 40 | 41 | const onResetRadio = () => { 42 | setChecked('') 43 | } 44 | 45 | const onSubmit = async () => { 46 | await updateGlobalSettingById({ 47 | variables: { input: { cvPostId: checked, id } }, 48 | }) 49 | } 50 | 51 | return ( 52 | 53 |

post ID: {cvPostId}

54 | 55 | 60 | 70 | 71 | {posts.length > 0 ? ( 72 | 73 | 74 | {posts.map((post) => ( 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | ))} 85 | 86 | 87 |
88 | 91 | 99 |
100 |
101 | ) : null} 102 |
103 | ) 104 | } 105 | 106 | export default CVPicker 107 | -------------------------------------------------------------------------------- /src/pages/Layouts/components/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Link } from '@material-ui/core' 3 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' 4 | import { useQuery } from '@apollo/client' 5 | import { 6 | YANCEY_BLOG_URL, 7 | YANCEY_GITHUB_URL, 8 | YANCEY_EMAIL_URL, 9 | } from 'src/shared/constants' 10 | import { GLOBAL_SETTING } from 'src/containers/Settings/GlobalConfig/typeDefs' 11 | import { Query } from 'src/containers/Settings/GlobalConfig/types' 12 | 13 | const useStyles = makeStyles((theme: Theme) => 14 | createStyles({ 15 | footer: { 16 | display: 'flex', 17 | justifyContent: 'space-between', 18 | alignItems: 'center', 19 | margin: 0, 20 | padding: '12px 24px', 21 | fontSize: '14px', 22 | textAlign: 'right', 23 | color: '#3c4858', 24 | }, 25 | 26 | footerList: { 27 | padding: 0, 28 | listStyle: 'none', 29 | }, 30 | 31 | footerItem: { 32 | display: 'inline-block', 33 | marginRight: theme.spacing(3.75), 34 | fontSize: '12px', 35 | fontWeight: 500, 36 | color: '#3c4858', 37 | }, 38 | }), 39 | ) 40 | 41 | const Footer: FC = () => { 42 | const classes = useStyles() 43 | 44 | const { data } = useQuery(GLOBAL_SETTING, { 45 | notifyOnNetworkStatusChange: true, 46 | }) 47 | 48 | return ( 49 |
50 |
    51 |
  • 52 | 57 | BLOG 58 | 59 |
  • 60 |
  • 61 | 68 | ABOUT ME 69 | 70 |
  • 71 |
  • 72 | 79 | RELEASE LOG 80 | 81 |
  • 82 |
  • 83 | 88 | EMAIL 89 | 90 |
  • 91 |
  • 92 | 97 | GITHUB 98 | 99 |
  • 100 |
101 |

102 | {`Copyright © ${new Date().getFullYear()} Yancey Inc. and its affiliates.`} 103 |

104 |
105 | ) 106 | } 107 | 108 | export default Footer 109 | -------------------------------------------------------------------------------- /src/service-worker.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /* eslint-disable no-restricted-globals */ 3 | 4 | // This service worker can be customized! 5 | // See https://developers.google.com/web/tools/workbox/modules 6 | // for the list of available Workbox modules, or add any other 7 | // code you'd like. 8 | // You can also remove this file if you'd prefer not to use a 9 | // service worker, and the Workbox build step will be skipped. 10 | 11 | import { clientsClaim } from 'workbox-core' 12 | import { ExpirationPlugin } from 'workbox-expiration' 13 | import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching' 14 | import { registerRoute } from 'workbox-routing' 15 | import { StaleWhileRevalidate } from 'workbox-strategies' 16 | 17 | declare const self: ServiceWorkerGlobalScope 18 | 19 | clientsClaim() 20 | 21 | // Precache all of the assets generated by your build process. 22 | // Their URLs are injected into the manifest variable below. 23 | // This variable must be present somewhere in your service worker file, 24 | // even if you decide not to use precaching. See https://cra.link/PWA 25 | precacheAndRoute(self.__WB_MANIFEST) 26 | 27 | // Set up App Shell-style routing, so that all navigation requests 28 | // are fulfilled with your index.html shell. Learn more at 29 | // https://developers.google.com/web/fundamentals/architecture/app-shell 30 | const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$') 31 | registerRoute( 32 | // Return false to exempt requests from being fulfilled by index.html. 33 | ({ request, url }: { request: Request; url: URL }) => { 34 | // If this isn't a navigation, skip. 35 | if (request.mode !== 'navigate') { 36 | return false 37 | } 38 | 39 | // If this is a URL that starts with /_, skip. 40 | if (url.pathname.startsWith('/_')) { 41 | return false 42 | } 43 | 44 | // If this looks like a URL for a resource, because it contains 45 | // a file extension, skip. 46 | if (url.pathname.match(fileExtensionRegexp)) { 47 | return false 48 | } 49 | 50 | // Return true to signal that we want to use the handler. 51 | return true 52 | }, 53 | createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html'), 54 | ) 55 | 56 | // An example runtime caching route for requests that aren't handled by the 57 | // precache, in this case same-origin .png requests like those from in public/ 58 | registerRoute( 59 | // Add in any other file extensions or routing criteria as needed. 60 | ({ url }) => 61 | url.origin === self.location.origin && url.pathname.endsWith('.png'), 62 | // Customize this strategy as needed, e.g., by changing to CacheFirst. 63 | new StaleWhileRevalidate({ 64 | cacheName: 'images', 65 | plugins: [ 66 | // Ensure that once this runtime cache reaches a maximum size the 67 | // least-recently used images are removed. 68 | new ExpirationPlugin({ maxEntries: 50 }), 69 | ], 70 | }), 71 | ) 72 | 73 | // This allows the web app to trigger skipWaiting via 74 | // registration.waiting.postMessage({type: 'SKIP_WAITING'}) 75 | self.addEventListener('message', (event) => { 76 | if (event.data && event.data.type === 'SKIP_WAITING') { 77 | self.skipWaiting() 78 | } 79 | }) 80 | 81 | // Any other custom service worker logic can go here. 82 | -------------------------------------------------------------------------------- /src/containers/Settings/GlobalConfig/GlobalConfig.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { useLazyQuery, useQuery, useMutation } from '@apollo/client' 3 | import { useSnackbar } from 'notistack' 4 | import Loading from 'src/components/Loading/Loading' 5 | import { POSTS } from 'src/containers/Post/typeDefs' 6 | import { Query as PostsQuery } from 'src/containers/Post/types' 7 | import SettingWrapper from '../components/SettingWrapper/SettingWrapper' 8 | import SettingsHeader from '../components/SettingsHeader/SettingsHeader' 9 | import { GLOBAL_SETTING, UPDATE_GLOBAL_SETTING_BY_ID } from './typeDefs' 10 | import { Query } from './types' 11 | import ReleasePicker from './components/ReleasePicker' 12 | import CVPicker from './components/CVPicker' 13 | import GrayTheme from './components/GrayTheme' 14 | 15 | const GlobalConfig: FC = () => { 16 | const { enqueueSnackbar } = useSnackbar() 17 | 18 | const [fetchPostsByPage, { loading: isFetchingPosts, data: postsData }] = 19 | useLazyQuery(POSTS, { 20 | notifyOnNetworkStatusChange: true, 21 | }) 22 | 23 | const fetchPosts = (title: string) => { 24 | fetchPostsByPage({ 25 | variables: { 26 | input: { 27 | page: 1, 28 | pageSize: 10, 29 | title, 30 | }, 31 | }, 32 | }) 33 | } 34 | 35 | const { loading: isFetching, data } = useQuery(GLOBAL_SETTING, { 36 | notifyOnNetworkStatusChange: true, 37 | }) 38 | 39 | const [updateGlobalSettingById, { loading: isSubmitting }] = useMutation( 40 | UPDATE_GLOBAL_SETTING_BY_ID, 41 | { 42 | onCompleted() { 43 | enqueueSnackbar('Update success!', { variant: 'success' }) 44 | }, 45 | onError() {}, 46 | }, 47 | ) 48 | 49 | return ( 50 | 51 | {isFetching ? ( 52 | 53 | ) : ( 54 | <> 55 | 59 | 60 | 69 | 78 | 84 | 85 | )} 86 | 87 | ) 88 | } 89 | 90 | export default GlobalConfig 91 | -------------------------------------------------------------------------------- /src/containers/Settings/GlobalConfig/components/ReleasePicker.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState, ChangeEvent } from 'react' 2 | import { 3 | Button, 4 | TextField, 5 | ListItem, 6 | ListItemAvatar, 7 | ListItemText, 8 | Avatar, 9 | ListItemIcon, 10 | RadioGroup, 11 | Radio, 12 | Card, 13 | } from '@material-ui/core' 14 | import SettingItemWrapper from '../../components/SettingItemWrapper/SettingItemWrapper' 15 | import { PostFilterProps } from '../types' 16 | import useStyles from '../styles' 17 | 18 | type Props = PostFilterProps & { releasePostId: string } 19 | 20 | const ReleasePicker: FC = ({ 21 | id, 22 | posts, 23 | isFetching, 24 | isSubmitting, 25 | fetchPosts, 26 | releasePostId, 27 | updateGlobalSettingById, 28 | }) => { 29 | const classes = useStyles() 30 | 31 | const [searchValue, setSearchValue] = useState('') 32 | const handleInputChange = (e: ChangeEvent) => { 33 | setSearchValue(e.target.value) 34 | } 35 | 36 | const [checked, setChecked] = useState('') 37 | const handleRadioChange = (e: ChangeEvent) => { 38 | setChecked(e.target.value) 39 | } 40 | 41 | const onResetRadio = () => { 42 | setChecked('') 43 | } 44 | 45 | const onSubmit = async () => { 46 | await updateGlobalSettingById({ 47 | variables: { input: { releasePostId: checked, id } }, 48 | }) 49 | } 50 | 51 | return ( 52 | 53 |

post ID: {releasePostId}

54 | 55 | 60 | 70 | 71 | {posts.length > 0 ? ( 72 | 73 | 74 | {posts.map((post) => ( 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | ))} 85 | 86 | 87 |
88 | 91 | 99 |
100 |
101 | ) : null} 102 |
103 | ) 104 | } 105 | 106 | export default ReleasePicker 107 | -------------------------------------------------------------------------------- /src/containers/Settings/Security/components/TwoFactors/TwoFactors.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from 'react' 2 | import { 3 | List, 4 | ListItem, 5 | ListItemText, 6 | ListItemAvatar, 7 | Divider, 8 | } from '@material-ui/core' 9 | import { 10 | ArrowForwardIos, 11 | SentimentVerySatisfied, 12 | SentimentDissatisfied, 13 | } from '@material-ui/icons' 14 | import { AZURE_BLOB_PATH } from 'src/shared/constants' 15 | import client from 'src/graphql/apolloClient' 16 | import SnackbarUtils from 'src/components/Toast/Toast' 17 | import SettingItemWrapper from 'src/containers/Settings/components/SettingItemWrapper/SettingItemWrapper' 18 | import TOTP from '../TOTP/TOTP' 19 | import RecoveryCodes from '../RecoveryCodes/RecoveryCodes' 20 | import styles from './twoFactors.module.scss' 21 | 22 | const TwoFactors: FC = () => { 23 | const [openTOTP, setOpenTOTP] = useState(false) 24 | const [openRecoveryCodes, setOpenRecoveryCodes] = useState(false) 25 | 26 | const { isTOTP } = 27 | // @ts-ignore 28 | client.cache.data.data[`UserModel:${window.localStorage.getItem('userId')}`] 29 | 30 | const openRecoveryCodesDialog = () => { 31 | if (!isTOTP) { 32 | SnackbarUtils.error('Please turn on Authenticator app options first!') 33 | return 34 | } 35 | setOpenRecoveryCodes(true) 36 | } 37 | 38 | return ( 39 | <> 40 | 44 | 49 | setOpenTOTP(true)}> 50 | 54 | 58 | {isTOTP ? ( 59 | 60 | ) : ( 61 | 62 | )} 63 | 64 | 65 | {isTOTP ? 'Enable' : 'Disable'} 66 | 67 | 68 | } 69 | /> 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | Click to generate

} 81 | className={styles.title} 82 | /> 83 | 84 | 85 | 86 |
87 |
88 |
89 | 90 | 91 | 92 | 93 | ) 94 | } 95 | 96 | export default TwoFactors 97 | -------------------------------------------------------------------------------- /src/containers/Post/PostList.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useCallback } from 'react' 2 | import { useLazyQuery, useMutation } from '@apollo/client' 3 | import { useSnackbar } from 'notistack' 4 | import { 5 | POSTS, 6 | DELETE_ONE_POST, 7 | BATCH_DELETE_POSTS, 8 | UPDATE_ONE_POST, 9 | CREATE_POST_STATISTICS, 10 | } from './typeDefs' 11 | import { 12 | Query, 13 | UpdatePostByIdMutation, 14 | PostStatisticsVars, 15 | CreatePostStatisticsMutation, 16 | } from './types' 17 | import PostTable from './components/PostTable' 18 | import { 19 | deletePostOnAlgolia, 20 | deletePostsOnAlgolia, 21 | } from './algolia/algoliaSearch' 22 | 23 | const Post: FC = () => { 24 | const { enqueueSnackbar } = useSnackbar() 25 | 26 | const [fetchPostsByPage, { loading: isFetching, data }] = useLazyQuery( 27 | POSTS, 28 | { 29 | notifyOnNetworkStatusChange: true, 30 | }, 31 | ) 32 | 33 | const fetchFirstData = useCallback(() => { 34 | fetchPostsByPage({ 35 | variables: { 36 | input: { 37 | page: 1, 38 | pageSize: 10, 39 | }, 40 | }, 41 | }) 42 | }, [fetchPostsByPage]) 43 | 44 | const [createPostStatistics] = useMutation< 45 | CreatePostStatisticsMutation, 46 | PostStatisticsVars 47 | >(CREATE_POST_STATISTICS) 48 | 49 | const [updatePostById] = useMutation( 50 | UPDATE_ONE_POST, 51 | { 52 | onCompleted(data) { 53 | const { _id, title, isPublic } = data.updatePostById 54 | enqueueSnackbar(`「${title}」 is ${isPublic ? 'public' : 'hide'}.`, { 55 | variant: 'success', 56 | }) 57 | 58 | createPostStatistics({ 59 | variables: { 60 | input: { 61 | postId: _id, 62 | postName: title, 63 | scenes: `switched to ${isPublic ? 'public' : 'hide'}`, 64 | }, 65 | }, 66 | }) 67 | }, 68 | }, 69 | ) 70 | 71 | const [deletePostById, { loading: isDeleting }] = useMutation( 72 | DELETE_ONE_POST, 73 | { 74 | onCompleted(data) { 75 | const { _id } = data.deletePostById 76 | enqueueSnackbar('Delete success!', { variant: 'success' }) 77 | fetchFirstData() 78 | deletePostOnAlgolia(_id) 79 | }, 80 | }, 81 | ) 82 | 83 | const [deletePosts, { loading: isBatchDeleting }] = useMutation( 84 | BATCH_DELETE_POSTS, 85 | { 86 | onCompleted(data) { 87 | const { ids } = data.deletePosts 88 | enqueueSnackbar('Delete success!', { variant: 'success' }) 89 | fetchFirstData() 90 | deletePostsOnAlgolia(ids) 91 | }, 92 | }, 93 | ) 94 | 95 | useEffect(() => { 96 | fetchFirstData() 97 | }, [fetchFirstData]) 98 | 99 | return ( 100 | 113 | ) 114 | } 115 | 116 | export default Post 117 | -------------------------------------------------------------------------------- /src/containers/Music/LiveTour/LiveTour.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { useQuery, useMutation } from '@apollo/client' 3 | import { useSnackbar } from 'notistack' 4 | import { 5 | LIVE_TOURS, 6 | CREATE_ONE_LIVE_TOUR, 7 | UPDATE_ONE_LIVE_TOUR, 8 | DELETE_ONE_LIVE_TOUR, 9 | BATCH_DELETE_LIVE_TOUR, 10 | } from './typeDefs' 11 | import { ILiveTour, Query } from './types' 12 | import LiveTourTable from './components/LiveTourTable' 13 | 14 | const LiveTour: FC = () => { 15 | const { enqueueSnackbar } = useSnackbar() 16 | 17 | const { loading: isFetching, data } = useQuery(LIVE_TOURS, { 18 | notifyOnNetworkStatusChange: true, 19 | }) 20 | 21 | const [createLiveTour] = useMutation(CREATE_ONE_LIVE_TOUR, { 22 | update(proxy, { data: { createLiveTour } }) { 23 | const data = proxy.readQuery({ query: LIVE_TOURS }) 24 | 25 | if (data) { 26 | proxy.writeQuery({ 27 | query: LIVE_TOURS, 28 | data: { 29 | ...data, 30 | getLiveTours: [createLiveTour, ...data.getLiveTours], 31 | }, 32 | }) 33 | } 34 | }, 35 | 36 | onCompleted() { 37 | enqueueSnackbar('Create success!', { variant: 'success' }) 38 | }, 39 | onError() {}, 40 | }) 41 | 42 | const [updateLiveTourById] = useMutation(UPDATE_ONE_LIVE_TOUR, { 43 | onCompleted() { 44 | enqueueSnackbar('Update success!', { variant: 'success' }) 45 | }, 46 | }) 47 | 48 | const [deleteLiveTourById, { loading: isDeleting }] = useMutation( 49 | DELETE_ONE_LIVE_TOUR, 50 | { 51 | update(proxy, { data: { deleteLiveTourById } }) { 52 | const data = proxy.readQuery({ query: LIVE_TOURS }) 53 | 54 | if (data) { 55 | proxy.writeQuery({ 56 | query: LIVE_TOURS, 57 | data: { 58 | getLiveTours: data.getLiveTours.filter( 59 | (liveTour: ILiveTour) => 60 | liveTour._id !== deleteLiveTourById._id, 61 | ), 62 | }, 63 | }) 64 | } 65 | }, 66 | onCompleted() { 67 | enqueueSnackbar('Delete success!', { variant: 'success' }) 68 | }, 69 | onError() {}, 70 | }, 71 | ) 72 | 73 | const [deleteLiveTours, { loading: isBatchDeleting }] = useMutation( 74 | BATCH_DELETE_LIVE_TOUR, 75 | { 76 | update(proxy, { data: { deleteLiveTours } }) { 77 | const data = proxy.readQuery({ query: LIVE_TOURS }) 78 | 79 | if (data) { 80 | proxy.writeQuery({ 81 | query: LIVE_TOURS, 82 | data: { 83 | getLiveTours: data.getLiveTours.filter( 84 | (liveTour: ILiveTour) => 85 | !deleteLiveTours.ids.includes(liveTour._id), 86 | ), 87 | }, 88 | }) 89 | } 90 | }, 91 | onCompleted() { 92 | enqueueSnackbar('Delete success!', { variant: 'success' }) 93 | }, 94 | onError() {}, 95 | }, 96 | ) 97 | 98 | return ( 99 | 109 | ) 110 | } 111 | 112 | export default LiveTour 113 | -------------------------------------------------------------------------------- /src/containers/Settings/Security/components/RecoveryCodes/RecoveryCodes.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState, useEffect } from 'react' 2 | import { useMutation } from '@apollo/client' 3 | import { 4 | Dialog, 5 | Button, 6 | DialogTitle, 7 | DialogContent, 8 | DialogActions, 9 | CircularProgress, 10 | } from '@material-ui/core' 11 | import { DateTime } from 'luxon' 12 | import CopyToClipboard from 'react-copy-to-clipboard' 13 | import Transition from 'src/components/Transition/Transition' 14 | import { generateFile } from 'src/shared/utils' 15 | import { RECOVERY_CODES_FILE_NAME } from 'src/shared/constants' 16 | import { CREATE_RECOVERY_CODES } from '../../typeDefs' 17 | import styles from './recoveryCode.module.scss' 18 | 19 | interface Props { 20 | setOpen: Function 21 | open: boolean 22 | } 23 | 24 | const RecoveryCodes: FC = ({ setOpen, open }) => { 25 | const [copyTxt, setCopyTxt] = useState('Copy') 26 | const [recoveryCodes, setRecoveryCodes] = useState([]) 27 | 28 | const onClose = () => { 29 | setOpen(false) 30 | } 31 | 32 | const [createRecoveryCodes, { loading }] = useMutation(CREATE_RECOVERY_CODES) 33 | 34 | useEffect(() => { 35 | const fetchTOTPAndRecoveryCodes = async () => { 36 | const res = await createRecoveryCodes() 37 | setRecoveryCodes(res.data.createRecoveryCodes.recoveryCodes) 38 | } 39 | 40 | if (open) fetchTOTPAndRecoveryCodes() 41 | }, [createRecoveryCodes, open]) 42 | 43 | return ( 44 | 52 | Save your backup codes 53 | 54 | {loading ? ( 55 |
56 | 57 |
58 | ) : ( 59 | <> 60 |
61 |
    62 | {recoveryCodes.map((recoveryCodes) => ( 63 |
  • 64 | 65 | {recoveryCodes} 66 |
  • 67 | ))} 68 |
69 |
70 |
    71 |
  • You can only use each backup code once.
  • 72 |
  • 73 | These codes were generated on: {DateTime.now().toFormat('DD')}. 74 |
  • 75 |
76 | 77 | )} 78 |
79 | 80 | 83 | 92 | setCopyTxt('Copied!')} 95 | > 96 | 99 | 100 | 101 |
102 | ) 103 | } 104 | 105 | export default RecoveryCodes 106 | -------------------------------------------------------------------------------- /src/containers/Music/BestAlbum/BestAlbum.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { useQuery, useMutation } from '@apollo/client' 3 | import { useSnackbar } from 'notistack' 4 | import { 5 | BEST_ALBUMS, 6 | CREATE_ONE_BEST_ALBUM, 7 | UPDATE_ONE_BEST_ALBUM, 8 | DELETE_ONE_BEST_ALBUM, 9 | BATCH_DELETE_BEST_ALBUMS, 10 | } from './typeDefs' 11 | import { IBestAlbum, Query } from './types' 12 | import BestAlbumTable from './components/BestAlbumTable' 13 | 14 | const BestAlbum: FC = () => { 15 | const { enqueueSnackbar } = useSnackbar() 16 | 17 | const { loading: isFetching, data } = useQuery(BEST_ALBUMS, { 18 | notifyOnNetworkStatusChange: true, 19 | }) 20 | 21 | const [createBestAlbum] = useMutation(CREATE_ONE_BEST_ALBUM, { 22 | update(proxy, { data: { createBestAlbum } }) { 23 | const data = proxy.readQuery({ query: BEST_ALBUMS }) 24 | 25 | if (data) { 26 | proxy.writeQuery({ 27 | query: BEST_ALBUMS, 28 | data: { 29 | ...data, 30 | getBestAlbums: [createBestAlbum, ...data.getBestAlbums], 31 | }, 32 | }) 33 | } 34 | }, 35 | 36 | onCompleted() { 37 | enqueueSnackbar('Create success!', { variant: 'success' }) 38 | }, 39 | onError() {}, 40 | }) 41 | 42 | const [updateBestAlbumById] = useMutation(UPDATE_ONE_BEST_ALBUM, { 43 | onCompleted() { 44 | enqueueSnackbar('Update success!', { variant: 'success' }) 45 | }, 46 | }) 47 | 48 | const [deleteBestAlbumById, { loading: isDeleting }] = useMutation( 49 | DELETE_ONE_BEST_ALBUM, 50 | { 51 | update(proxy, { data: { deleteBestAlbumById } }) { 52 | const data = proxy.readQuery({ query: BEST_ALBUMS }) 53 | 54 | if (data) { 55 | proxy.writeQuery({ 56 | query: BEST_ALBUMS, 57 | data: { 58 | getBestAlbums: data.getBestAlbums.filter( 59 | (bestAlbum: IBestAlbum) => 60 | bestAlbum._id !== deleteBestAlbumById._id, 61 | ), 62 | }, 63 | }) 64 | } 65 | }, 66 | onCompleted() { 67 | enqueueSnackbar('Delete success!', { variant: 'success' }) 68 | }, 69 | onError() {}, 70 | }, 71 | ) 72 | 73 | const [deleteBestAlbums, { loading: isBatchDeleting }] = useMutation( 74 | BATCH_DELETE_BEST_ALBUMS, 75 | { 76 | update(proxy, { data: { deleteBestAlbums } }) { 77 | const data = proxy.readQuery({ query: BEST_ALBUMS }) 78 | 79 | if (data) { 80 | proxy.writeQuery({ 81 | query: BEST_ALBUMS, 82 | data: { 83 | getBestAlbums: data.getBestAlbums.filter( 84 | (bestAlbum: IBestAlbum) => 85 | !deleteBestAlbums.ids.includes(bestAlbum._id), 86 | ), 87 | }, 88 | }) 89 | } 90 | }, 91 | onCompleted() { 92 | enqueueSnackbar('Delete success!', { variant: 'success' }) 93 | }, 94 | onError() {}, 95 | }, 96 | ) 97 | 98 | return ( 99 | 109 | ) 110 | } 111 | 112 | export default BestAlbum 113 | -------------------------------------------------------------------------------- /src/components/Move/Move.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { MUIDataTableMeta } from 'mui-datatables' 3 | import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state' 4 | import { MoreVert } from '@material-ui/icons' 5 | import { Menu, MenuItem } from '@material-ui/core' 6 | 7 | interface Props { 8 | dataSource: any[] 9 | tableMeta: MUIDataTableMeta 10 | exchangePosition: Function 11 | } 12 | 13 | const Move: FC = ({ dataSource, tableMeta, exchangePosition }) => { 14 | const move = ( 15 | curId: string, 16 | nextId: string, 17 | curWeight: number, 18 | nextWeight: number, 19 | closePoper: Function, 20 | ) => { 21 | closePoper() 22 | 23 | exchangePosition({ 24 | variables: { 25 | input: { 26 | id: curId, 27 | exchangedId: nextId, 28 | weight: curWeight, 29 | exchangedWeight: nextWeight, 30 | }, 31 | }, 32 | }) 33 | } 34 | 35 | const curId = tableMeta.rowData[0] 36 | const curWeight = tableMeta.rowData[1] 37 | 38 | const prev = tableMeta.tableData[tableMeta.rowIndex - 1] 39 | const next = tableMeta.tableData[tableMeta.rowIndex + 1] 40 | const top = tableMeta.tableData[0] 41 | 42 | // @ts-ignore 43 | const prevId = prev && prev[0] 44 | // @ts-ignore 45 | const prevWeight = prev && prev[1] 46 | 47 | // @ts-ignore 48 | const nextId = next && next[0] 49 | // @ts-ignore 50 | const nextWeight = next && next[1] 51 | 52 | // @ts-ignore 53 | const topId = top[0] 54 | // @ts-ignore 55 | const topWeight = top[1] 56 | 57 | return ( 58 | <> 59 | {dataSource.length < 2 || ( 60 | 61 | {(popupState) => ( 62 | <> 63 | 67 | 68 | 69 | {curWeight !== dataSource[0].weight ? ( 70 | 72 | move(curId, topId, curWeight, topWeight, popupState.close) 73 | } 74 | > 75 | Move to the top 76 | 77 | ) : null} 78 | 79 | {curWeight !== dataSource[0].weight ? ( 80 | 82 | move( 83 | curId, 84 | prevId, 85 | curWeight, 86 | prevWeight, 87 | popupState.close, 88 | ) 89 | } 90 | > 91 | Move up 92 | 93 | ) : null} 94 | 95 | {curWeight !== 1 ? ( 96 | 98 | move( 99 | curId, 100 | nextId, 101 | curWeight, 102 | nextWeight, 103 | popupState.close, 104 | ) 105 | } 106 | > 107 | Move down 108 | 109 | ) : null} 110 | 111 | 112 | )} 113 | 114 | )} 115 | 116 | ) 117 | } 118 | 119 | export default Move 120 | -------------------------------------------------------------------------------- /src/containers/Home/OpenSource/OpenSource.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { useQuery, useMutation } from '@apollo/client' 3 | import { useSnackbar } from 'notistack' 4 | import { 5 | OPEN_SOURCES, 6 | CREATE_ONE_OPEN_SOURCE, 7 | UPDATE_ONE_OPEN_SOURCE, 8 | DELETE_ONE_OPEN_SOURCE, 9 | BATCH_DELETE_OPEN_SOURCE, 10 | } from './typeDefs' 11 | import { IOpenSource, Query } from './types' 12 | import OpenSourceTable from './components/OpenSourceTable' 13 | 14 | const OpenSource: FC = () => { 15 | const { enqueueSnackbar } = useSnackbar() 16 | 17 | const { loading: isFetching, data } = useQuery(OPEN_SOURCES, { 18 | notifyOnNetworkStatusChange: true, 19 | }) 20 | 21 | const [createOpenSource] = useMutation(CREATE_ONE_OPEN_SOURCE, { 22 | update(proxy, { data: { createOpenSource } }) { 23 | const data = proxy.readQuery({ query: OPEN_SOURCES }) 24 | 25 | if (data) { 26 | proxy.writeQuery({ 27 | query: OPEN_SOURCES, 28 | data: { 29 | ...data, 30 | getOpenSources: [createOpenSource, ...data.getOpenSources], 31 | }, 32 | }) 33 | } 34 | }, 35 | 36 | onCompleted() { 37 | enqueueSnackbar('Create success!', { variant: 'success' }) 38 | }, 39 | onError() {}, 40 | }) 41 | 42 | const [updateOpenSourceById] = useMutation(UPDATE_ONE_OPEN_SOURCE, { 43 | onCompleted() { 44 | enqueueSnackbar('Update success!', { variant: 'success' }) 45 | }, 46 | }) 47 | 48 | const [deleteOpenSourceById, { loading: isDeleting }] = useMutation( 49 | DELETE_ONE_OPEN_SOURCE, 50 | { 51 | update(proxy, { data: { deleteOpenSourceById } }) { 52 | const data = proxy.readQuery({ query: OPEN_SOURCES }) 53 | 54 | if (data) { 55 | proxy.writeQuery({ 56 | query: OPEN_SOURCES, 57 | data: { 58 | getOpenSources: data.getOpenSources.filter( 59 | (openSource: IOpenSource) => 60 | openSource._id !== deleteOpenSourceById._id, 61 | ), 62 | }, 63 | }) 64 | } 65 | }, 66 | onCompleted() { 67 | enqueueSnackbar('Delete success!', { variant: 'success' }) 68 | }, 69 | onError() {}, 70 | }, 71 | ) 72 | 73 | const [deleteOpenSources, { loading: isBatchDeleting }] = useMutation( 74 | BATCH_DELETE_OPEN_SOURCE, 75 | { 76 | update(proxy, { data: { deleteOpenSources } }) { 77 | const data = proxy.readQuery({ query: OPEN_SOURCES }) 78 | 79 | if (data) { 80 | proxy.writeQuery({ 81 | query: OPEN_SOURCES, 82 | data: { 83 | getOpenSources: data.getOpenSources.filter( 84 | (openSource: IOpenSource) => 85 | !deleteOpenSources.ids.includes(openSource._id), 86 | ), 87 | }, 88 | }) 89 | } 90 | }, 91 | onCompleted() { 92 | enqueueSnackbar('Delete success!', { variant: 'success' }) 93 | }, 94 | onError() {}, 95 | }, 96 | ) 97 | 98 | return ( 99 | 109 | ) 110 | } 111 | 112 | export default OpenSource 113 | -------------------------------------------------------------------------------- /src/pages/Auth/Auth.module.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Whitney-400'; 3 | src: url('https://edge.yancey.app/beg/Whitney-400.woff') format('woff'); 4 | } 5 | 6 | @font-face { 7 | font-family: 'Whitney-500'; 8 | src: url('https://edge.yancey.app/beg/Whitney-500.woff') format('woff'); 9 | } 10 | 11 | @font-face { 12 | font-family: 'Whitney-600'; 13 | src: url('https://edge.yancey.app/beg/Whitney-600.woff') format('woff'); 14 | } 15 | 16 | .loginWrapper { 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | width: 100vw; 21 | height: 100vh; 22 | font-family: 'Whitney-400'; 23 | background-repeat: no-repeat; 24 | background-position: center center; 25 | background-size: cover; 26 | text-rendering: optimizeLegibility; 27 | } 28 | 29 | .header { 30 | margin-bottom: 8px; 31 | text-align: center; 32 | font-size: 26px; 33 | font-weight: 400; 34 | line-height: 32px; 35 | color: #ffffff; 36 | } 37 | 38 | .headerExtra { 39 | text-align: center; 40 | color: #72767d; 41 | font-family: 'Whitney-500'; 42 | font-size: 18px; 43 | font-weight: 500; 44 | line-height: 22px; 45 | } 46 | 47 | .loginForm { 48 | width: 480px; 49 | padding: 40px; 50 | font-size: 18px; 51 | color: #72767d; 52 | background: #36393f; 53 | box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.2); 54 | border-radius: 5px; 55 | } 56 | 57 | .label { 58 | display: block; 59 | margin-bottom: 20px; 60 | font-family: 'Whitney-600'; 61 | font-size: 12px; 62 | font-weight: 600; 63 | color: #8e9297; 64 | text-transform: uppercase; 65 | &:first-of-type { 66 | margin-top: 20px; 67 | } 68 | } 69 | 70 | .inputTxt { 71 | margin-top: 6px; 72 | padding: 10px; 73 | height: 40px; 74 | font-size: 16px; 75 | width: 100%; 76 | border-radius: 3px; 77 | color: #dcddde; 78 | background-color: rgba(#000000, 0.1); 79 | border: 1px solid rgba(#000000, 0.3); 80 | outline: none; 81 | &:hover { 82 | border-color: #000000; 83 | } 84 | &:focus { 85 | border-color: #7289da; 86 | } 87 | } 88 | 89 | .submitBtn { 90 | width: 100%; 91 | height: 44px; 92 | font-family: 'Whitney-600'; 93 | font-size: 16px; 94 | color: #fff; 95 | background-color: #7289da; 96 | border: none; 97 | border-radius: 3px; 98 | user-select: none; 99 | outline: none; 100 | cursor: pointer; 101 | transition: 300ms ease; 102 | &:hover { 103 | transition: 300ms ease; 104 | background-color: #677bc4; 105 | } 106 | } 107 | 108 | .link { 109 | margin: 8px 0; 110 | font-family: 'Whitney-600'; 111 | font-size: 14px; 112 | font-weight: 500; 113 | color: #7289da; 114 | cursor: pointer; 115 | text-decoration: none; 116 | text-transform: initial; 117 | &:hover { 118 | text-decoration: underline; 119 | } 120 | a { 121 | text-decoration: none; 122 | color: inherit; 123 | } 124 | } 125 | 126 | .registerTip { 127 | display: inline-block; 128 | margin-top: 8px; 129 | font-size: 14px; 130 | } 131 | 132 | .license { 133 | margin-top: 20px; 134 | font-size: 12px; 135 | color: #72767d; 136 | a { 137 | color: #7289da; 138 | text-decoration: none; 139 | &:hover { 140 | text-decoration: underline; 141 | } 142 | } 143 | } 144 | 145 | .error { 146 | color: #f04747; 147 | } 148 | 149 | .errorMsg { 150 | font-style: italic; 151 | text-transform: initial; 152 | } 153 | 154 | .errorInputTxt { 155 | border-color: #f04747; 156 | &:hover, 157 | &:focus { 158 | border-color: #f04747; 159 | } 160 | } 161 | 162 | .copyright { 163 | position: absolute; 164 | bottom: 1rem; 165 | } 166 | -------------------------------------------------------------------------------- /src/containers/Music/YanceyMusic/YanceyMusic.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { useQuery, useMutation } from '@apollo/client' 3 | import { useSnackbar } from 'notistack' 4 | import { 5 | YANCEY_MUSIC, 6 | CREATE_ONE_YANCEY_MUSIC, 7 | UPDATE_ONE_YANCEY_MUSIC, 8 | DELETE_ONE_YANCEY_MUSIC, 9 | BATCH_DELETE_YANCEY_MUSIC, 10 | } from './typeDefs' 11 | import { IYanceyMusic, Query } from './types' 12 | import YanceyMusicTable from './components/YanceyMusicTable' 13 | 14 | const YanceyMusic: FC = () => { 15 | const { enqueueSnackbar } = useSnackbar() 16 | 17 | const { loading: isFetching, data } = useQuery(YANCEY_MUSIC, { 18 | notifyOnNetworkStatusChange: true, 19 | }) 20 | 21 | const [createYanceyMusic] = useMutation(CREATE_ONE_YANCEY_MUSIC, { 22 | update(proxy, { data: { createYanceyMusic } }) { 23 | const data = proxy.readQuery({ query: YANCEY_MUSIC }) 24 | 25 | if (data) { 26 | proxy.writeQuery({ 27 | query: YANCEY_MUSIC, 28 | data: { 29 | ...data, 30 | getYanceyMusic: [createYanceyMusic, ...data.getYanceyMusic], 31 | }, 32 | }) 33 | } 34 | }, 35 | 36 | onCompleted() { 37 | enqueueSnackbar('Create success!', { variant: 'success' }) 38 | }, 39 | onError() {}, 40 | }) 41 | 42 | const [updateYanceyMusicById] = useMutation(UPDATE_ONE_YANCEY_MUSIC, { 43 | onCompleted() { 44 | enqueueSnackbar('Update success!', { variant: 'success' }) 45 | }, 46 | }) 47 | 48 | const [deleteYanceyMusicById, { loading: isDeleting }] = useMutation( 49 | DELETE_ONE_YANCEY_MUSIC, 50 | { 51 | update(proxy, { data: { deleteYanceyMusicById } }) { 52 | const data = proxy.readQuery({ query: YANCEY_MUSIC }) 53 | 54 | if (data) { 55 | proxy.writeQuery({ 56 | query: YANCEY_MUSIC, 57 | data: { 58 | getYanceyMusic: data.getYanceyMusic.filter( 59 | (yanceyMusic: IYanceyMusic) => 60 | yanceyMusic._id !== deleteYanceyMusicById._id, 61 | ), 62 | }, 63 | }) 64 | } 65 | }, 66 | onCompleted() { 67 | enqueueSnackbar('Delete success!', { variant: 'success' }) 68 | }, 69 | onError() {}, 70 | }, 71 | ) 72 | 73 | const [deleteYanceyMusic, { loading: isBatchDeleting }] = useMutation( 74 | BATCH_DELETE_YANCEY_MUSIC, 75 | { 76 | update(proxy, { data: { deleteYanceyMusic } }) { 77 | const data = proxy.readQuery({ query: YANCEY_MUSIC }) 78 | 79 | if (data) { 80 | proxy.writeQuery({ 81 | query: YANCEY_MUSIC, 82 | data: { 83 | getYanceyMusic: data.getYanceyMusic.filter( 84 | (yanceyMusic: IYanceyMusic) => 85 | !deleteYanceyMusic.ids.includes(yanceyMusic._id), 86 | ), 87 | }, 88 | }) 89 | } 90 | }, 91 | onCompleted() { 92 | enqueueSnackbar('Delete success!', { variant: 'success' }) 93 | }, 94 | onError() {}, 95 | }, 96 | ) 97 | 98 | return ( 99 | 109 | ) 110 | } 111 | 112 | export default YanceyMusic 113 | --------------------------------------------------------------------------------