diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4a7ea30 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..b38db2f --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +node_modules/ +build/ diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..d691799 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,26 @@ +module.exports = { + env: { + browser: true, + es2021: true + }, + extends: ["plugin:react/recommended", "plugin:prettier/recommended"], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaFeatures: { + jsx: true + }, + ecmaVersion: 12, + sourceType: "module" + }, + settings: { + react: { + version: "detect" + } + }, + plugins: ["react", "@typescript-eslint"], + rules: { + "react-hooks/exhaustive-deps": "off", + "react/react-in-jsx-scope": "off", + "react/no-unescaped-entities": "off" + } +}; diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..6227023 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,44 @@ +name: build and deploy to GitHub Pages + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup Node + uses: actions/setup-node@v2.1.2 + with: + node-version: '12' + + - name: Get yarn cache + id: yarn-cache + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Build + run: | + yarn install --frozen-lockfile + yarn build + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: dist + cname: portfolio.zxh.io diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90c657b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.log +.DS_Store +.cache/ +node_modules +dist +.npmrc +.yarnrc +.idea diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..d2ae35e --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint-staged diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..02db67b --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1 @@ +trailingComma: none diff --git a/README.md b/README.md index 33fda9e..6e44f80 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ +# The Tech Devs + +Official portfolio website simulating macOS's GUI: http://thetechdevs.com + +Powered by [React](https://reactjs.org/) + [React Redux](https://react-redux.js.org/) + [Tailwind CSS](https://tailwindcss.com/) + [TypeScript](https://www.typescriptlang.org/) + [Vite](https://vitejs.dev/). + +![day](./public/screenshots/day.png) +![night](./public/screenshots/night.png) +

logo @@ -27,6 +36,13 @@ - Firebase - Hosting (cPanel, Github pages, Netlify, Firebase, Vercel, Heroku, etc.) +## Credits + +- [macOS Big Sur](https://www.apple.com/in/macos/big-sur/) +- [macOS Catalina](https://www.apple.com/bw/macos/catalina/) +- [macOS Icon Gallery](https://www.macosicongallery.com/) +- [sindresorhus/file-icon-cli](https://github.com/sindresorhus/file-icon-cli) + ### Members

diff --git a/index.html b/index.html new file mode 100644 index 0000000..88c026b --- /dev/null +++ b/index.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + The Tech Devs + + +

+ + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..644e813 --- /dev/null +++ b/package.json @@ -0,0 +1,58 @@ +{ + "name": "playground-macos", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc && vite build", + "deploy": "yarn build && gh-pages -d dist", + "dev": "vite", + "dev:host": "vite --host", + "lint": "eslint --ext .js,.ts,.tsx ./", + "prepare": "husky install", + "serve": "vite preview" + }, + "lint-staged": { + "*.{js,ts,tsx}": "eslint --fix", + "package.json": "sort-package-json" + }, + "dependencies": { + "@rooks/use-raf": "^4.11.2", + "date-fns": "2.27.0", + "framer-motion": "^4.1.17", + "nightwind": "^1.1.12", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-icons": "^4.3.1", + "react-markdown": "^7.1.1", + "react-rangeslider": "2.2.0", + "react-redux": "^7.2.6", + "react-rnd": "^10.3.5", + "react-syntax-highlighter": "^15.4.5", + "react-webcam": "6.0.0", + "redux": "^4.1.2", + "remark-gfm": "^3.0.1", + "web-vitals": "^1.0.1" + }, + "devDependencies": { + "@types/node": "^16.11.11", + "@types/react": "^17.0.37", + "@types/react-dom": "^17.0.11", + "@typescript-eslint/eslint-plugin": "^5.10.1", + "@typescript-eslint/parser": "^5.10.1", + "@vitejs/plugin-react": "^1.1.4", + "autoprefixer": "^10.4.2", + "eslint": "^8.8.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-react": "^7.28.0", + "gh-pages": "^3.2.3", + "husky": "^7.0.4", + "lint-staged": "^11.2.6", + "postcss": "^8.4.5", + "prettier": "^2.5.1", + "sort-package-json": "^1.53.1", + "tailwindcss": "^3.0.18", + "typescript": "^4.5.2", + "vite": "^2.7.13" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..5cbc2c7 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/CNAME b/public/CNAME similarity index 100% rename from CNAME rename to public/CNAME diff --git a/public/img/icons/bear.png b/public/img/icons/bear.png new file mode 100644 index 0000000..316dcfa Binary files /dev/null and b/public/img/icons/bear.png differ diff --git a/public/img/icons/facetime.png b/public/img/icons/facetime.png new file mode 100644 index 0000000..b6514f0 Binary files /dev/null and b/public/img/icons/facetime.png differ diff --git a/public/img/icons/github.png b/public/img/icons/github.png new file mode 100644 index 0000000..e742a08 Binary files /dev/null and b/public/img/icons/github.png differ diff --git a/public/img/icons/launchpad.png b/public/img/icons/launchpad.png new file mode 100644 index 0000000..0a228a0 Binary files /dev/null and b/public/img/icons/launchpad.png differ diff --git a/public/img/icons/launchpad/cube.png b/public/img/icons/launchpad/cube.png new file mode 100644 index 0000000..453b9c5 Binary files /dev/null and b/public/img/icons/launchpad/cube.png differ diff --git a/public/img/icons/launchpad/fishmail.png b/public/img/icons/launchpad/fishmail.png new file mode 100644 index 0000000..bb7a7b0 Binary files /dev/null and b/public/img/icons/launchpad/fishmail.png differ diff --git a/public/img/icons/launchpad/flint.png b/public/img/icons/launchpad/flint.png new file mode 100644 index 0000000..38958b1 Binary files /dev/null and b/public/img/icons/launchpad/flint.png differ diff --git a/public/img/icons/launchpad/gungnir.png b/public/img/icons/launchpad/gungnir.png new file mode 100644 index 0000000..63617f9 Binary files /dev/null and b/public/img/icons/launchpad/gungnir.png differ diff --git a/public/img/icons/launchpad/icon.png b/public/img/icons/launchpad/icon.png new file mode 100644 index 0000000..d5b0a11 Binary files /dev/null and b/public/img/icons/launchpad/icon.png differ diff --git a/public/img/icons/launchpad/meta.png b/public/img/icons/launchpad/meta.png new file mode 100644 index 0000000..83b7b7c Binary files /dev/null and b/public/img/icons/launchpad/meta.png differ diff --git a/public/img/icons/launchpad/notebook.png b/public/img/icons/launchpad/notebook.png new file mode 100644 index 0000000..b2135d3 Binary files /dev/null and b/public/img/icons/launchpad/notebook.png differ diff --git a/public/img/icons/launchpad/resume.png b/public/img/icons/launchpad/resume.png new file mode 100644 index 0000000..d83a20a Binary files /dev/null and b/public/img/icons/launchpad/resume.png differ diff --git a/public/img/icons/launchpad/rl.png b/public/img/icons/launchpad/rl.png new file mode 100644 index 0000000..0c20c78 Binary files /dev/null and b/public/img/icons/launchpad/rl.png differ diff --git a/public/img/icons/launchpad/zelda.png b/public/img/icons/launchpad/zelda.png new file mode 100644 index 0000000..b4702ea Binary files /dev/null and b/public/img/icons/launchpad/zelda.png differ diff --git a/public/img/icons/mail.png b/public/img/icons/mail.png new file mode 100644 index 0000000..64ddfc9 Binary files /dev/null and b/public/img/icons/mail.png differ diff --git a/public/img/icons/safari.png b/public/img/icons/safari.png new file mode 100644 index 0000000..48bb966 Binary files /dev/null and b/public/img/icons/safari.png differ diff --git a/public/img/icons/terminal.png b/public/img/icons/terminal.png new file mode 100644 index 0000000..5e54d1f Binary files /dev/null and b/public/img/icons/terminal.png differ diff --git a/public/img/icons/vscode.png b/public/img/icons/vscode.png new file mode 100644 index 0000000..aa7b880 Binary files /dev/null and b/public/img/icons/vscode.png differ diff --git a/public/img/sites/arxiv.png b/public/img/sites/arxiv.png new file mode 100644 index 0000000..a6ca65d Binary files /dev/null and b/public/img/sites/arxiv.png differ diff --git a/public/img/sites/astral.svg b/public/img/sites/astral.svg new file mode 100644 index 0000000..4c0991d --- /dev/null +++ b/public/img/sites/astral.svg @@ -0,0 +1 @@ + diff --git a/public/img/sites/bilibili.svg b/public/img/sites/bilibili.svg new file mode 100644 index 0000000..9f04890 --- /dev/null +++ b/public/img/sites/bilibili.svg @@ -0,0 +1 @@ + diff --git a/public/img/sites/dribbble.svg b/public/img/sites/dribbble.svg new file mode 100644 index 0000000..d11fa6a --- /dev/null +++ b/public/img/sites/dribbble.svg @@ -0,0 +1 @@ + diff --git a/public/img/sites/facebook.svg b/public/img/sites/facebook.svg new file mode 100644 index 0000000..8b598e0 --- /dev/null +++ b/public/img/sites/facebook.svg @@ -0,0 +1 @@ + diff --git a/public/img/sites/gitee.svg b/public/img/sites/gitee.svg new file mode 100644 index 0000000..85b60d5 --- /dev/null +++ b/public/img/sites/gitee.svg @@ -0,0 +1 @@ + diff --git a/public/img/sites/github.svg b/public/img/sites/github.svg new file mode 100644 index 0000000..76fbfee --- /dev/null +++ b/public/img/sites/github.svg @@ -0,0 +1 @@ + diff --git a/public/img/sites/gmail.svg b/public/img/sites/gmail.svg new file mode 100644 index 0000000..5845d8b --- /dev/null +++ b/public/img/sites/gmail.svg @@ -0,0 +1 @@ + diff --git a/public/img/sites/hacker.svg b/public/img/sites/hacker.svg new file mode 100644 index 0000000..9bef4b1 --- /dev/null +++ b/public/img/sites/hacker.svg @@ -0,0 +1 @@ + diff --git a/public/img/sites/leetcode.svg b/public/img/sites/leetcode.svg new file mode 100644 index 0000000..4a06755 --- /dev/null +++ b/public/img/sites/leetcode.svg @@ -0,0 +1 @@ + diff --git a/public/img/sites/linkedin.svg b/public/img/sites/linkedin.svg new file mode 100644 index 0000000..25792c2 --- /dev/null +++ b/public/img/sites/linkedin.svg @@ -0,0 +1 @@ + diff --git a/public/img/sites/oh-vue-icons.svg b/public/img/sites/oh-vue-icons.svg new file mode 100644 index 0000000..6aec65e --- /dev/null +++ b/public/img/sites/oh-vue-icons.svg @@ -0,0 +1 @@ + diff --git a/public/img/sites/pinterest.svg b/public/img/sites/pinterest.svg new file mode 100644 index 0000000..5467978 --- /dev/null +++ b/public/img/sites/pinterest.svg @@ -0,0 +1 @@ + diff --git a/public/img/sites/reddit.svg b/public/img/sites/reddit.svg new file mode 100644 index 0000000..e5a66c2 --- /dev/null +++ b/public/img/sites/reddit.svg @@ -0,0 +1 @@ + diff --git a/public/img/sites/zhihu.jpeg b/public/img/sites/zhihu.jpeg new file mode 100644 index 0000000..b00341f Binary files /dev/null and b/public/img/sites/zhihu.jpeg differ diff --git a/public/img/ui/avatar.png b/public/img/ui/avatar.png new file mode 100644 index 0000000..616b726 Binary files /dev/null and b/public/img/ui/avatar.png differ diff --git a/public/img/ui/wallpaper-day.jpg b/public/img/ui/wallpaper-day.jpg new file mode 100644 index 0000000..fea581e Binary files /dev/null and b/public/img/ui/wallpaper-day.jpg differ diff --git a/public/img/ui/wallpaper-night.jpg b/public/img/ui/wallpaper-night.jpg new file mode 100644 index 0000000..d6e9d18 Binary files /dev/null and b/public/img/ui/wallpaper-night.jpg differ diff --git a/public/img/ui/wallpaper.jpg b/public/img/ui/wallpaper.jpg new file mode 100644 index 0000000..893e6eb Binary files /dev/null and b/public/img/ui/wallpaper.jpg differ diff --git a/public/logo/TheTechDevsLogo.png b/public/logo/TheTechDevsLogo.png new file mode 100644 index 0000000..616b726 Binary files /dev/null and b/public/logo/TheTechDevsLogo.png differ diff --git a/public/logo/favicon.ico b/public/logo/favicon.ico new file mode 100644 index 0000000..35c68e8 Binary files /dev/null and b/public/logo/favicon.ico differ diff --git a/public/logo/logo192.png b/public/logo/logo192.png new file mode 100644 index 0000000..0508246 Binary files /dev/null and b/public/logo/logo192.png differ diff --git a/public/logo/logo512.png b/public/logo/logo512.png new file mode 100644 index 0000000..e66a537 Binary files /dev/null and b/public/logo/logo512.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..1f478a2 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "playground-macos", + "name": "The Tech Devs's Portfolio", + "icons": [ + { + "src": "logo/favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo/logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo/logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/public/markdown/about-me.md b/public/markdown/about-me.md new file mode 100644 index 0000000..7857feb --- /dev/null +++ b/public/markdown/about-me.md @@ -0,0 +1,30 @@ +# About Me + +## Biography + +Hey there! I'm ~~a dragon lost in human world~~ now a [Computer Science](https://www.bu.edu/cs/) master's student at [Boston University](https://www.bu.edu/) and machine learning engineer intern at Multimedia Understanding (MMU) department of [Kuaishou](https://www.kuaishou.com/) ([Kwai](https://www.kwai.com/)). I'm also working as a research assistant at Peking University. Before that, I got my bachelor's degree in [Software Engineering](http://sse.tongji.edu.cn/) at [Tongji University](https://www.tongji.edu.cn/). + +I'm trying to find a balance between research and engineering. + +My research interests lie primarily in exploring the capability of machines to be continual and efficient, like meta-learning, few-shot learning and continual learning. I'm also working on multi-modal retrieval for my internship. + +I'm also learning to build machine learning softwares and systems. I'm also learning TypeScript, React and Vue. + + +## Contact + +Contact me by: + +- Email: [thetechdevs@gmail.com](mailto:thetechdevs@gmail.com) +- Github: [@Renovamen](https://github.com/Renovamen) +- Linkedin: [thetechdevs](https://www.linkedin.com/in/thetechdevs) +- 知乎: [@Renovamen](https://www.zhihu.com/people/chao-neng-gui-su) +- Blog: [zxh.io](https://zxh.io) + +## Résumé + +- Normal version: [English](https://zxh.io/files/cv/en/brief.pdf) / [中文](https://zxh.io/files/cv/cn/brief.pdf) + + 中文版的更新很可能不及时 + +- Interesting version: [portfolio.zxh.io](https://portfolio.zxh.io) / [resume.zxh.io](https://resume.zxh.io) diff --git a/public/markdown/about-site.md b/public/markdown/about-site.md new file mode 100644 index 0000000..d64821b --- /dev/null +++ b/public/markdown/about-site.md @@ -0,0 +1,5 @@ +# About This Site + +This site is inspired by macOS [Big Sur](https://www.apple.com/in/macos/big-sur/) and [Catalina](https://www.apple.com/bw/macos/catalina/), developed using [React](https://reactjs.org/), [React Redux](https://react-redux.js.org/) and [Tailwind CSS](https://tailwindcss.com/), and hosted on [Vercel](https://vercel.com/) and [Github Pages](https://pages.github.com/). Some of the icons are generated using [sindresorhus/file-icon-cli](https://github.com/sindresorhus/file-icon-cli). + +The source code is hosted [here](https://github.com/Renovamen/playground-macos). diff --git a/public/markdown/github-stats.md b/public/markdown/github-stats.md new file mode 100644 index 0000000..6a63a54 --- /dev/null +++ b/public/markdown/github-stats.md @@ -0,0 +1,7 @@ +# Github Stats + +My GitHub stats (powered by [github-readme-stats](https://github.com/anuraghazra/github-readme-stats)): + +[![github stats](https://github-readme-stats.vercel.app/api?username=Renovamen&show_icons=true&hide_title=true&hide_border=true)](https://zxh.io) + +[![top langs](https://github-readme-stats.vercel.app/api/top-langs/?username=Renovamen&layout=compact&hide_border=true)](https://zxh.io) diff --git a/public/music/sunflower.mp3 b/public/music/sunflower.mp3 new file mode 100644 index 0000000..182a9a5 Binary files /dev/null and b/public/music/sunflower.mp3 differ diff --git a/public/screenshots/day.png b/public/screenshots/day.png new file mode 100644 index 0000000..0ba5045 Binary files /dev/null and b/public/screenshots/day.png differ diff --git a/public/screenshots/night.png b/public/screenshots/night.png new file mode 100644 index 0000000..0ddb6cd Binary files /dev/null and b/public/screenshots/night.png differ diff --git a/src/components/Launchpad.tsx b/src/components/Launchpad.tsx new file mode 100644 index 0000000..f4983b8 --- /dev/null +++ b/src/components/Launchpad.tsx @@ -0,0 +1,95 @@ +import { useState } from "react"; +import { useSelector } from "react-redux"; +import { BiSearch } from "react-icons/bi"; +import { wallpapers, launchpadApps } from "../configs"; +import type { RootReduxState } from "../types"; + +interface LaunchpadProps { + show: boolean; + toggleLaunchpad: (target: boolean) => void; +} + +const placeholderText = "Search"; + +export default function Launchpad({ show, toggleLaunchpad }: LaunchpadProps) { + const dark = useSelector((state: RootReduxState) => state.dark); + const [searchText, setSearchText] = useState(""); + + const search = () => { + if (searchText === "") return launchpadApps; + const text = searchText.toLowerCase(); + const list = launchpadApps.filter((item) => { + return ( + item.title.toLowerCase().includes(text) || + item.id.toLowerCase().includes(text) + ); + }); + return list; + }; + + const close = show + ? "" + : "opacity-0 invisible transition-opacity duration-200"; + + return ( +
toggleLaunchpad(false)} + > +
+
e.stopPropagation()} + > +
+ +
+ setSearchText(e.target.value)} + /> +
+ +
+ {search().map((app) => ( +
+
+ e.stopPropagation()} + > + {app.title} + + + {app.title} + +
+
+ ))} +
+
+
+ ); +} diff --git a/src/components/Spotlight.tsx b/src/components/Spotlight.tsx new file mode 100644 index 0000000..999cb54 --- /dev/null +++ b/src/components/Spotlight.tsx @@ -0,0 +1,336 @@ +import React, { useRef, useState, useEffect } from "react"; +import type { RefObject } from "react"; +import format from "date-fns/format"; +import { BiSearch } from "react-icons/bi"; +import { apps, launchpadApps } from "../configs"; +import type { LaunchpadData, AppsData } from "../types"; +import { useClickOutside } from "../hooks"; + +const allApps: { [key: string]: (LaunchpadData | AppsData)[] } = { + app: apps, + portfolio: launchpadApps +}; + +const getRandom = (min: number, max: number): number => { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; +}; + +const getRandomDate = (): string => { + const timeStamp = new Date().getTime(); + const randomStamp = getRandom(0, timeStamp); + const date = format(randomStamp, "MM/dd/yyyy"); + return date; +}; + +interface SpotlightProps { + toggleSpotlight: () => void; + openApp: (id: string) => void; + toggleLaunchpad: (target: boolean) => void; + btnRef: RefObject; +} + +export default function Spotlight({ + toggleSpotlight, + openApp, + toggleLaunchpad, + btnRef +}: SpotlightProps) { + const spotlightRef = useRef(null); + + const [selectedIndex, setSelectedIndex] = useState(0); + const [clickedID, setClickedID] = useState(""); + const [doubleClicked, setDoubleClicked] = useState(false); + + const [searchText, setSearchText] = useState(""); + const [curDetails, setCurDetails] = useState(null); + + const [appIdList, setAppIdList] = useState([]); + const [appList, setAppList] = useState(null); + + useClickOutside(spotlightRef, toggleSpotlight, [btnRef]); + + useEffect(() => { + updateAppList(); + // don't show app details when there is no input + if (searchText === "") setCurDetails(null); + }, [searchText]); + + useEffect(() => { + updateCurrentDetails(); + }, [selectedIndex]); + + useEffect(() => { + if (appIdList.length === 0) return; + // find app's index given its id + const newSelectedIndex = appIdList.findIndex((item) => { + return item === clickedID; + }); + // update index + updateHighlight(selectedIndex, newSelectedIndex); + setSelectedIndex(newSelectedIndex); + }, [clickedID]); + + useEffect(() => { + if (doubleClicked) { + launchSelectedApp(); + setDoubleClicked(false); + } + }, [doubleClicked]); + + const search = (type: string) => { + if (searchText === "") return []; + const text = searchText.toLowerCase(); + const list = allApps[type].filter((item: LaunchpadData | AppsData) => { + return ( + item.title.toLowerCase().includes(text) || + item.id.toLowerCase().includes(text) + ); + }); + return list; + }; + + const handleClick = (id: string): void => { + setClickedID(id); + }; + + const handleDoubleClick = (id: string): void => { + setClickedID(id); + setDoubleClicked(true); + }; + + const launchSelectedApp = (): void => { + if (curDetails.type === "app" && !curDetails.link) { + const id = curDetails.id; + if (id === "launchpad") toggleLaunchpad(true); + else openApp(id); + toggleSpotlight(); + } else { + window.open(curDetails.link); + toggleSpotlight(); + } + }; + + const getTypeAppList = (type: string, startIndex: number) => { + const result = search(type); + const typeAppList = []; + const typeAppIdList = []; + + for (const app of result) { + const curIndex = startIndex + typeAppList.length; + const bg = curIndex === 0 ? "bg-blue-500" : "bg-transparent"; + const text = curIndex === 0 ? "text-white" : "text-black"; + + if (curIndex === 0) setCurrentDetailsWithType(app, type); + + typeAppList.push( +
  • handleClick(app.id)} + onDoubleClick={() => handleDoubleClick(app.id)} + > +
    + {app.title} +
    +
    + {app.title} +
    +
  • + ); + typeAppIdList.push(app.id); + } + + return { + appList: typeAppList, + appIdList: typeAppIdList + }; + }; + + const updateAppList = (): void => { + const app = getTypeAppList("app", 0); + const portfolio = getTypeAppList("portfolio", app.appIdList.length); + + const newAppIdList = [...app.appIdList, ...portfolio.appIdList]; + // don't show app details when there is no associating app + if (newAppIdList.length === 0) setCurDetails(null); + + const newAppList = ( +
    + {app.appList.length !== 0 && ( +
    +
    + Applications +
    +
      {app.appList}
    +
    + )} + {portfolio.appList.length !== 0 && ( +
    +
    + Portfolio +
    +
      {portfolio.appList}
    +
    + )} +
    + ); + + setAppIdList(newAppIdList); + setAppList(newAppList); + }; + + const setCurrentDetailsWithType = (app: any, type: string): void => { + const details = app; + details.type = type; + setCurDetails(details); + }; + + const updateCurrentDetails = (): void => { + if (appIdList.length === 0) return; + const appId = appIdList[selectedIndex]; + const elem = document.querySelector(`#spotlight-${appId}`) as HTMLElement; + const id = appId; + const type = elem.dataset.appType as string; + const app = allApps[type].find((item: LaunchpadData | AppsData) => { + return item.id === id; + }); + setCurrentDetailsWithType(app, type); + }; + + const updateHighlight = (prevIndex: number, curIndex: number): void => { + if (appIdList.length === 0) return; + + // remove highlight + const prevAppId = appIdList[prevIndex]; + const prev = document.querySelector( + `#spotlight-${prevAppId}` + ) as HTMLElement; + let classes = prev.className; + classes = classes.replace("text-white", "text-black"); + classes = classes.replace("bg-blue-500", "bg-transparent"); + prev.className = classes; + + // add highlight + const curAppId = appIdList[curIndex]; + const cur = document.querySelector(`#spotlight-${curAppId}`) as HTMLElement; + classes = cur.className; + classes = classes.replace("text-black", "text-white"); + classes = classes.replace("bg-transparent", "bg-blue-500"); + cur.className = classes; + }; + + const handleKeyPress = (e: React.KeyboardEvent): void => { + const keyCode = e.key; + const numApps = appIdList.length; + + // ----------- select next app ----------- + if (keyCode === "ArrowDown" && selectedIndex < numApps - 1) { + updateHighlight(selectedIndex, selectedIndex + 1); + setSelectedIndex(selectedIndex + 1); + } + // ----------- select previous app ----------- + else if (keyCode === "ArrowUp" && selectedIndex > 0) { + updateHighlight(selectedIndex, selectedIndex - 1); + setSelectedIndex(selectedIndex - 1); + } + // ----------- launch app ----------- + else if (keyCode === "Enter") { + if (!curDetails) return; + launchSelectedApp(); + } + }; + + const handleInputChange = (e: React.ChangeEvent): void => { + // update highlighted line + updateHighlight(selectedIndex, 0); + // current selected id go back to 0 + setSelectedIndex(0); + // update search text and associating app list + setSearchText(e.target.value); + }; + + const focusOnInput = (): void => { + const input = document.querySelector("#spotlight-input") as HTMLElement; + input.focus(); + }; + + return ( +
    +
    +
    + +
    + +
    + {searchText !== "" && ( +
    +
    + {appList} +
    +
    + {curDetails && ( +
    +
    + {curDetails.title} +
    + {curDetails.title} +
    +
    + {`Version: ${getRandom(0, 99)}.${getRandom(0, 999)}`} +
    +
    +
    +
    +
    Kind
    +
    Size
    +
    Created
    +
    Modified
    +
    Last opened
    +
    +
    +
    + {curDetails.type === "app" ? "Application" : "Portfolio"} +
    +
    {`${getRandom(0, 999)} G`}
    +
    {getRandomDate()}
    +
    {getRandomDate()}
    +
    {getRandomDate()}
    +
    +
    +
    + )} +
    +
    + )} +
    + ); +} diff --git a/src/components/Window.tsx b/src/components/Window.tsx new file mode 100644 index 0000000..eaf288d --- /dev/null +++ b/src/components/Window.tsx @@ -0,0 +1,212 @@ +import React, { useState, useEffect } from "react"; +import { useSelector } from "react-redux"; +import { Rnd } from "react-rnd"; +import { IoCloseOutline } from "react-icons/io5"; +import { FiMinus } from "react-icons/fi"; +import { useWindowSize } from "../hooks"; +import type { RootReduxState } from "../types"; + +const FullIcon = ({ size }: { size: number }) => { + return ( + + + + ); +}; + +const ExitFullIcon = ({ size }: { size: number }) => { + return ( + + + + ); +}; + +const minMarginY = 24; +const minMarginX = 100; + +interface TrafficProps { + id: string; + max: boolean; + setMax: (id: string, target?: boolean) => void; + setMin: (id: string) => void; + close: (id: string) => void; +} + +interface WindowProps extends TrafficProps { + min: boolean; + width?: number; + height?: number; + minWidth?: number; + minHeight?: number; + title: string; + z: number; + focus: (id: string) => void; + children: React.ReactNode; +} + +interface WindowState { + width: number; + height: number; + x: number; + y: number; +} + +const TrafficLights = ({ id, close, max, setMax, setMin }: TrafficProps) => { + const closeWindow = (e: React.MouseEvent | React.TouchEvent): void => { + e.stopPropagation(); + close(id); + }; + + return ( +
    + + + +
    + ); +}; + +const Window = (props: WindowProps) => { + const dockSize = useSelector((state: RootReduxState) => state.dockSize); + const { winWidth, winHeight } = useWindowSize(); + + const initWidth = Math.min(winWidth, props.width ? props.width : 640); + const initHeight = Math.min(winHeight, props.height ? props.height : 400); + + const [state, setState] = useState({ + width: initWidth, + height: initHeight, + // "+ winWidth" because of the boundary for windows + x: winWidth + Math.random() * (winWidth - initWidth), + // "- minMarginY" because of the boundary for windows + y: Math.random() * (winHeight - initHeight - minMarginY) + }); + + useEffect(() => { + setState({ + ...state, + width: Math.min(winWidth, state.width), + height: Math.min(winHeight, state.height) + }); + }, [winWidth, winHeight]); + + const round = props.max ? "rounded-none" : "rounded-lg"; + const minimized = props.min + ? "opacity-0 invisible transition-opacity duration-300" + : ""; + const border = props.max ? "" : "border border-gray-500 border-opacity-30"; + const width = props.max ? winWidth : state.width; + const height = props.max ? winHeight : state.height; + + const children = React.cloneElement( + props.children as React.ReactElement, + { width: width } + ); + + return ( + { + setState({ ...state, x: d.x, y: d.y }); + }} + onResizeStop={(e, direction, ref, delta, position) => { + setState({ + ...state, + width: parseInt(ref.style.width), + height: parseInt(ref.style.height), + ...position + }); + }} + minWidth={props.minWidth ? props.minWidth : 200} + minHeight={props.minHeight ? props.minHeight : 150} + dragHandleClassName="window-bar" + disableDragging={props.max} + enableResizing={!props.max} + style={{ zIndex: props.z }} + onMouseDown={() => props.focus(props.id)} + className={`absolute ${round} overflow-hidden bg-transparent w-full h-full ${border} shadow-md ${minimized}`} + id={`window-${props.id}`} + > +
    props.setMax(props.id)} + > + + {props.title} +
    +
    {children}
    +
    + ); +}; + +export default Window; diff --git a/src/components/apps/Bear.tsx b/src/components/apps/Bear.tsx new file mode 100644 index 0000000..88225ff --- /dev/null +++ b/src/components/apps/Bear.tsx @@ -0,0 +1,240 @@ +import { useState, useEffect, useCallback } from "react"; +import { useSelector } from "react-redux"; +import ReactMarkdown from "react-markdown"; +import gfm from "remark-gfm"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { dracula, prism } from "react-syntax-highlighter/dist/esm/styles/prism"; +import { GiSettingsKnobs } from "react-icons/gi"; +import { AiOutlineLink } from "react-icons/ai"; +import { IoCloudOfflineOutline } from "react-icons/io5"; +import bear from "../../configs/bear"; +import type { BearMdData, RootReduxState } from "../../types"; + +interface ContentProps { + contentID: string; + contentURL: string; +} + +interface MiddlebarProps { + items: BearMdData[]; + cur: number; + setContent: (id: string, url: string, index: number) => void; +} + +interface SidebarProps { + cur: number; + setMidBar: (items: BearMdData[], index: number) => void; +} + +interface BearState extends ContentProps { + curSidebar: number; + curMidbar: number; + midbarList: BearMdData[]; +} + +const Highlighter = (dark: boolean): any => { + interface codeProps { + node: any; + inline: boolean; + className: string; + children: any; + } + + return { + code({ node, inline, className, children, ...props }: codeProps) { + const match = /language-(\w+)/.exec(className || ""); + return !inline && match ? ( + + {String(children).replace(/\n$/, "")} + + ) : ( + {children} + ); + } + }; +}; + +const Sidebar = ({ cur, setMidBar }: SidebarProps) => { + return ( +
    +
    + + +
    +
      + {bear.map((item, index) => ( +
    • setMidBar(item.md, index)} + > + {item.icon} + {item.title} +
    • + ))} +
    +
    + ); +}; + +const Middlebar = ({ items, cur, setContent }: MiddlebarProps) => { + return ( +
    +
      + {items.map((item: BearMdData, index: number) => ( +
    • setContent(item.id, item.file, index)} + > +
      +
      + {item.icon} +
      + + {item.title} + {item.link && ( + + + + )} + +
      +
      + {item.excerpt} +
      +
    • + ))} +
    +
    + ); +}; + +const getRepoURL = (url: string) => { + return url.slice(0, -10) + "/"; +}; + +const fixImageURL = (text: string, contentURL: string): string => { + text = text.replace(/ /g, ""); + if (contentURL.indexOf("raw.githubusercontent.com") !== -1) { + const repoURL = getRepoURL(contentURL); + + const imgReg = /!\[(.*?)\]\((.*?)\)/; + const imgRegGlobal = /!\[(.*?)\]\((.*?)\)/g; + + const imgList = text.match(imgRegGlobal); + + if (imgList) { + for (const img of imgList) { + const imgURL = (img.match(imgReg) as Array)[2]; + if (imgURL.indexOf("http") !== -1) continue; + const newImgURL = repoURL + imgURL; + text = text.replace(imgURL, newImgURL); + } + } + } + return text; +}; + +const Content = ({ contentID, contentURL }: ContentProps) => { + const [storeMd, setStoreMd] = useState<{ [key: string]: string }>({}); + const dark = useSelector((state: RootReduxState) => state.dark); + + const fetchMarkdown = useCallback( + (id: string, url: string) => { + if (!storeMd[id]) { + fetch(url) + .then((response) => response.text()) + .then((text) => { + storeMd[id] = fixImageURL(text, url); + setStoreMd({ ...storeMd }); + }) + .catch((error) => console.error(error)); + } + }, + [storeMd] + ); + + useEffect(() => { + fetchMarkdown(contentID, contentURL); + }, [contentID, contentURL, fetchMarkdown]); + + return ( +
    +
    + + {storeMd[contentID]} + +
    +
    + ); +}; + +const Bear = () => { + const [state, setState] = useState({ + curSidebar: 0, + curMidbar: 0, + midbarList: bear[0].md, + contentID: bear[0].md[0].id, + contentURL: bear[0].md[0].file + }); + + const setMidBar = (items: BearMdData[], index: number) => { + setState({ + curSidebar: index, + curMidbar: 0, + midbarList: items, + contentID: items[0].id, + contentURL: items[0].file + }); + }; + + const setContent = (id: string, url: string, index: number) => { + setState({ + ...state, + curMidbar: index, + contentID: id, + contentURL: url + }); + }; + + return ( +
    +
    + +
    +
    + +
    +
    + +
    +
    + ); +}; + +export default Bear; diff --git a/src/components/apps/FaceTime.tsx b/src/components/apps/FaceTime.tsx new file mode 100644 index 0000000..ea363bd --- /dev/null +++ b/src/components/apps/FaceTime.tsx @@ -0,0 +1,60 @@ +import { useRef, useState } from "react"; +import Webcam from "react-webcam"; + +const videoConstraints = { + facingMode: "user" +}; + +const FaceTime = () => { + const [click, setClick] = useState(false); + const [img, setImg] = useState(""); + const webcamRef = useRef(null); + + const capture = () => { + if (!webcamRef.current) return; + const imageSrc = webcamRef.current.getScreenshot() as string; + setImg(imageSrc); + }; + + if (click) + return ( +
    + {img && ( + yourimage + )} + +
    + ); + else + return ( +
    + +
    + ); +}; + +export default FaceTime; diff --git a/src/components/apps/Safari.tsx b/src/components/apps/Safari.tsx new file mode 100644 index 0000000..fe302c6 --- /dev/null +++ b/src/components/apps/Safari.tsx @@ -0,0 +1,258 @@ +import React, { useState } from "react"; +import { useSelector } from "react-redux"; +import { websites, wallpapers } from "../../configs"; +import { FaShieldAlt } from "react-icons/fa"; +import { FiChevronLeft, FiChevronRight } from "react-icons/fi"; +import { BsLayoutSidebar } from "react-icons/bs"; +import { IoShareOutline, IoCopyOutline } from "react-icons/io5"; +import { checkURL } from "../../utils"; +import type { SiteSectionData, SiteData, RootReduxState } from "../../types"; + +interface SafariState { + goURL: string; + currentURL: string; +} + +interface SafariProps { + width?: number; +} + +interface NavProps { + width: number; + setGoURL: (url: string) => void; +} + +interface NavSectionProps extends NavProps { + section: SiteSectionData; +} + +const NavSection = ({ width, section, setGoURL }: NavSectionProps) => { + const grid = width < 640 ? "grid-cols-4" : "grid-cols-9"; + + return ( +
    +
    + {section.title} +
    +
    + {section.sites.map((site: SiteData) => ( +
    +
    +
    + {site.img ? ( + {site.title} setGoURL(site.link) + : () => window.open(site.link) + } + /> + ) : ( +
    setGoURL(site.link) + : () => window.open(site.link) + } + > + {site.title} +
    + )} +
    + + {site.title} + +
    +
    + ))} +
    +
    + ); +}; + +const numTracker = Math.floor(Math.random() * 99 + 1); + +const NavPage = ({ width, setGoURL }: NavProps) => { + const dark = useSelector((state: RootReduxState) => state.dark); + + const grid = width < 640 ? "grid-cols-4" : "grid-cols-8"; + const span = width < 640 ? "col-span-3" : "col-span-7"; + + return ( +
    +
    + {/* Favorites */} + + + {/* Frequently Visited */} + + + {/* Privacy Report */} +
    +
    + Privacy Report +
    +
    +
    + + {numTracker} +
    +
    + In the last seven days, Safari has prevent {numTracker} tracker + from profiling you. +
    +
    +
    +
    +
    + ); +}; + +const NoInternetPage = () => { + const dark = useSelector((state: RootReduxState) => state.dark); + + return ( +
    +
    +
    +
    + You Are Not Connected to the Internet +
    +
    + This page can't be displayed because your computer is currently + offline. +
    +
    +
    +
    + ); +}; + +const Safari = ({ width }: SafariProps) => { + const wifi = useSelector((state: RootReduxState) => state.wifi); + const [state, setState] = useState({ + goURL: "", + currentURL: "" + }); + + const setGoURL = (url: string) => { + const isValid = checkURL(url); + + if (isValid) { + if ( + url.substring(0, 7) !== "http://" && + url.substring(0, 8) !== "https://" + ) + url = `https://${url}`; + } else if (url !== "") { + url = `https://www.bing.com/search?q=${url}`; + } + + setState({ + goURL: url, + currentURL: url + }); + }; + + const pressURL = (e: React.KeyboardEvent) => { + const keyCode = e.key; + if (keyCode === "Enter") setGoURL((e.target as HTMLInputElement).value); + }; + + const buttonColor = state.goURL === "" ? "text-gray-400" : "text-gray-700"; + const grid = (width as number) < 640 ? "grid-cols-2" : "grid-cols-3"; + const hideLast = (width as number) < 640 ? "hidden" : ""; + + return ( +
    + {/* browser topbar */} +
    +
    + + + +
    +
    + + setState({ ...state, currentURL: e.target.value })} + onKeyPress={pressURL} + className="h-6 w-full p-2 rounded text-sm text-center font-normal text-gray-500 bg-gray-200 outline-none focus:outline-none border-2 border-transparent focus:border-blue-400" + placeholder="Search or enter website name" + /> +
    +
    + + +
    +
    + + {/* browser content */} + {wifi ? ( + state.goURL === "" ? ( + + ) : ( +