├── .DS_Store ├── public ├── logo128.png ├── logo16.png ├── logo48.png ├── robots.txt ├── manifest.json └── index.html ├── src ├── media │ ├── login-icon.png │ ├── blue-bar-img.png │ ├── gray-bar-img.png │ ├── start-button.png │ ├── start-footer.png │ ├── start-header.png │ ├── time-bar-img.png │ ├── window-icon.png │ ├── maximize-second.png │ ├── new-icon-icon.png │ ├── new-window-icon.png │ ├── tab-background.png │ ├── windows7-logo.png │ ├── windows98-logo.png │ ├── control-panel-icon.jpg │ └── start-button-pressed.png ├── index.js ├── styles │ ├── StyledComponents.js │ ├── theme.js │ ├── Layout.js │ └── Headers.js ├── components │ ├── Editor │ │ ├── Plugins │ │ │ ├── CodeHighlightPlugin.js │ │ │ ├── ReadOnlyPlugin.js │ │ │ └── ToolbarPlugins │ │ │ │ ├── BlockOptions.js │ │ │ │ ├── FloatingLinkEditor.js │ │ │ │ └── index.js │ │ ├── RichText.js │ │ ├── Theme.js │ │ └── index.js │ ├── SvgMaster.js │ ├── BackButton │ │ └── index.js │ ├── Toggle │ │ └── index.js │ ├── Today.js │ ├── Window │ │ ├── SortableItem.jsx │ │ ├── RenderWindowComponents.jsx │ │ ├── helper.js │ │ └── index.js │ ├── ComponentOptions │ │ └── index.js │ ├── KanbanBoard │ │ ├── KanbanItemOverlay.jsx │ │ ├── KanbanHeader.jsx │ │ ├── Column.jsx │ │ ├── KanbanItem.jsx │ │ └── index.jsx │ ├── DragIndicator │ │ └── index.js │ ├── SettingsWindow │ │ ├── index.js │ │ └── Tabs.js │ ├── YouTubeVideo │ │ └── index.js │ ├── SearchBar │ │ └── index.js │ ├── TopBanner │ │ └── index.jsx │ ├── Startbar │ │ ├── StartWindow.js │ │ ├── items.js │ │ ├── index.js │ │ └── styles.js │ ├── Icon │ │ └── index.js │ ├── WindowTitleBar │ │ └── index.js │ └── Image │ │ └── index.js ├── data │ ├── RenderIcons.js │ └── RenderWindows.js ├── Store.js ├── App.js ├── index.css └── functions │ └── helpers.js ├── .idea ├── .gitignore ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── fileTemplates │ └── internal │ │ └── JavaScript File.js ├── vcs.xml ├── prettier.xml ├── modules.xml └── etesam-startpage.iml ├── cypress.config.js ├── cypress ├── fixtures │ └── example.json ├── support │ ├── e2e.js │ └── commands.js └── e2e │ ├── start-menu.cy.js │ └── windows.cy.js ├── .gitignore ├── CONTRIBUTING.md ├── .github └── workflows │ └── cypress-e2e-tests.yml ├── README.md └── package.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/.DS_Store -------------------------------------------------------------------------------- /public/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/public/logo128.png -------------------------------------------------------------------------------- /public/logo16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/public/logo16.png -------------------------------------------------------------------------------- /public/logo48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/public/logo48.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/media/login-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/login-icon.png -------------------------------------------------------------------------------- /src/media/blue-bar-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/blue-bar-img.png -------------------------------------------------------------------------------- /src/media/gray-bar-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/gray-bar-img.png -------------------------------------------------------------------------------- /src/media/start-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/start-button.png -------------------------------------------------------------------------------- /src/media/start-footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/start-footer.png -------------------------------------------------------------------------------- /src/media/start-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/start-header.png -------------------------------------------------------------------------------- /src/media/time-bar-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/time-bar-img.png -------------------------------------------------------------------------------- /src/media/window-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/window-icon.png -------------------------------------------------------------------------------- /src/media/maximize-second.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/maximize-second.png -------------------------------------------------------------------------------- /src/media/new-icon-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/new-icon-icon.png -------------------------------------------------------------------------------- /src/media/new-window-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/new-window-icon.png -------------------------------------------------------------------------------- /src/media/tab-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/tab-background.png -------------------------------------------------------------------------------- /src/media/windows7-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/windows7-logo.png -------------------------------------------------------------------------------- /src/media/windows98-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/windows98-logo.png -------------------------------------------------------------------------------- /src/media/control-panel-icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/control-panel-icon.jpg -------------------------------------------------------------------------------- /src/media/start-button-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Etesam913/xp-newtab/HEAD/src/media/start-button-pressed.png -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require("cypress"); 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | baseUrl: "http://localhost:3000/", 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /.idea/fileTemplates/internal/JavaScript File.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function insertNameHere(){ 4 | return( 5 |
test
6 | ); 7 | } 8 | 9 | export default insertNameHere; -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.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 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/styles/StyledComponents.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const DeleteButton = styled.button` 4 | min-width: 55px; 5 | padding: 0 6px; 6 | text-align: center; 7 | margin: ${props => props.margin}; 8 | `; 9 | 10 | export const OptionsButton = styled.button` 11 | margin: ${props => props.margin ? props.margin : "0 0 0 0.5rem"}; 12 | width: ${props => props.width ? props.width : "117px"}; 13 | `; 14 | -------------------------------------------------------------------------------- /src/components/Editor/Plugins/CodeHighlightPlugin.js: -------------------------------------------------------------------------------- 1 | import { registerCodeHighlighting } from "@lexical/code"; 2 | import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; 3 | import { useEffect } from "react"; 4 | 5 | export default function CodeHighlightPlugin() { 6 | const [editor] = useLexicalComposerContext(); 7 | useEffect(() => { 8 | return registerCodeHighlighting(editor); 9 | }, [editor]); 10 | return null; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Editor/Plugins/ReadOnlyPlugin.js: -------------------------------------------------------------------------------- 1 | import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; 2 | import { Fragment, useEffect } from "react"; 3 | 4 | function ReadOnlyPlugin({ isEditModeOn }) { 5 | const [editor] = useLexicalComposerContext(); 6 | 7 | useEffect(() => { 8 | editor.setReadOnly(!isEditModeOn); 9 | }, [isEditModeOn, editor]); 10 | 11 | return ; 12 | } 13 | export default ReadOnlyPlugin; 14 | -------------------------------------------------------------------------------- /src/data/RenderIcons.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Icon from "../components/Icon/index"; 3 | import { useStore } from "../Store"; 4 | 5 | function RenderIcons() { 6 | const iconData = useStore((state) => state.iconData); 7 | const icons = iconData.map((icon, index) => { 8 | return ; 9 | }); 10 | 11 | return
{icons}
; 12 | } 13 | 14 | export default RenderIcons; 15 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "XP Newtab", 4 | "author": "Etesam Ansari", 5 | "version": "1.2.1", 6 | "description": "A new tab extension that is in the style of Windows XP.", 7 | "chrome_url_overrides": { 8 | "newtab": "index.html" 9 | }, 10 | "permissions": [ 11 | "unlimitedStorage" 12 | ], 13 | "icons": { 14 | "16": "logo16.png", 15 | "48": "logo48.png", 16 | "128": "logo128.png" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/data/RenderWindows.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Window from "../components/Window/index"; 3 | 4 | import { useStore } from "../Store"; 5 | 6 | function RenderWindows() { 7 | const windowData = useStore((state) => state.windowData); 8 | 9 | const windows = windowData.map((item, index) => { 10 | return ( 11 | 12 | ); 13 | }); 14 | return
{windows}
; 15 | } 16 | 17 | export default RenderWindows; 18 | -------------------------------------------------------------------------------- /src/components/SvgMaster.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function MagnifyingGlass() { 4 | return ( 5 | 11 | 13 | 14 | ); 15 | } -------------------------------------------------------------------------------- /.idea/etesam-startpage.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## XP-Newtab 2 | 3 | ### Instructions to run locally 4 | 5 | 1. Install npm 6 | 2. Clone the repo by running: `git clone https://github.com/Etesam913/xp-newtab.git` 7 | 3. Run `cd xp-newtab` and run `npm install` 8 | 4. Run `npm start` 9 | 5. Visit localhost:3000 to visit the running app 10 | 11 | ### How to run tests 12 | 13 | 1. Run `npm run test` 14 | 2. Open the cypress window and follow the steps 15 | 16 | ### Instructions to Build 17 | 18 | 1. Run `npm run build` 19 | 2. Run `web-ext build` in the `build/` directory 20 | -------------------------------------------------------------------------------- /src/styles/theme.js: -------------------------------------------------------------------------------- 1 | export const theme ={ 2 | fonts: { 3 | primary: '"Pixelated MS Sans Serif", "Arial", "Helvetica", serif', 4 | secondary: '"Trebuchet MS", "Arial", "Helvetica", serif' 5 | }, 6 | cursors: { 7 | move: 'url("https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/cursors/move.cur"), move', 8 | auto: 'url("https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/cursors/auto.cur"), auto', 9 | pointer: 'url("https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/cursors/pointer.cur"), pointer', 10 | wait: 'url("https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/cursors/loading.cur"), wait' 11 | } 12 | } -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | XP Newtab 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/cypress-e2e-tests.yml: -------------------------------------------------------------------------------- 1 | name: Cypress E2E Tests 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: 🔀 Checking out repo 15 | uses: actions/checkout@v2 16 | 17 | - name: 🪨 Setup Node 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 16 21 | 22 | - name: ☝️ Install dependencies 23 | run: npm install 24 | 25 | - name: ✅ Running Cypress tests 26 | uses: cypress-io/github-action@v3.1.0 27 | with: 28 | install: false 29 | browser: chrome 30 | config-file: cypress.config.js 31 | start: npm start 32 | wait-on: "http://localhost:3000" 33 | headless: true 34 | 35 | -------------------------------------------------------------------------------- /src/Store.js: -------------------------------------------------------------------------------- 1 | import create from "zustand"; 2 | import { getDefaultValue } from "./functions/helpers"; 3 | 4 | export const useStore = create((set) => ({ 5 | isEditModeOn: false, 6 | toggleEditMode: () => set((state) => ({ isEditModeOn: !state.isEditModeOn })), 7 | 8 | isSettingsShowing: false, 9 | setIsSettingsShowing: (val) => set(() => ({ isSettingsShowing: val })), 10 | 11 | settingsData: getDefaultValue("settingsData"), 12 | setSettingsData: (val) => set(() => ({ settingsData: val })), 13 | 14 | iconData: getDefaultValue("iconData"), 15 | setIconData: (val) => set(() => ({ iconData: val })), 16 | 17 | windowData: getDefaultValue("windowData"), 18 | setWindowData: (val) => set(() => ({ windowData: val })), 19 | 20 | focusedWindow: 0, 21 | setFocusedWindow: (val) => set(() => ({ focusedWindow: val })), 22 | })); 23 | -------------------------------------------------------------------------------- /src/styles/Layout.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | 3 | export const FlexContainer = styled.div` 4 | display: flex; 5 | padding: ${(props) => props.padding}; 6 | margin: ${(props) => props.margin}; 7 | cursor: ${(props) => props.cursor}; 8 | width: ${(props) => props.width}; 9 | height: ${(props) => props.height}; 10 | flex-wrap: ${(props) => props.flexWrap}; 11 | flex-direction: ${(props) => 12 | props.flexDirection ? props.flexDirection : "row"}; 13 | justify-content: ${(props) => 14 | props.justifyContent ? props.justifyContent : "center"}; 15 | align-items: ${(props) => (props.alignItems ? props.alignItems : "center")}; 16 | flex: ${(props) => props.flex}; 17 | ${(props) => 18 | props.tablet && 19 | css` 20 | @media only screen and (max-width: 768px) { 21 | flex-direction: column; 22 | align-items: flex-start !important; 23 | } 24 | `} 25 | `; 26 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) -------------------------------------------------------------------------------- /src/components/BackButton/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | function BackButton({ margin, onClick, dataCy }) { 5 | function handleClick() { 6 | if (onClick) onClick(); 7 | } 8 | 9 | return ( 10 | 11 | 19 | 20 | ); 21 | } 22 | 23 | const BackImage = styled.img` 24 | height: 24px; 25 | width: 24px; 26 | `; 27 | 28 | const ImageButton = styled.button` 29 | background: transparent !important; 30 | border: 0; 31 | box-shadow: none !important; 32 | min-width: 24px !important; 33 | min-height: 24px; 34 | width: 24px; 35 | height: 24px; 36 | padding: 0; 37 | margin: ${(props) => (props.margin ? props.margin : "0 0.25rem 0 0")}; 38 | `; 39 | 40 | export default BackButton; 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

✏️ XP-Newtab

2 |

An extension that customizes your new-tab page in the style of Windows XP.

3 | 4 | https://github.com/Etesam913/xp-newtab/assets/55665282/82186bc0-a41b-480b-9635-264d1260995f 5 | 6 | 7 |

🔗 Links

8 |
    9 |
  • Firefox: https://addons.mozilla.org/en-US/firefox/addon/xp-newtab/
  • 10 |
  • Chrome: https://chrome.google.com/webstore/detail/xp-newtab/ncfmlogaelpnniflgipmnnglhfiifkke
  • 11 |
12 | 13 |

✨ Features

14 |
    15 |
  • Create draggable icons that redirect to websites when double clicked
  • 16 |
  • Create draggable windows that can hold a plethora of components
  • 17 |
  • Add search bars, images, videos, and text to windows.
  • 18 |
  • Instantaneous saves
  • 19 |
20 | 21 |

📝 Created with

22 |
    23 |
  • React.js
  • 24 |
  • Styled Components
  • 25 |
  • XP.css
  • 26 |
27 | 28 | I am not affiliated, associated, authorized, endorsed by, or in any way officially connected with Microsoft, or any of its subsidiaries or its affiliates. 29 | -------------------------------------------------------------------------------- /src/components/Toggle/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled, { css } from "styled-components"; 3 | 4 | function Toggle({ stateVal, toggleStateVal, pointerEvents }) { 5 | return ( 6 | 11 | 12 | 13 | ); 14 | } 15 | 16 | const ToggleWrapper = styled.div` 17 | width: 34px; 18 | height: 20px; 19 | border-radius: 100px; 20 | padding: 0 3px; 21 | display: flex; 22 | box-sizing: border-box; 23 | align-items: center; 24 | cursor: pointer; 25 | ${(props) => 26 | props.on && 27 | css` 28 | background-color: #22cc88; 29 | justify-content: flex-end; 30 | `} 31 | 32 | ${(props) => 33 | !props.on && 34 | css` 35 | background-color: #dddddd; 36 | justify-content: flex-start; 37 | `}; 38 | pointer-events: ${(props) => props.pointerEvents}; 39 | `; 40 | 41 | const ToggleCircle = styled.div` 42 | width: 14px; 43 | height: 14px; 44 | background-color: #ffffff; 45 | border-radius: 20px; 46 | box-shadow: 1px 2px 3px rgba(0, 0, 0, 0.02); 47 | `; 48 | 49 | export default Toggle; 50 | -------------------------------------------------------------------------------- /src/styles/Headers.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Header1 = styled.h1` 4 | margin: ${(props) => (props.margin ? props.margin : "1rem 0")}; 5 | font-size: 1.5em; 6 | font-weight: normal; 7 | width: ${(props) => props.width}; 8 | text-align: ${(props) => (props.textAlign ? props.textAlign : "left")}; 9 | display: ${(props) => props.display}; 10 | justify-content: ${(props) => props.justifyContent}; 11 | background: ${(props) => props.background}; 12 | border: ${(props) => props.border}; 13 | `; 14 | 15 | export const Header2 = styled.h2` 16 | margin: ${(props) => (props.margin ? props.margin : "0.5rem 0")}; 17 | font-size: 2.5em; 18 | transition: color 150ms; 19 | `; 20 | 21 | export const Header3 = styled.h3` 22 | margin: ${(props) => (props.margin ? props.margin : "0.5 0rem")}; 23 | font-size: 2em; 24 | transition: color 150ms; 25 | `; 26 | 27 | export const Header4 = styled.h4` 28 | margin: ${(props) => (props.margin ? props.margin : "0.5 0rem")}; 29 | font-size: 1.5em; 30 | transition: color 150ms; 31 | `; 32 | export const Header5 = styled.h5` 33 | margin: ${(props) => (props.margin ? props.margin : "0.5 0rem")}; 34 | font-size: 1.15em; 35 | transition: color 150ms; 36 | font-weight: normal; 37 | `; 38 | -------------------------------------------------------------------------------- /src/components/Today.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Header2 } from "../styles/Headers"; 3 | 4 | function Today() { 5 | const [today, setToday] = useState(""); 6 | 7 | function getMonth(index) { 8 | let months = [ 9 | "January", 10 | "February", 11 | "March", 12 | "April", 13 | "May", 14 | "June", 15 | "July", 16 | "August", 17 | "September", 18 | "October", 19 | "November", 20 | "December" 21 | ]; 22 | for (let i = 0; i < months.length; i++) { 23 | if (i === index) { 24 | return months[i]; 25 | } 26 | } 27 | console.error("THAT MONTH DOES NOT EXIST!"); 28 | return; 29 | } 30 | 31 | useEffect(() => { 32 | let d = new Date(); 33 | let monthNumber = d.getMonth(); 34 | let day = d.getDate(); 35 | let year = d.getFullYear(); 36 | setToday(getMonth(monthNumber) + " " + day + " " + year); 37 | 38 | const interval = setInterval(() => { 39 | d = new Date(); 40 | monthNumber = d.getMonth(); 41 | day = d.getDate(); 42 | year = d.getFullYear(); 43 | setToday(getMonth(monthNumber) + " " + day + " " + year); 44 | }, 30000); // Done every 30 seconds 45 | return () => clearInterval(interval); 46 | }, []); 47 | 48 | return {today}; 49 | } 50 | 51 | export default Today; 52 | -------------------------------------------------------------------------------- /src/components/Window/SortableItem.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { useSortable } from "@dnd-kit/sortable"; 4 | import DragIndicator from "../DragIndicator"; 5 | import { useStore } from "../../Store"; 6 | 7 | function SortableItem({ children, id, height }) { 8 | const isEditModeOn = useStore((state) => state.isEditModeOn); 9 | 10 | const { attributes, listeners, setNodeRef, transform, transition } = 11 | useSortable({ id: id }); 12 | 13 | const style = { 14 | transform: transform 15 | ? `translate3d(${transform.x}px, ${transform.y}px, 0)` 16 | : undefined, 17 | transition, 18 | height: height, 19 | }; 20 | 21 | return ( 22 |
23 | 29 | 30 | 31 | {children} 32 |
33 | ); 34 | } 35 | 36 | const DragHandle = styled.button` 37 | border: 0 !important; 38 | background: transparent !important; 39 | display: ${(props) => (!props.isEditModeOn ? "none" : "flex")}; 40 | padding: 0 !important; 41 | box-shadow: none; 42 | align-items: center; 43 | 44 | :active { 45 | box-shadow: none !important; 46 | padding: 0 !important; 47 | } 48 | `; 49 | 50 | export default SortableItem; 51 | -------------------------------------------------------------------------------- /src/components/ComponentOptions/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { changeItemProperty } from "../Window/helper"; 5 | import { 6 | convertJustifyContentToTextAlign, 7 | convertTextAlignToJustifyContent, 8 | } from "../../functions/helpers"; 9 | import { useStore } from "../../Store"; 10 | 11 | export function TextAlignOptions({ windowObj, windowItem, text }) { 12 | const windowData = useStore((state) => state.windowData); 13 | const setWindowData = useStore((state) => state.setWindowData); 14 | 15 | return ( 16 |
17 | {text ? "Text Align:" : "Align"} 18 | 38 |
39 | ); 40 | } 41 | 42 | const OptionTitle = styled.span` 43 | margin-right: 0.25rem; 44 | white-space: nowrap; 45 | `; 46 | -------------------------------------------------------------------------------- /src/components/Editor/RichText.js: -------------------------------------------------------------------------------- 1 | import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"; 2 | import { ContentEditable } from "@lexical/react/LexicalContentEditable"; 3 | import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; 4 | import { changeItemProperty } from "../Window/helper"; 5 | import { useStore } from "../../Store"; 6 | import { useRef } from "react"; 7 | 8 | function RichText({ windowItem, windowObj }) { 9 | const [editor] = useLexicalComposerContext(); 10 | const richTextContainerRef = useRef(null); 11 | const windowData = useStore((state) => state.windowData); 12 | const setWindowData = useStore((state) => state.setWindowData); 13 | 14 | return ( 15 |
18 | changeItemProperty( 19 | windowObj, 20 | windowData, 21 | setWindowData, 22 | windowItem, 23 | "editorState", 24 | JSON.stringify(editor.getEditorState()) 25 | ) 26 | } 27 | onBlur={() => 28 | changeItemProperty( 29 | windowObj, 30 | windowData, 31 | setWindowData, 32 | windowItem, 33 | "editorState", 34 | JSON.stringify(editor.getEditorState()) 35 | ) 36 | } 37 | > 38 | } 40 | /> 41 |
42 | ); 43 | } 44 | export default RichText; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "etesam-startpage", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@dnd-kit/core": "^6.0.5", 7 | "@dnd-kit/modifiers": "^6.0.0", 8 | "@dnd-kit/sortable": "^7.0.1", 9 | "@dnd-kit/utilities": "^3.2.0", 10 | "@lexical/react": "^0.3.5", 11 | "lexical": "^0.3.5", 12 | "re-resizable": "^6.9.9", 13 | "react": "18.2.0", 14 | "react-colorful": "^5.0.0", 15 | "react-dom": "18.2.0", 16 | "react-draggable": "^4.4.3", 17 | "react-scripts": "^5.0.0", 18 | "styled-components": "^5.3.5", 19 | "xp.css": "^0.2.4", 20 | "zustand": "^3.7.0" 21 | }, 22 | "prettier": { 23 | "bracketSpacing": true 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "INLINE_RUNTIME_CHUNK=false react-scripts build", 28 | "test": "cypress open", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app", 34 | "react-app/jest", 35 | "plugin:cypress/recommended" 36 | ] 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "chai-colors": "^1.0.1", 52 | "cypress": "^10.2.0", 53 | "eslint-plugin-cypress": "^2.12.1", 54 | "prettier": "^2.5.1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/KanbanBoard/KanbanItemOverlay.jsx: -------------------------------------------------------------------------------- 1 | import { DragOverlay } from "@dnd-kit/core"; 2 | import styled, { css } from "styled-components"; 3 | import { useStore } from "../../Store"; 4 | 5 | function KanbanItemDragOverlay({ activeId, items, width }) { 6 | const settingsData = useStore((state) => state.settingsData); 7 | function getText() { 8 | const itemsArrays = Object.values(items); 9 | for (let i = 0; i < itemsArrays.length; i++) { 10 | for (let j = 0; j < itemsArrays[i].length; j++) { 11 | if (itemsArrays[i][j].id === activeId) { 12 | return itemsArrays[i][j].text; 13 | } 14 | } 15 | } 16 | } 17 | 18 | return ( 19 | 20 | {activeId && ( 21 | 25 | {getText()} 26 | 27 | )} 28 | 29 | ); 30 | } 31 | 32 | const KanbanItemContainer = styled.div` 33 | display: flex; 34 | align-items: center; 35 | 36 | ${(props) => 37 | props.windowsOS === 0 && 38 | css` 39 | background-color: #d5d1c1; 40 | `}; 41 | 42 | ${(props) => 43 | props.windowsOS === 1 && 44 | css` 45 | background-color: #bdbdbd; 46 | `}; 47 | 48 | ${(props) => 49 | props.windowsOS === 2 && 50 | css` 51 | background-color: #acd2e0; 52 | `}; 53 | opacity: 0.6; 54 | padding: 0.35rem; 55 | font-size: 0.8rem; 56 | white-space: pre-wrap; 57 | width: ${(props) => props.width}; 58 | transform: rotate(-4deg); 59 | `; 60 | 61 | export default KanbanItemDragOverlay; 62 | -------------------------------------------------------------------------------- /src/components/DragIndicator/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | function DragIndicator() { 5 | return ( 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | const Indicator = styled.svg` 27 | cursor: ${(props) => props.theme.cursors.move}; 28 | `; 29 | 30 | export default DragIndicator; 31 | -------------------------------------------------------------------------------- /src/components/Editor/Theme.js: -------------------------------------------------------------------------------- 1 | const theme = { 2 | // Theme styling goes here 3 | text: { 4 | underline: "editor-underline", 5 | strikethrough: "editor-strikethrough", 6 | underlineStrikethrough: "editor-text-underline-strikethrough", 7 | bold: "editor-text-bold", 8 | italic: "editor-text-italic", 9 | }, 10 | paragraph: "editor-paragraph", 11 | code: "editor-code", 12 | list: { 13 | nested: { 14 | listitem: "editor-nested-listitem", 15 | }, 16 | ol: "editor-list-ol", 17 | ul: "editor-list-ul", 18 | listitem: "editor-listitem", 19 | }, 20 | codeHighlight: { 21 | atrule: "editor-tokenAttr", 22 | attr: "editor-tokenAttr", 23 | boolean: "editor-tokenProperty", 24 | builtin: "editor-tokenSelector", 25 | cdata: "editor-tokenComment", 26 | char: "editor-tokenSelector", 27 | class: "editor-tokenFunction", 28 | "class-name": "editor-tokenFunction", 29 | comment: "editor-tokenComment", 30 | constant: "editor-tokenProperty", 31 | deleted: "editor-tokenProperty", 32 | doctype: "editor-tokenComment", 33 | entity: "editor-tokenOperator", 34 | function: "editor-tokenFunction", 35 | important: "editor-tokenVariable", 36 | inserted: "editor-tokenSelector", 37 | keyword: "editor-tokenAttr", 38 | namespace: "editor-tokenVariable", 39 | number: "editor-tokenProperty", 40 | operator: "editor-tokenOperator", 41 | prolog: "editor-tokenComment", 42 | property: "editor-tokenProperty", 43 | punctuation: "editor-tokenPunctuation", 44 | regex: "editor-tokenVariable", 45 | selector: "editor-tokenSelector", 46 | string: "editor-tokenSelector", 47 | symbol: "editor-tokenProperty", 48 | tag: "editor-tokenProperty", 49 | url: "editor-tokenOperator", 50 | variable: "editor-tokenVariable", 51 | }, 52 | }; 53 | export default theme; 54 | -------------------------------------------------------------------------------- /src/components/KanbanBoard/KanbanHeader.jsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | import { useStore } from "../../Store"; 3 | 4 | function updateColumnHeader(text, id, columnHeaders, setColumnHeaders) { 5 | const copyOfColumnHeaders = { ...columnHeaders }; 6 | copyOfColumnHeaders[id] = text; 7 | setColumnHeaders(copyOfColumnHeaders); 8 | } 9 | 10 | function KanbanHeader({ columnHeaders, id, setColumnHeaders, margin }) { 11 | const isEditModeOn = useStore((store) => store.isEditModeOn); 12 | const settingsData = useStore((state) => state.settingsData); 13 | 14 | return ( 15 | 16 | {isEditModeOn ? ( 17 | 22 | updateColumnHeader( 23 | e.target.value, 24 | id, 25 | columnHeaders, 26 | setColumnHeaders 27 | ) 28 | } 29 | /> 30 | ) : ( 31 | {columnHeaders[id]} 32 | )} 33 | 34 | ); 35 | } 36 | 37 | const HeaderContainer = styled.div` 38 | padding: 0.75rem 0.6rem 0.4rem; 39 | margin: ${(props) => props.margin}; 40 | 41 | ${(props) => 42 | props.windowsOS === 0 && 43 | css` 44 | background-color: #f2eedc; 45 | `}; 46 | 47 | ${(props) => 48 | props.windowsOS === 1 && 49 | css` 50 | background-color: #dddddd; 51 | `}; 52 | 53 | ${(props) => 54 | props.windowsOS === 2 && 55 | css` 56 | background-color: #cbebf6; 57 | `}; 58 | `; 59 | 60 | const KanbanTitle = styled.h2` 61 | font-size: 0.9rem; 62 | margin: 0; 63 | `; 64 | 65 | const KanbanTitleInput = styled.input` 66 | color: black !important; 67 | font-size: 0.9rem; 68 | width: 100%; 69 | padding: 0 0 0 3px !important; 70 | `; 71 | 72 | export default KanbanHeader; 73 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 59 | -------------------------------------------------------------------------------- /src/components/KanbanBoard/Column.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | SortableContext, 3 | verticalListSortingStrategy, 4 | } from "@dnd-kit/sortable"; 5 | import styled, { css } from "styled-components"; 6 | import { useDroppable } from "@dnd-kit/core"; 7 | import KanbanItem from "./KanbanItem"; 8 | import { useStore } from "../../Store"; 9 | 10 | function Column({ items, setItems, id, margin }) { 11 | const settingsData = useStore((state) => state.settingsData); 12 | const { setNodeRef } = useDroppable({ 13 | id, 14 | }); 15 | 16 | function addItem() { 17 | const copyOfItems = { ...items }; 18 | let greatestId = 0; 19 | 20 | // have to get the greatest id out of every kanban item to prevent collisions 21 | const keys = Object.keys(copyOfItems); 22 | for (let i = 0; i < keys.length; i++) { 23 | const currentColumn = copyOfItems[keys[i]]; 24 | for (let j = 0; j < currentColumn.length; j++) { 25 | if (currentColumn[j].id > greatestId) { 26 | greatestId = currentColumn[j].id; 27 | } 28 | } 29 | } 30 | 31 | copyOfItems[id].push({ 32 | id: greatestId + 1, 33 | text: "✨ This is your new item", 34 | }); 35 | setItems(copyOfItems); 36 | } 37 | const columnValues = items[id].map((item) => item.text); 38 | const isEditModeOn = useStore((store) => store.isEditModeOn); 39 | return ( 40 | 41 | 45 | 46 | {columnValues.map((text, index) => ( 47 | 56 | ))} 57 | 58 | 59 | {isEditModeOn && ( 60 | 61 | 62 | 63 | )} 64 | 65 | ); 66 | } 67 | 68 | const KanbanColumn = styled.div` 69 | ${(props) => 70 | props.windowsOS === 0 && 71 | css` 72 | background-color: #f2eedc; 73 | `}; 74 | 75 | ${(props) => 76 | props.windowsOS === 1 && 77 | css` 78 | background-color: #dddddd; 79 | `}; 80 | 81 | ${(props) => 82 | props.windowsOS === 2 && 83 | css` 84 | background-color: #cbebf6; 85 | `}; 86 | 87 | padding: 0 0.6rem; 88 | margin: ${(props) => props.margin}; 89 | `; 90 | 91 | const ItemsContainer = styled.div` 92 | max-height: 20rem; 93 | overflow-y: auto; 94 | `; 95 | 96 | const AddButtonContainer = styled.div` 97 | display: flex; 98 | justify-content: center; 99 | margin-bottom: 0.5rem; 100 | `; 101 | 102 | export default Column; 103 | -------------------------------------------------------------------------------- /src/components/Editor/index.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | import { LexicalComposer } from "@lexical/react/LexicalComposer"; 3 | import { HeadingNode } from "@lexical/rich-text"; 4 | import theme from "./Theme"; 5 | import { AutoLinkNode, LinkNode } from "@lexical/link"; 6 | import { CodeHighlightNode, CodeNode } from "@lexical/code"; 7 | import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"; 8 | import CodeHighlightPlugin from "./Plugins/CodeHighlightPlugin"; 9 | import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"; 10 | import { ListPlugin } from "@lexical/react/LexicalListPlugin"; 11 | import { ListItemNode, ListNode } from "@lexical/list"; 12 | import ToolbarPlugins from "./Plugins/ToolbarPlugins"; 13 | import { useStore } from "../../Store"; 14 | import ReadOnlyPlugin from "./Plugins/ReadOnlyPlugin"; 15 | import RichText from "./RichText"; 16 | import { DeleteButton } from "../../styles/StyledComponents"; 17 | import { handleDelete } from "../Window/helper"; 18 | import { FlexContainer } from "../../styles/Layout"; 19 | import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"; 20 | import { TRANSFORMERS } from "@lexical/markdown"; 21 | 22 | // Catch any errors that occur during Lexical updates and log them 23 | // or throw them as needed. If you don't throw them, Lexical will 24 | // try to recover gracefully without losing user data. 25 | function onError(error) { 26 | console.error(error); 27 | } 28 | 29 | function Editor({ windowItem, windowObj }) { 30 | const isEditModeOn = useStore((state) => state.isEditModeOn); 31 | const windowData = useStore((state) => state.windowData); 32 | const setWindowData = useStore((state) => state.setWindowData); 33 | 34 | return ( 35 | 36 | 53 | {isEditModeOn ? : } 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | {isEditModeOn && ( 63 | 64 | { 67 | handleDelete( 68 | windowData, 69 | setWindowData, 70 | windowObj, 71 | windowItem["id"] 72 | ); 73 | }} 74 | > 75 | Delete 76 | 77 | 78 | )} 79 | 80 | ); 81 | } 82 | 83 | export default Editor; 84 | -------------------------------------------------------------------------------- /src/components/SettingsWindow/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import styled from "styled-components"; 3 | import { AppearanceTab, InfoTab, MiscTab } from "./Tabs"; 4 | import { useStore } from "../../Store"; 5 | import { TitleBarButton } from "../WindowTitleBar"; 6 | 7 | function SettingsWindow({ settingsData }) { 8 | const setIsSettingsShowing = useStore((state) => state.setIsSettingsShowing); 9 | const [currentTab, setCurrentTab] = useState("Appearance"); 10 | const imageInput = useRef(null); 11 | const colorInput = useRef(null); 12 | 13 | useEffect(() => { 14 | if (imageInput.current && colorInput.current) { 15 | imageInput.current.value = settingsData["backgroundImage"]; 16 | colorInput.current.value = settingsData["backgroundColor"]; 17 | } 18 | }, [imageInput, colorInput, settingsData]); 19 | 20 | const tabData = ["Appearance", "Miscellaneous", "Information"]; 21 | const tabs = tabData.map((tab, index) => { 22 | return ( 23 | 35 | ); 36 | }); 37 | 38 | return ( 39 | <> 40 | 41 |
42 |
Settings
43 |
44 | { 48 | setIsSettingsShowing(false); 49 | }} 50 | /> 51 |
52 |
53 |
54 | 59 | {tabs} 60 | 61 | 62 | {currentTab === "Appearance" && ( 63 | 64 | )} 65 | 66 | {currentTab === "Information" && } 67 | {currentTab === "Miscellaneous" && } 68 |
69 |
70 | { 72 | setIsSettingsShowing(false); 73 | }} 74 | /> 75 | 76 | ); 77 | } 78 | 79 | const Window = styled.div` 80 | width: 37rem; 81 | height: auto; 82 | position: absolute !important ; 83 | top: 50%; 84 | left: 50%; 85 | transform: translate(-50%, -50%); 86 | font-family: ${(props) => props.theme.fonts.primary}; 87 | z-index: 6 !important; 88 | @media only screen and (max-width: 768px) { 89 | width: 80% !important; 90 | } 91 | `; 92 | const GrayShade = styled.div` 93 | position: absolute; 94 | height: 100vh; 95 | width: 100vw; 96 | background: rgba(0, 0, 0, 0.5); 97 | z-index: 5; 98 | `; 99 | 100 | export default SettingsWindow; 101 | -------------------------------------------------------------------------------- /src/components/KanbanBoard/KanbanItem.jsx: -------------------------------------------------------------------------------- 1 | import DragIndicator from "../DragIndicator"; 2 | import { useSortable } from "@dnd-kit/sortable"; 3 | import styled, { css } from "styled-components"; 4 | import { useStore } from "../../Store"; 5 | 6 | function KanbanItem({ 7 | columnId, 8 | text = "🐛 🐛 🐛", 9 | items, 10 | setItems, 11 | index, 12 | id, 13 | }) { 14 | const { attributes, listeners, setNodeRef, transform } = useSortable({ 15 | id: id, 16 | }); 17 | const settingsData = useStore((state) => state.settingsData); 18 | const isEditModeOn = useStore((state) => state.isEditModeOn); 19 | 20 | const style = { 21 | transform: transform 22 | ? `translate3d(${transform.x}px, ${transform.y}px, 0)` 23 | : undefined, 24 | }; 25 | function updateKanbanItem(text) { 26 | const copyOfItems = { ...items }; 27 | copyOfItems[columnId][index].text = text; 28 | setItems(copyOfItems); 29 | } 30 | 31 | function deleteKanbanItem() { 32 | const copyOfItems = { ...items }; 33 | copyOfItems[columnId].splice(index, 1); 34 | setItems(copyOfItems); 35 | } 36 | 37 | return ( 38 | 39 | {isEditModeOn ? ( 40 |
41 | updateKanbanItem(e.target.value)} 45 | /> 46 | Delete Item 47 |
48 | ) : ( 49 | {text} 50 | )} 51 | 52 | 59 | 60 | 61 |
62 | ); 63 | } 64 | 65 | const KanbanItemContainer = styled.div` 66 | display: flex; 67 | margin: 0.5rem 0; 68 | align-items: center; 69 | ${(props) => 70 | props.windowsOS === 0 && 71 | css` 72 | background-color: #d5d1c1; 73 | `}; 74 | 75 | ${(props) => 76 | props.windowsOS === 1 && 77 | css` 78 | background-color: #bdbdbd; 79 | `}; 80 | 81 | ${(props) => 82 | props.windowsOS === 2 && 83 | css` 84 | background-color: #acd2e0; 85 | `}; 86 | `; 87 | 88 | const TextContainer = styled.p` 89 | width: calc(100% - 1.5rem); 90 | padding: 0.35rem; 91 | font-size: 0.8rem; 92 | white-space: pre-wrap; 93 | `; 94 | 95 | const KanbanTextArea = styled.textarea` 96 | font-size: 0.8rem; 97 | border: 1px solid rgb(127, 157, 185); 98 | width: 100%; 99 | resize: vertical; 100 | `; 101 | 102 | const DragHandle = styled.button` 103 | border: 0 !important; 104 | background: transparent !important; 105 | display: ${(props) => (!props.isEditModeOn ? "none" : "flex")}; 106 | padding: 0; 107 | box-shadow: none !important; 108 | align-items: center; 109 | margin-right: 0.2rem; 110 | `; 111 | 112 | const DeleteButton = styled.button` 113 | margin: 0.35rem 0 0.35rem 0.35rem; 114 | `; 115 | 116 | export default KanbanItem; 117 | -------------------------------------------------------------------------------- /src/components/Window/RenderWindowComponents.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | DndContext, 4 | closestCenter, 5 | KeyboardSensor, 6 | PointerSensor, 7 | useSensor, 8 | useSensors, 9 | } from "@dnd-kit/core"; 10 | import { 11 | arrayMove, 12 | SortableContext, 13 | sortableKeyboardCoordinates, 14 | verticalListSortingStrategy, 15 | } from "@dnd-kit/sortable"; 16 | 17 | import SortableItem from "./SortableItem"; 18 | import Editor from "../Editor"; 19 | import Image from "../Image"; 20 | import YouTubeVideo from "../YouTubeVideo"; 21 | import SearchBar from "../SearchBar"; 22 | import { replaceDesiredWindowItem } from "../../functions/helpers"; 23 | import { useStore } from "../../Store"; 24 | import KanbanBoard from "../KanbanBoard"; 25 | 26 | function getComponent(componentObj, windowItem) { 27 | if (componentObj["componentName"] === "Text") { 28 | return ; 29 | } else if (componentObj["componentName"] === "Image") { 30 | return ; 31 | } else if (componentObj["componentName"] === "YouTube Video") { 32 | return ; 33 | } else if (componentObj["componentName"] === "Search Bar") { 34 | return ; 35 | } else if (componentObj["componentName"] === "Kanban Board") { 36 | return ; 37 | } 38 | } 39 | 40 | function RenderWindowComponents({ componentsArr, windowItem }) { 41 | const windowData = useStore((state) => state.windowData); 42 | const setWindowData = useStore((state) => state.setWindowData); 43 | 44 | const sensors = useSensors( 45 | useSensor(PointerSensor), 46 | useSensor(KeyboardSensor, { 47 | coordinateGetter: sortableKeyboardCoordinates, 48 | }) 49 | ); 50 | const components = componentsArr 51 | .map((item) => item["id"]) 52 | .map((componentId, index) => { 53 | return ( 54 | 55 | {getComponent(componentsArr[index], windowItem)} 56 | 57 | ); 58 | }); 59 | 60 | return ( 61 | 67 | item["id"])} 70 | strategy={verticalListSortingStrategy} 71 | > 72 | {components} 73 | 74 | 75 | ); 76 | 77 | function handleDragEnd(event) { 78 | const { active, over } = event; 79 | 80 | if (active.id !== over.id) { 81 | let tempComponentsArr = [...componentsArr]; 82 | let tempWindowItem = { ...windowItem }; 83 | 84 | const oldIndex = componentsArr.findIndex((item) => item.id === active.id); 85 | const newIndex = componentsArr.findIndex((item) => item.id === over.id); 86 | tempComponentsArr = arrayMove(tempComponentsArr, oldIndex, newIndex); 87 | 88 | tempWindowItem["items"] = tempComponentsArr; 89 | let tempWindowData = [...windowData]; 90 | replaceDesiredWindowItem(tempWindowData, tempWindowItem); 91 | 92 | setWindowData(tempWindowData); 93 | } 94 | } 95 | } 96 | 97 | export default RenderWindowComponents; 98 | -------------------------------------------------------------------------------- /src/components/YouTubeVideo/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from "react"; 2 | import styled from "styled-components"; 3 | import { FlexContainer } from "../../styles/Layout"; 4 | import { DeleteButton, OptionsButton } from "../../styles/StyledComponents"; 5 | import { changeItemProperty, handleDelete } from "../Window/helper"; 6 | import BackButton from "../BackButton"; 7 | import { useStore } from "../../Store"; 8 | 9 | function YouTubeVideo({ windowObj, windowItem }) { 10 | const windowData = useStore((state) => state.windowData); 11 | const setWindowData = useStore((state) => state.setWindowData); 12 | 13 | const [isChangeUrlClicked, setIsChangedUrlClicked] = useState(false); 14 | const srcInput = useRef(null); 15 | const isEditModeOn = useStore((store) => store.isEditModeOn); 16 | 17 | function handleOptions() { 18 | if (isEditModeOn && !isChangeUrlClicked) { 19 | return ( 20 | 21 | 29 | 30 | ); 31 | } else if (isEditModeOn && isChangeUrlClicked) { 32 | return ( 33 | 34 | { 36 | setIsChangedUrlClicked(false); 37 | }} 38 | /> 39 | 45 | Set Video Link 46 | 47 | ); 48 | } 49 | } 50 | 51 | function setVideoSrc() { 52 | const inputText = srcInput.current.value.trim(); 53 | if (inputText !== "") { 54 | // Gets the youtube video id 55 | let regExp = 56 | /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/; 57 | let match = inputText.match(regExp); 58 | const videoId = match && match[7].length === 11 ? match[7] : false; 59 | const valueToInsert = "https://www.youtube.com/embed/" + videoId; 60 | changeItemProperty( 61 | windowObj, 62 | windowData, 63 | setWindowData, 64 | windowItem, 65 | "src", 66 | valueToInsert 67 | ); 68 | } 69 | } 70 | 71 | return ( 72 | 73 | {handleOptions()} 74 | 75 | 81 | 82 | 83 | {isEditModeOn && ( 84 | { 86 | handleDelete( 87 | windowData, 88 | setWindowData, 89 | windowObj, 90 | windowItem["id"] 91 | ); 92 | }} 93 | > 94 | Delete 95 | 96 | )} 97 | 98 | 99 | ); 100 | } 101 | 102 | const VideoContainer = styled.div` 103 | position: relative; 104 | width: 100%; 105 | padding-bottom: 56.25%; 106 | `; 107 | 108 | const VideoComponent = styled.iframe` 109 | position: absolute; 110 | top: 0; 111 | left: 0; 112 | width: 100%; 113 | height: 100%; 114 | border: 0; 115 | `; 116 | 117 | export default YouTubeVideo; 118 | -------------------------------------------------------------------------------- /cypress/e2e/start-menu.cy.js: -------------------------------------------------------------------------------- 1 | import chaiColors from "chai-colors"; 2 | chai.use(chaiColors); 3 | const blissImageUrl = 4 | "https://etesam.nyc3.cdn.digitaloceanspaces.com/Windows-XP-Newtab/images/bliss.jpg"; 5 | 6 | describe("Settings Tests", () => { 7 | it("Opens/closes menu and checks for correct menu items", () => { 8 | cy.visit("/"); 9 | cy.get("[data-cy='start-button']").as("startButton"); 10 | cy.get("@startButton").click(); 11 | cy.get("[data-cy='settings-menu-item']").as("settingsMenuItem"); 12 | cy.get("[data-cy='create-window-menu-item']").as("createWindowsMenuItem"); 13 | cy.get("[data-cy='add-icon-menu-item']").as("addIconMenuItem"); 14 | cy.get("[data-cy='edit-mode-menu-item']").as("editModeMenuItem"); 15 | 16 | cy.get("@startButton").click(); 17 | 18 | cy.get("@settingsMenuItem").should("not.exist"); 19 | cy.get("@createWindowsMenuItem").should("not.exist"); 20 | cy.get("@addIconMenuItem").should("not.exist"); 21 | cy.get("@editModeMenuItem").should("not.exist"); 22 | }); 23 | 24 | it("Opens settings and checks for components", () => { 25 | cy.visit("/"); 26 | cy.get("[data-cy='start-button']").as("startButton"); 27 | cy.get("@startButton").click(); 28 | cy.get("[data-cy='settings-menu-item']").click(); 29 | 30 | // Checking settings tabs 31 | cy.get("[data-cy='setting-tab-0']"); 32 | cy.get("[data-cy='setting-tab-1']"); 33 | cy.get("[data-cy='setting-tab-2']"); 34 | }); 35 | 36 | it("Checks and uses features of appearance tab", () => { 37 | cy.visit("/"); 38 | cy.get("[data-cy='start-button']").click(); 39 | cy.get("[data-cy='settings-menu-item']").click(); 40 | 41 | cy.get("[data-cy='remove-background-image-button']").click(); 42 | cy.get("[data-cy='document-body']").as("documentBody"); 43 | 44 | cy.get("@documentBody").should( 45 | "not.have.css", 46 | "background-image", 47 | `url("${blissImageUrl}")` 48 | ); 49 | 50 | cy.get("[data-cy='background-color-input']").as("backgroundColorInput"); 51 | cy.get("@backgroundColorInput").clear(); 52 | cy.get("@backgroundColorInput").type("#da5d5d"); 53 | cy.get("@documentBody") 54 | .should("have.css", "background-color") 55 | .and("be.colored", "#da5d5d"); 56 | 57 | cy.get("[data-cy='reset-background-image-button']").click(); 58 | cy.get("@documentBody").should( 59 | "have.css", 60 | "background-image", 61 | `url("${blissImageUrl}")` 62 | ); 63 | }); 64 | 65 | it("Checks and uses features of miscellaneous tab", () => { 66 | cy.visit("/"); 67 | cy.get("[data-cy='start-button']").click(); 68 | cy.get("[data-cy='settings-menu-item']").click(); 69 | cy.get("[data-cy='setting-tab-1']").click(); 70 | cy.get("[data-cy='dragging-grid']").as("draggingGrid"); 71 | cy.get("@draggingGrid").select("0px"); 72 | cy.get("@draggingGrid").select("15px"); 73 | cy.get("@draggingGrid").select("30px"); 74 | cy.get("@draggingGrid").select("45px"); 75 | }); 76 | 77 | it("Checks and uses features of info tab", () => { 78 | cy.visit("/"); 79 | cy.get("[data-cy='start-button']").click(); 80 | cy.get("[data-cy='settings-menu-item']").click(); 81 | cy.get("[data-cy='setting-tab-2']").click(); 82 | cy.get("[data-cy='github-text']"); 83 | cy.get("[data-cy='firefox-addon-text']"); 84 | cy.get("[data-cy='chrome-addon-text']"); 85 | }); 86 | }); 87 | 88 | describe("Create window test", () => { 89 | it("Creating new window", () => { 90 | cy.visit("/"); 91 | 92 | cy.get("[data-cy='window-title-display-0'") 93 | .as("defaultWindow") 94 | .should("have.text", "Insert Title Here"); 95 | 96 | cy.get("[data-cy='close-button-0']").click(); 97 | cy.get("@defaultWindow").should("not.exist"); 98 | cy.get("[data-cy='start-button']").click(); 99 | cy.get("[data-cy='create-window-menu-item']").click(); 100 | cy.get("@defaultWindow").should("have.text", "Insert Title Here"); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | //import windows98CSS from "xp.css/dist/98.css"; 2 | import React, { useEffect } from "react"; 3 | import styled, { 4 | createGlobalStyle, 5 | css, 6 | ThemeProvider, 7 | } from "styled-components"; 8 | import RenderWindows from "./data/RenderWindows"; 9 | import RenderIcons from "./data/RenderIcons"; 10 | import SettingsWindow from "./components/SettingsWindow"; 11 | import Startbar from "./components/Startbar"; 12 | import { theme } from "./styles/theme"; 13 | import { useStore } from "./Store"; 14 | import { toggleEditOnKeyPress } from "./functions/helpers"; 15 | import TopBanner from "./components/TopBanner"; 16 | 17 | function App() { 18 | const isSettingsShowing = useStore((state) => state.isSettingsShowing); 19 | const settingsData = useStore((state) => state.settingsData); 20 | const iconData = useStore((state) => state.iconData); 21 | const windowData = useStore((state) => state.windowData); 22 | 23 | const toggleEditMode = useStore((state) => state.toggleEditMode); 24 | 25 | useEffect(() => { 26 | document.addEventListener("keydown", (e) => 27 | toggleEditOnKeyPress(e, toggleEditMode) 28 | ); 29 | return () => { 30 | document.removeEventListener("keydown", (e) => 31 | toggleEditOnKeyPress(e, toggleEditMode) 32 | ); 33 | }; 34 | }, [toggleEditMode]); 35 | 36 | useEffect(() => { 37 | localStorage.setItem("windowData", JSON.stringify(windowData)); 38 | }, [windowData]); 39 | 40 | useEffect(() => { 41 | localStorage.setItem("iconData", JSON.stringify(iconData)); 42 | }, [iconData]); 43 | 44 | useEffect(() => { 45 | localStorage.setItem("settingsData", JSON.stringify(settingsData)); 46 | }, [settingsData]); 47 | 48 | return ( 49 | 50 | 55 | 60 | {isSettingsShowing && } 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | ); 69 | } 70 | 71 | const Wrapper = styled.div` 72 | height: calc(100vh - ${(props) => (props.windowsOS === 2 ? "42px" : "30px")}); 73 | width: 100vw; 74 | overflow: hidden; 75 | `; 76 | 77 | const GlobalStyle = createGlobalStyle` 78 | body { 79 | background-attachment: fixed; 80 | background-position: center center; 81 | background-size: cover; 82 | height: 100%; 83 | width: 100%; 84 | background: ${(props) => props.background}; 85 | background-image: url(${(props) => props.backgroundImage}) ; 86 | background-repeat: no-repeat; 87 | background-position: center center; 88 | background-attachment: fixed; 89 | background-size: cover; 90 | cursor: ${(props) => props.theme.cursors.auto}; 91 | overflow: hidden; 92 | position: fixed; 93 | 94 | 95 | } 96 | ::selection{ 97 | ${(props) => 98 | props.windowsOS === 0 && 99 | css` 100 | background: #1064cc; 101 | `}; 102 | 103 | ${(props) => 104 | props.windowsOS === 1 && 105 | css` 106 | background: #010080; 107 | `}; 108 | 109 | ${(props) => 110 | props.windowsOS === 2 && 111 | css` 112 | background: #1064cc; 113 | `}; 114 | 115 | 116 | color: white; 117 | } 118 | .window { 119 | font-size: 12px; 120 | } 121 | 122 | input { 123 | box-sizing: border-box; 124 | } 125 | 126 | p { 127 | margin: 0; 128 | } 129 | 130 | label { 131 | cursor: ${(props) => props.theme.cursors.auto}; 132 | } 133 | 134 | a, select, option, button { 135 | cursor: ${(props) => props.theme.cursors.pointer}; 136 | } 137 | 138 | button { 139 | color: black; 140 | } 141 | `; 142 | 143 | export default App; 144 | -------------------------------------------------------------------------------- /src/components/SearchBar/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { FlexContainer } from "../../styles/Layout"; 4 | import { MagnifyingGlass } from "../SvgMaster"; 5 | import { replaceDesiredWindowItem } from "../../functions/helpers"; 6 | import { handleDelete } from "../Window/helper"; 7 | import { useStore } from "../../Store"; 8 | 9 | function SearchBar({ windowItem, windowObj }) { 10 | const windowData = useStore((state) => state.windowData); 11 | const setWindowData = useStore((state) => state.setWindowData); 12 | 13 | const isEditModeOn = useStore((store) => store.isEditModeOn); 14 | function handleDropdown(e) { 15 | const dropdownValue = e.target.value; 16 | let tempWindowData = [...windowData]; 17 | let tempWindowObj = { ...windowObj }; 18 | let tempWindowItem = { ...windowItem }; 19 | tempWindowItem["engine"] = dropdownValue; 20 | if (dropdownValue === "Google") { 21 | tempWindowItem["action"] = "https://www.google.com/search"; 22 | } else if (dropdownValue === "DuckDuckGo") { 23 | tempWindowItem["action"] = "https://www.duckduckgo.com"; 24 | } else if (dropdownValue === "Bing") { 25 | tempWindowItem["action"] = "https://www.bing.com/search"; 26 | } 27 | replaceDesiredWindowItem(tempWindowObj["items"], tempWindowItem); 28 | replaceDesiredWindowItem(tempWindowData, tempWindowObj); 29 | setWindowData(tempWindowData); 30 | } 31 | 32 | return ( 33 | 34 | 35 | 40 | 41 | 42 | 43 | 44 | {isEditModeOn && ( 45 | 50 | 51 | 52 | 53 | 58 | 59 | 60 | 61 | 62 | 63 | 76 | 77 | )} 78 | 79 | ); 80 | } 81 | 82 | const SearchForm = styled.form` 83 | display: flex; 84 | flex-direction: column; 85 | justify-content: center; 86 | margin: 0.5rem 0; 87 | `; 88 | 89 | const SearchInput = styled.input` 90 | width: 100%; 91 | height: 1.8rem !important; 92 | padding: 0.15rem 1.6rem 0.15rem 0.25rem; 93 | box-sizing: content-box; 94 | font-size: 0.9rem; 95 | `; 96 | 97 | const SearchButton = styled.button` 98 | border: 0; 99 | padding: 0; 100 | background: transparent; 101 | box-shadow: none !important; 102 | display: flex; 103 | flex-direction: column; 104 | justify-content: center; 105 | align-items: flex-end; 106 | :hover { 107 | background: transparent !important; 108 | } 109 | 110 | :active { 111 | background: transparent !important; 112 | padding: 0 !important; 113 | } 114 | 115 | :focus { 116 | background: transparent; 117 | outline: 0 !important; 118 | } 119 | margin: 0 0.25rem; 120 | right: 1.25rem; 121 | min-width: 0; 122 | min-height: 0; 123 | width: 16px !important; 124 | height: 16px !important; 125 | `; 126 | 127 | const SearchDropdown = styled.select` 128 | width: min-content; 129 | margin-left: 0.35rem; 130 | `; 131 | export default SearchBar; 132 | -------------------------------------------------------------------------------- /src/components/TopBanner/index.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useEffect, useState } from "react"; 2 | import styled, { keyframes } from "styled-components"; 3 | 4 | function getBrowser() { 5 | const userAgent = navigator.userAgent; 6 | 7 | if (userAgent.indexOf("Chrome") > -1) { 8 | return "chrome" 9 | } else if (userAgent.indexOf("Firefox") > -1) { 10 | return "firefox" 11 | } else { 12 | return "other" 13 | } 14 | 15 | } 16 | 17 | function TopBanner() { 18 | const messages = [ 19 | "📝 Did you know that the text component supports markdown syntax?", 20 | "😉 It would be greatly appreciated if you could write a review for the extension: ", 21 | "✨ Did you know that you can switch the theme from Windows XP to Windows 98 or Windows 7 in the settings menu?", 22 | ]; 23 | const [shouldShowBanner, setShouldShowBanner] = useState(false); 24 | const [bannerIndex, setBannerIndex] = useState(""); 25 | 26 | useEffect(() => { 27 | const luckyNumber = parseInt((Math.random() * 10).toFixed()); 28 | if (luckyNumber === 1) { 29 | setShouldShowBanner(true); 30 | const messageIndex = parseInt( 31 | (Math.random() * (messages.length - 1)).toFixed() 32 | ); 33 | setBannerIndex(messageIndex); 34 | } 35 | }, [messages.length]); 36 | 37 | if (shouldShowBanner) { 38 | return ( 39 | 40 | 41 | {bannerIndex === 0 && messages[0]} 42 | {bannerIndex === 1 && ( 43 | 44 | {messages[1]} 45 | 46 | here 47 | 48 | 49 | )} 50 | {bannerIndex === 2 && messages[2]} 51 | 52 | setShouldShowBanner(false)} 55 | > 56 | 63 | 67 | 68 | 69 | 70 | ); 71 | } else { 72 | return ; 73 | } 74 | } 75 | 76 | const slideIn = keyframes` 77 | from{ 78 | transform: translateY(-50px); 79 | } 80 | to { 81 | transform: translateY(0px); 82 | } 83 | `; 84 | 85 | const Banner = styled.nav` 86 | background-color: rgba(10, 10, 10, 0.85); 87 | 88 | position: absolute; 89 | top: 0; 90 | width: 100%; 91 | padding: 0.5rem; 92 | z-index: 99; 93 | font-family: ${(props) => props.theme.fonts.primary}; 94 | box-sizing: border-box; 95 | 96 | animation: ${slideIn} 0.35s ease-in-out; 97 | `; 98 | 99 | const BannerText = styled.p` 100 | color: white; 101 | text-align: center; 102 | width: calc(100% - 1.5rem); 103 | user-select: text; 104 | `; 105 | 106 | const CloseButton = styled.button` 107 | position: absolute; 108 | right: 0.3rem; 109 | top: 0.35rem; 110 | padding: 0; 111 | background: 0; 112 | color: white; 113 | border: 0; 114 | box-shadow: none !important; 115 | :hover { 116 | box-shadow: none !important; 117 | background: transparent !important; 118 | } 119 | 120 | :active { 121 | box-shadow: none !important; 122 | background: transparent !important; 123 | } 124 | :focus { 125 | box-shadow: none !important; 126 | } 127 | `; 128 | 129 | const BannerLink = styled.a` 130 | color: #1db6e9; 131 | `; 132 | 133 | export default TopBanner; 134 | -------------------------------------------------------------------------------- /src/components/Startbar/StartWindow.js: -------------------------------------------------------------------------------- 1 | import { 2 | WindowsXPStartBody, 3 | WindowsXPStartFooter, 4 | WindowsXPStartHeader, 5 | WindowsXPStartWindow, 6 | Windows98StartWindow, 7 | Windows98BlueStripe, 8 | Windows98StartBody, 9 | Windows98BoldText, 10 | Windows7StartWindow, 11 | } from "./styles"; 12 | import startHeaderImg from "../../media/start-header.png"; 13 | import startFooterImg from "../../media/start-footer.png"; 14 | import React, { Fragment } from "react"; 15 | import { WindowsXPStartbarItem, Windows98StartbarItem } from "./items"; 16 | import { FlexContainer } from "../../styles/Layout"; 17 | 18 | function StartWindow({ windowsOS, startWindow, setIsStartWindowShowing }) { 19 | return ( 20 | 21 | {windowsOS === 0 && ( 22 | 23 | 24 | Administrator 25 | 26 | 27 | 32 | 37 | 42 | 47 | 48 | 49 | 50 | )} 51 | {windowsOS === 1 && ( 52 | 53 | 58 | 59 | Windows98 60 | 61 | 62 | 67 | 71 | 75 | 79 | 80 | 81 | 82 | )} 83 | 84 | {windowsOS === 2 && ( 85 | 86 | 87 | 92 | 97 | 102 | 107 | 108 | 109 | )} 110 | 111 | ); 112 | } 113 | export default StartWindow; 114 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | user-select: none; 6 | } 7 | 8 | code { 9 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 10 | monospace; 11 | } 12 | 13 | .selected { 14 | background-color: #2267cb; 15 | color: white; 16 | } 17 | 18 | .rich-text-editor{ 19 | padding: 0.25rem; 20 | margin: 0.5rem 0; 21 | } 22 | [contenteditable="true"] { 23 | border: 1px solid #9e9e9e; 24 | -webkit-user-select: text; 25 | user-select: text; 26 | cursor: text; 27 | background-color: white; 28 | } 29 | [contenteditable="false"] { 30 | cursor: url("https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/cursors/auto.cur"), auto !important; 31 | } 32 | 33 | .show { 34 | display: flex !important; 35 | } 36 | 37 | .hide { 38 | display: none !important; 39 | } 40 | 41 | .list-item-options { 42 | display: flex; 43 | justify-content: space-between; 44 | width: 100%; 45 | padding: 0.4rem 0; 46 | } 47 | 48 | .strikethrough { 49 | text-decoration: line-through; 50 | } 51 | 52 | .list-item { 53 | font-size: 13px; 54 | margin: 0.15rem 0; 55 | padding: 0.075rem 0; 56 | cursor: text; 57 | } 58 | 59 | button{ 60 | min-width: 0 !important; 61 | } 62 | 63 | 64 | .editor-underline{ 65 | text-decoration: underline; 66 | text-decoration-thickness: 2px; 67 | } 68 | .editor-strikethrough{ 69 | text-decoration: line-through; 70 | text-decoration-thickness: 2px; 71 | } 72 | .editor-text-underline-strikethrough { 73 | text-decoration: underline line-through; 74 | text-decoration-thickness: 2px; 75 | } 76 | .editor-paragraph{ 77 | font-size: 1rem; 78 | } 79 | 80 | .editor-text-bold { 81 | font-weight: bold; 82 | } 83 | 84 | .editor-text-italic { 85 | font-style: italic; 86 | } 87 | 88 | .editor-code { 89 | background-color: rgb(232, 234, 234); 90 | font-family: Menlo, Consolas, Monaco, monospace; 91 | display: block; 92 | padding: 0.5rem 1rem; 93 | line-height: 1.53; 94 | font-size: 13px; 95 | margin: 0.5rem 0; 96 | tab-size: 2; 97 | /* white-space: pre; */ 98 | overflow-x: auto; 99 | position: relative; 100 | } 101 | 102 | .link-editor{ 103 | position: absolute; 104 | z-index: 999; 105 | background-color: #f0f0f0; 106 | padding: 0.5rem; 107 | box-shadow: 10px 10px 65px 2px rgba(0, 0, 0, 0.39) 108 | } 109 | .link-input{ 110 | font-size: 1.1em; 111 | font-family: "MS Sans Serif", serif 112 | } 113 | .link-edit{ 114 | margin-left: 0.5rem; 115 | } 116 | 117 | .format{ 118 | display: block; 119 | height: 23px; 120 | background-repeat: no-repeat; 121 | background-position: center; 122 | } 123 | .toolbar{ 124 | display: flex; 125 | align-items: flex-start; 126 | justify-content: space-between; 127 | } 128 | .toolbar-item{ 129 | width: 36px; 130 | padding: 0; 131 | margin: 0.25rem 0.2rem; 132 | } 133 | 134 | h1, h2, h3, h4, h5, h6{ 135 | font-weight: 600; 136 | margin: 0 0 0.5rem 0 ; 137 | } 138 | 139 | h1{ 140 | font-size: 2.15em !important; 141 | } 142 | 143 | h2{ 144 | font-size: 1.65em !important; 145 | } 146 | h3{ 147 | font-size: 1.35em !important; 148 | } 149 | h4{ 150 | font-size: 1.15em !important; 151 | } 152 | h5{ 153 | font-size: 1em !important; 154 | } 155 | h6{ 156 | font-size: 0.8em !important; 157 | } 158 | 159 | .editor-tokenComment { 160 | color: slategray; 161 | } 162 | 163 | .editor-tokenPunctuation { 164 | color: #999; 165 | } 166 | 167 | .editor-tokenProperty { 168 | color: #905; 169 | } 170 | 171 | .editor-tokenSelector { 172 | color: #690; 173 | } 174 | 175 | .editor-tokenOperator { 176 | color: #9a6e3a; 177 | } 178 | 179 | .editor-tokenAttr { 180 | color: #07a; 181 | } 182 | 183 | .editor-tokenVariable { 184 | color: #e90; 185 | } 186 | 187 | .editor-tokenFunction { 188 | color: #dd4a68; 189 | } 190 | 191 | .editor-list-ol { 192 | padding: 0; 193 | margin: 0 0 0 1.5rem; 194 | } 195 | 196 | .editor-list-ul { 197 | padding: 0; 198 | margin: 0 0 0 1.5rem; 199 | list-style: disc; 200 | } 201 | 202 | .editor-listitem { 203 | font-size: 0.9rem; 204 | padding: 0.15rem 0; 205 | } 206 | 207 | .editor-nested-listitem { 208 | list-style-type: none; 209 | } 210 | button.active{ 211 | box-shadow: inset -1px -1px #fff,inset 1px 1px #0a0a0a,inset -2px -2px #dfdfdf,inset 2px 2px grey; 212 | } 213 | -------------------------------------------------------------------------------- /src/components/Window/helper.js: -------------------------------------------------------------------------------- 1 | import { 2 | getTranslateXY, 3 | replaceDesiredWindowItem, 4 | } from "../../functions/helpers"; 5 | 6 | export function handleComponentCreation( 7 | refToSearch, 8 | windowData, 9 | setWindowData, 10 | windowItem 11 | ) { 12 | const selectedComponent = getSelectedComponent(refToSearch); 13 | addComponent(selectedComponent, windowData, setWindowData, windowItem); 14 | } 15 | 16 | export function addComponent( 17 | componentToAdd, 18 | windowData, 19 | setWindowData, 20 | windowItem 21 | ) { 22 | let newItem = { ...windowItem }; 23 | let tempData = [...windowData]; 24 | let maxId = 0; 25 | for (let i = 0; i < newItem["items"].length; i++) { 26 | if (newItem["items"][i]["id"] > maxId) maxId = newItem["items"][i]["id"]; 27 | } 28 | if (componentToAdd === "Text") { 29 | newItem["items"].push({ 30 | id: maxId + 1, 31 | componentName: "Text", 32 | editorState: null, 33 | }); 34 | } else if (componentToAdd === "Image") { 35 | newItem["items"].push({ 36 | id: maxId + 1, 37 | componentName: "Image", 38 | href: null, 39 | src: "https://via.placeholder.com/300x175", 40 | justifyContent: "flex-start", 41 | imageWidth: "50%", 42 | }); 43 | } else if (componentToAdd === "YouTube Video") { 44 | newItem["items"].push({ 45 | id: maxId + 1, 46 | componentName: "YouTube Video", 47 | src: "https://www.youtube.com/embed/5pzM_pFNWak", 48 | }); 49 | } else if (componentToAdd === "Search Bar") { 50 | newItem["items"].push({ 51 | id: maxId + 1, 52 | componentName: "Search Bar", 53 | engine: "Google", 54 | action: "https://www.google.com/search", 55 | }); 56 | } else if (componentToAdd === "Kanban Board") { 57 | newItem["items"].push({ 58 | id: maxId + 1, 59 | componentName: "Kanban Board", 60 | columnHeaders: { 61 | A: "To Do", 62 | B: "Doing", 63 | C: "Done", 64 | }, 65 | items: { 66 | A: [ 67 | { id: 1, text: "🐶 Walk the dog" }, 68 | { id: 2, text: "🖊 Give this app a review" }, 69 | ], 70 | B: [{ id: 3, text: "📝 Finish my homework" }], 71 | C: [], 72 | }, 73 | }); 74 | } 75 | replaceDesiredWindowItem(tempData, newItem); 76 | setWindowData(tempData); 77 | } 78 | 79 | // For radio buttons 80 | export function getSelectedComponent(componentsParent) { 81 | const components = componentsParent.current.children; 82 | // Starts at 1 to skip the "Select One" text. Ends at components.length-1 to skip the add component button 83 | for (let i = 1; i < components.length - 1; i++) { 84 | const currentComponent = components[i]; 85 | const componentChildren = currentComponent.children; 86 | const radioInput = componentChildren[0]; 87 | const radioLabel = componentChildren[1]; 88 | if (radioInput.checked) { 89 | return radioLabel.innerText; 90 | } 91 | } 92 | return null; 93 | } 94 | 95 | export function setDataProperty( 96 | data, 97 | setData, 98 | item, 99 | propertyName, 100 | propertyValue 101 | ) { 102 | const tempData = [...data]; 103 | const itemToInsert = { ...item }; 104 | if (propertyName === "position") { 105 | // Property value is the window ref in this case 106 | const positions = getTranslateXY(propertyValue.current); 107 | const xPos = positions["translateX"]; 108 | const yPos = positions["translateY"]; 109 | itemToInsert["xCoord"] = xPos; 110 | itemToInsert["yCoord"] = yPos; 111 | } else { 112 | itemToInsert[propertyName] = propertyValue; 113 | } 114 | 115 | replaceDesiredWindowItem(tempData, itemToInsert); 116 | 117 | setData(tempData); 118 | return tempData; 119 | } 120 | 121 | export function changeItemProperty( 122 | windowObj, 123 | windowData, 124 | setWindowData, 125 | windowItem, 126 | propertyName, 127 | propertyValue 128 | ) { 129 | let tempWindowData = [...windowData]; 130 | let tempWindow = { ...windowObj }; 131 | let items = tempWindow["items"]; 132 | let tempWindowItem = { ...windowItem }; 133 | tempWindowItem[propertyName] = propertyValue; 134 | replaceDesiredWindowItem(items, tempWindowItem); 135 | replaceDesiredWindowItem(tempWindowData, tempWindow); 136 | 137 | setWindowData(tempWindowData); 138 | } 139 | 140 | // For deleting an item 141 | export function handleDelete(windowData, setWindowData, windowItem, id) { 142 | const tempItem = { ...windowItem }; 143 | tempItem["items"] = tempItem["items"].filter((item) => item.id !== id); 144 | setDataProperty( 145 | windowData, 146 | setWindowData, 147 | windowItem, 148 | "items", 149 | tempItem["items"] 150 | ); 151 | } 152 | 153 | export function handleResize(setHeight, setWidth, size) { 154 | setHeight(size.height); 155 | setWidth(size.width); 156 | } 157 | -------------------------------------------------------------------------------- /src/components/Icon/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import styled, { withTheme } from "styled-components"; 3 | import Draggable from "react-draggable"; 4 | import { setDataProperty } from "../Window/helper"; 5 | import { deleteDataItem } from "../../functions/helpers"; 6 | import { useStore } from "../../Store"; 7 | 8 | function Index({ iconItem, theme }) { 9 | const iconData = useStore((state) => state.iconData); 10 | const setIconData = useStore((state) => state.setIconData); 11 | const iconRef = useRef(null); 12 | 13 | const settingsData = useStore((state) => state.settingsData); 14 | const isEditModeOn = useStore((store) => store.isEditModeOn); 15 | function handleDoubleClick() { 16 | if (!isEditModeOn) { 17 | document.body.style.cursor = theme.cursors.wait; 18 | window.location = iconItem["redirect"]; 19 | } 20 | } 21 | 22 | return ( 23 | { 34 | setDataProperty(iconData, setIconData, iconItem, "position", iconRef); 35 | }} 36 | > 37 | 38 | {isEditModeOn ? ( 39 |
40 | Img Url 41 | { 44 | setDataProperty( 45 | iconData, 46 | setIconData, 47 | iconItem, 48 | "src", 49 | e.target.value 50 | ); 51 | }} 52 | /> 53 |
54 | ) : ( 55 | 56 | )} 57 | 58 | {isEditModeOn ? ( 59 |
60 | Icon Title 61 | { 64 | setDataProperty( 65 | iconData, 66 | setIconData, 67 | iconItem, 68 | "title", 69 | e.target.value 70 | ); 71 | }} 72 | /> 73 |
74 | ) : ( 75 | {iconItem["title"]} 76 | )} 77 | {isEditModeOn && ( 78 |
79 |
80 | Redirect Url 81 | { 84 | setDataProperty( 85 | iconData, 86 | setIconData, 87 | iconItem, 88 | "redirect", 89 | e.target.value 90 | ); 91 | }} 92 | /> 93 |
94 | 95 | 102 | 103 |
104 | )} 105 |
106 |
107 | ); 108 | } 109 | 110 | const IconWrapper = styled.div` 111 | position: absolute; 112 | z-index: 1; 113 | left: 1rem; 114 | top: 1rem; 115 | display: flex; 116 | flex-direction: column; 117 | align-items: center; 118 | 119 | :focus { 120 | outline: 1px dotted black; 121 | } 122 | `; 123 | 124 | const IconInput = styled.input` 125 | margin-top: 0.25rem; 126 | `; 127 | 128 | const IconTextArea = styled.textarea` 129 | text-align: center; 130 | margin-top: 0.25rem; 131 | resize: none; 132 | `; 133 | 134 | const IconImg = styled.img` 135 | height: 48px; 136 | width: auto; 137 | pointer-events: none; 138 | `; 139 | 140 | const DeleteRow = styled.section` 141 | display: flex; 142 | justify-content: center; 143 | padding-top: 0.25rem; 144 | `; 145 | 146 | const IconText = styled.p` 147 | font-family: ${(props) => props.theme.fonts.primary}; 148 | margin-top: 0.35rem; 149 | color: white; 150 | text-align: center; 151 | width: auto; 152 | max-width: 100px; 153 | max-height: 43px; 154 | display: -webkit-box; 155 | -webkit-box-orient: vertical; 156 | overflow: hidden; 157 | -webkit-line-clamp: 3; 158 | text-overflow: ellipsis; 159 | text-shadow: 1.25px 1.2px 1px #000000; 160 | `; 161 | 162 | export default withTheme(Index); 163 | -------------------------------------------------------------------------------- /cypress/e2e/windows.cy.js: -------------------------------------------------------------------------------- 1 | const updatedWindowTitle = "🎉 This is a modified window title"; 2 | const blissImageUrl = 3 | "https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/images/bliss.jpg"; 4 | 5 | describe("Basic Window Tests", () => { 6 | it("Gets the default window", () => { 7 | cy.visit("/"); 8 | cy.get("[data-cy='window-title-display-0'").should( 9 | "have.text", 10 | "Insert Title Here" 11 | ); 12 | }); 13 | it("Moves the default window", () => { 14 | cy.visit("/"); 15 | cy.get("[data-cy='window-title-bar-0'").as("titleBar"); 16 | cy.get("@titleBar") 17 | .trigger("mouseover") 18 | .trigger("mousedown", { which: 1 }) 19 | .trigger("mousemove", { 20 | eventConstructor: "MouseEvent", 21 | clientX: 500, 22 | clientY: 100, 23 | }) 24 | .trigger("mouseup", { which: 1 }); 25 | 26 | cy.get("[data-cy='window-0']").should( 27 | "have.css", 28 | "transform", 29 | "matrix(1, 0, 0, 1, 260, 86)" 30 | ); 31 | }); 32 | it("Renames the default window", () => { 33 | cy.get("[data-cy='start-button']").as("startButton").click(); 34 | cy.get("[data-cy='edit-mode-menu-item']").as("editModeButton"); 35 | cy.get("@editModeButton").click(); 36 | cy.get("[data-cy='window-title-edit-0'").clear().type(updatedWindowTitle); 37 | 38 | cy.get("@startButton").click(); 39 | cy.get("@editModeButton").click(); 40 | cy.get("[data-cy='window-title-display-0'").should( 41 | "have.text", 42 | updatedWindowTitle 43 | ); 44 | cy.get("[data-cy='tab-0']").should("have.text", updatedWindowTitle); 45 | cy.get("@editModeButton").click(); 46 | }); 47 | 48 | it("Tests window minimize/maximize/close functionality", () => { 49 | cy.get("[data-cy='maximize-button-0']").as("maximizeButton"); 50 | cy.get("[data-cy='minimize-button-0']").as("minimizeButton"); 51 | cy.get("[data-cy='close-button-0']").as("closeButton"); 52 | 53 | cy.get("@maximizeButton").click(); 54 | cy.get("[data-cy='window-0']").as("window"); 55 | cy.get("@window").should("have.css", "width", "1000px"); 56 | cy.get("@window").should("have.css", "height", "628px"); 57 | 58 | cy.get("body").then(($body) => { 59 | const bannerButton = $body.find("button[data-cy=close-banner-button]"); 60 | if (bannerButton.length > 0) { 61 | bannerButton.click(); 62 | //evaluates as true 63 | } 64 | }); 65 | 66 | cy.get("@maximizeButton").click(); 67 | cy.get("@window").should("have.css", "width", "480px"); 68 | 69 | cy.get("@minimizeButton").click(); 70 | cy.get("@window").should("not.be.visible"); 71 | cy.get("[data-cy='tab-0']").as("windowTab"); 72 | cy.get("@windowTab").click(); 73 | cy.get("@window").should("be.visible"); 74 | 75 | cy.get("@closeButton").click(); 76 | cy.get("@window").should("not.exist"); 77 | cy.get("@windowTab").should("not.exist"); 78 | }); 79 | 80 | it("Creates a window", () => { 81 | cy.get("[data-cy='start-button']").as("startButton").click(); 82 | cy.get("[data-cy='create-window-menu-item']").click(); 83 | }); 84 | }); 85 | describe("Image Tests", () => { 86 | it("Creates an image", () => { 87 | cy.get("[data-cy='image-option-0']").click(); 88 | cy.get("[data-cy='add-component-button-0']").click(); 89 | cy.get("[data-cy='align-image-1']"); 90 | cy.get("[data-cy='image-delete-1']"); 91 | cy.get("[data-cy='set-image-url-1']"); 92 | cy.get("[data-cy='image-size-slider-1']"); 93 | }); 94 | 95 | it("Changes align property of image", () => { 96 | cy.get("[data-cy='align-image-1']").as("imageAlign"); 97 | cy.get("[data-cy='image-1']"); 98 | cy.get("[data-cy='image-container-1']") 99 | .as("imageContainer") 100 | .should("have.css", "justifyContent", "flex-start"); 101 | cy.get("@imageAlign").select("center"); 102 | cy.get("@imageContainer").should("have.css", "justifyContent", "center"); 103 | cy.get("@imageAlign").select("right"); 104 | cy.get("@imageContainer").should("have.css", "justifyContent", "flex-end"); 105 | }); 106 | 107 | it("Changes Image Url", () => { 108 | cy.get("[data-cy='set-image-url-1']").click(); 109 | cy.get("[data-cy='image-url-input-1']") 110 | .as("imageUrlInput") 111 | .clear() 112 | .type(blissImageUrl); 113 | cy.get("[data-cy='set-image-url-update-1']").click(); 114 | cy.get("[data-cy='image-1']") 115 | .invoke("attr", "src") 116 | .should("eq", blissImageUrl); 117 | cy.get("[data-cy='image-back-button-1']").as("backButton").click(); 118 | cy.get("@imageUrlInput").should("not.exist"); 119 | cy.get("[data-cy='set-image-url-update-1']").should("not.exist"); 120 | cy.get("@backButton").should("not.exist"); 121 | }); 122 | // it("Changes size of image", () => { 123 | // cy.get("[data-cy='image-size-slider-0']").as("imageSlider"); 124 | // }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/components/Editor/Plugins/ToolbarPlugins/BlockOptions.js: -------------------------------------------------------------------------------- 1 | import { 2 | $createParagraphNode, 3 | $getSelection, 4 | $isRangeSelection, 5 | } from "lexical"; 6 | import { $wrapLeafNodesInElements } from "@lexical/selection"; 7 | import { 8 | INSERT_ORDERED_LIST_COMMAND, 9 | INSERT_UNORDERED_LIST_COMMAND, 10 | REMOVE_LIST_COMMAND, 11 | } from "@lexical/list"; 12 | import { $createHeadingNode } from "@lexical/rich-text"; 13 | import { $createCodeNode } from "@lexical/code"; 14 | import { Fragment, useEffect, useRef } from "react"; 15 | import styled from "styled-components"; 16 | 17 | function BlockOptions() { 18 | return ( 19 | 20 | 23 | 26 | 29 | 32 | 35 | 38 | 41 | 44 | 47 | 50 | 51 | ); 52 | } 53 | 54 | export function BlockOptionsDropdownList({ editor, blockType }) { 55 | const dropdownRef = useRef(null); 56 | 57 | const formatParagraph = () => { 58 | if (blockType !== "paragraph") { 59 | editor.update(() => { 60 | const selection = $getSelection(); 61 | 62 | if ($isRangeSelection(selection)) { 63 | $wrapLeafNodesInElements(selection, () => $createParagraphNode()); 64 | } 65 | }); 66 | } 67 | }; 68 | 69 | const formatHeader = (headerTag) => { 70 | if (blockType !== headerTag) { 71 | editor.update(() => { 72 | const selection = $getSelection(); 73 | 74 | if ($isRangeSelection(selection)) { 75 | $wrapLeafNodesInElements(selection, () => 76 | $createHeadingNode(headerTag) 77 | ); 78 | } 79 | }); 80 | } 81 | }; 82 | 83 | const formatBulletList = () => { 84 | if (blockType !== "ul") { 85 | editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND); 86 | } else { 87 | editor.dispatchCommand(REMOVE_LIST_COMMAND); 88 | } 89 | }; 90 | 91 | const formatNumberedList = () => { 92 | if (blockType !== "ol") { 93 | editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND); 94 | } else { 95 | editor.dispatchCommand(REMOVE_LIST_COMMAND); 96 | } 97 | }; 98 | 99 | const formatCode = () => { 100 | if (blockType !== "code") { 101 | editor.update(() => { 102 | const selection = $getSelection(); 103 | if ($isRangeSelection(selection)) { 104 | $wrapLeafNodesInElements(selection, () => $createCodeNode()); 105 | } 106 | }); 107 | } 108 | }; 109 | 110 | // useEffect for blockType that modified selectIndex when blockType changes 111 | 112 | useEffect(() => { 113 | if (dropdownRef && dropdownRef.current) { 114 | const children = dropdownRef.current.children; 115 | for (let i = 0; i < children.length; i++) { 116 | if (children[i].value === blockTypeToBlockName[blockType]) { 117 | dropdownRef.current.selectedIndex = i + ""; 118 | } 119 | } 120 | } 121 | }, [blockType, dropdownRef]); 122 | 123 | return ( 124 | 125 | { 128 | if (e.target.value === "normal") formatParagraph(); 129 | else if (e.target.value === "h1") formatHeader("h1"); 130 | else if (e.target.value === "h2") formatHeader("h2"); 131 | else if (e.target.value === "h3") formatHeader("h3"); 132 | else if (e.target.value === "h4") formatHeader("h4"); 133 | else if (e.target.value === "h5") formatHeader("h5"); 134 | else if (e.target.value === "h6") formatHeader("h6"); 135 | else if (e.target.value === "bulleted-list") formatBulletList(); 136 | else if (e.target.value === "numbered-list") formatNumberedList(); 137 | else if (e.target.value === "code-block") formatCode(); 138 | }} 139 | aria-label="Formatting Options" 140 | > 141 | 142 | 143 | 144 | ); 145 | } 146 | 147 | const blockTypeToBlockName = { 148 | code: "code-block", 149 | h1: "h1", 150 | h2: "h2", 151 | h3: "h3", 152 | h4: "h4", 153 | h5: "h5", 154 | h6: "h6", 155 | ol: "numbered-list", 156 | ul: "bulleted-list", 157 | paragraph: "normal", 158 | }; 159 | 160 | const BlockOptionsDropdown = styled.select` 161 | width: 6rem; 162 | padding: 0 0.25rem; 163 | `; 164 | -------------------------------------------------------------------------------- /src/components/WindowTitleBar/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { setDataProperty } from "../Window/helper"; 3 | import maximizeSecond from "../../media/maximize-second.png"; 4 | import { deleteDataItem } from "../../functions/helpers"; 5 | import { useStore } from "../../Store"; 6 | import styled, { css } from "styled-components"; 7 | 8 | function WindowTitleBar({ windowItem, windowId }) { 9 | const isEditModeOn = useStore((state) => state.isEditModeOn); 10 | const focusedWindow = useStore((state) => state.focusedWindow); 11 | const windowData = useStore((state) => state.windowData); 12 | const setWindowData = useStore((state) => state.setWindowData); 13 | const settingsData = useStore((state) => state.settingsData); 14 | 15 | return ( 16 | 21 | {isEditModeOn ? ( 22 | { 28 | setDataProperty( 29 | windowData, 30 | setWindowData, 31 | windowItem, 32 | "windowTitle", 33 | e.target.value 34 | ); 35 | }} 36 | /> 37 | ) : ( 38 |
42 | {windowItem["windowTitle"]} 43 |
44 | )} 45 | 46 | 50 | { 55 | setDataProperty( 56 | windowData, 57 | setWindowData, 58 | windowItem, 59 | "hidden", 60 | true 61 | ); 62 | }} 63 | /> 64 | { 75 | setDataProperty( 76 | windowData, 77 | setWindowData, 78 | windowItem, 79 | "isMaximized", 80 | !windowItem["isMaximized"] 81 | ); 82 | }} 83 | /> 84 | { 89 | deleteDataItem(windowData, setWindowData, windowItem); 90 | }} 91 | /> 92 | 93 |
94 | ); 95 | } 96 | 97 | const TitleBar = styled.div` 98 | cursor: ${(props) => props.theme.cursors.move}; 99 | ${(props) => 100 | props.notFocused && 101 | css` 102 | background: linear-gradient( 103 | 180deg, 104 | #9db4f6, 105 | #8296e3 8%, 106 | #8394e0 40%, 107 | #8da6eb 88%, 108 | #8da6eb 93%, 109 | #a3b5e6 95%, 110 | #93bbdd 96%, 111 | #a8c0ff 112 | ) !important; 113 | border: 0 !important; 114 | `} 115 | `; 116 | 117 | const TitleInput = styled.input` 118 | color: black !important; 119 | font-weight: 700; 120 | font-size: 13px; 121 | width: 100%; 122 | padding: 0 0 0 3px !important; 123 | `; 124 | 125 | const ControlButtons = styled.div` 126 | filter: ${(props) => props.notFocused && "contrast(50%) brightness(120%)"}; 127 | pointer-events: ${(props) => props.notFocused && "none"}; 128 | `; 129 | 130 | export const TitleBarButton = styled.button` 131 | ${(props) => 132 | props.windowsOS === 0 && 133 | css` 134 | height: 22px; 135 | width: 22px; 136 | `} 137 | 138 | ${(props) => 139 | props.windowsOS === 1 && 140 | css` 141 | height: 14px; 142 | width: 16px; 143 | `} 144 | 145 | ${(props) => 146 | props.windowsOS === 2 && 147 | css` 148 | height: 19px; 149 | width: 29px; 150 | `} 151 | `; 152 | 153 | const MaximizeButton = styled(TitleBarButton)` 154 | ${(props) => 155 | props.windowsOS === 0 && 156 | css` 157 | background-image: ${(props) => 158 | props.isMaximized && `url(${props.maximizeSecond})`} !important; 159 | 160 | :hover { 161 | filter: ${(props) => props.isMaximized && "brightness(120%)"}; 162 | } 163 | 164 | :hover:active { 165 | filter: ${(props) => props.isMaximized && "brightness(90%)"}; 166 | } 167 | `} 168 | `; 169 | 170 | export default WindowTitleBar; 171 | -------------------------------------------------------------------------------- /src/components/Editor/Plugins/ToolbarPlugins/FloatingLinkEditor.js: -------------------------------------------------------------------------------- 1 | import { 2 | $getSelection, 3 | $isRangeSelection, 4 | SELECTION_CHANGE_COMMAND, 5 | } from "lexical"; 6 | import { useCallback, useEffect, useRef, useState } from "react"; 7 | import { getSelectedNode } from "./index"; 8 | import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"; 9 | import { mergeRegister } from "@lexical/utils"; 10 | const LowPriority = 1; 11 | 12 | function positionEditorElement(editor, rect) { 13 | if (rect === null) { 14 | editor.style.opacity = "0"; 15 | editor.style.top = "-1000px"; 16 | editor.style.left = "-1000px"; 17 | } else { 18 | editor.style.opacity = "1"; 19 | editor.style.top = `${rect.top + rect.height + window.pageYOffset + 10}px`; 20 | editor.style.left = `${ 21 | rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2 22 | }px`; 23 | } 24 | } 25 | 26 | function FloatingLinkEditor({ editor }) { 27 | const editorRef = useRef(null); 28 | const inputRef = useRef(null); 29 | const mouseDownRef = useRef(false); 30 | const [linkUrl, setLinkUrl] = useState(""); 31 | const [isEditMode, setEditMode] = useState(false); 32 | const [lastSelection, setLastSelection] = useState(null); 33 | 34 | const updateLinkEditor = useCallback(() => { 35 | const selection = $getSelection(); 36 | if ($isRangeSelection(selection)) { 37 | const node = getSelectedNode(selection); 38 | const parent = node.getParent(); 39 | if ($isLinkNode(parent)) { 40 | setLinkUrl(parent.getURL()); 41 | } else if ($isLinkNode(node)) { 42 | setLinkUrl(node.getURL()); 43 | } else { 44 | setLinkUrl(""); 45 | } 46 | } 47 | const editorElem = editorRef.current; 48 | const nativeSelection = window.getSelection(); 49 | const activeElement = document.activeElement; 50 | 51 | if (editorElem === null) { 52 | return; 53 | } 54 | 55 | const rootElement = editor.getRootElement(); 56 | if ( 57 | selection !== null && 58 | !nativeSelection.isCollapsed && 59 | rootElement !== null && 60 | rootElement.contains(nativeSelection.anchorNode) 61 | ) { 62 | const domRange = nativeSelection.getRangeAt(0); 63 | let rect; 64 | if (nativeSelection.anchorNode === rootElement) { 65 | let inner = rootElement; 66 | while (inner.firstElementChild != null) { 67 | inner = inner.firstElementChild; 68 | } 69 | rect = inner.getBoundingClientRect(); 70 | } else { 71 | rect = domRange.getBoundingClientRect(); 72 | } 73 | 74 | if (!mouseDownRef.current) { 75 | positionEditorElement(editorElem, rect); 76 | } 77 | setLastSelection(selection); 78 | } else if (!activeElement || activeElement.className !== "link-input") { 79 | positionEditorElement(editorElem, null); 80 | setLastSelection(null); 81 | setEditMode(false); 82 | setLinkUrl(""); 83 | } 84 | 85 | return true; 86 | }, [editor]); 87 | 88 | useEffect(() => { 89 | return mergeRegister( 90 | editor.registerUpdateListener(({ editorState }) => { 91 | editorState.read(() => { 92 | updateLinkEditor(); 93 | }); 94 | }), 95 | 96 | editor.registerCommand( 97 | SELECTION_CHANGE_COMMAND, 98 | () => { 99 | updateLinkEditor(); 100 | return true; 101 | }, 102 | LowPriority 103 | ) 104 | ); 105 | }, [editor, updateLinkEditor]); 106 | 107 | useEffect(() => { 108 | editor.getEditorState().read(() => { 109 | updateLinkEditor(); 110 | }); 111 | }, [editor, updateLinkEditor]); 112 | 113 | useEffect(() => { 114 | if (isEditMode && inputRef.current) { 115 | inputRef.current.focus(); 116 | } 117 | }, [isEditMode]); 118 | 119 | return ( 120 |
121 | {isEditMode ? ( 122 | { 127 | setLinkUrl(event.target.value); 128 | }} 129 | onKeyDown={(event) => { 130 | if (event.key === "Enter") { 131 | event.preventDefault(); 132 | if (lastSelection !== null) { 133 | if (linkUrl !== "") { 134 | editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl); 135 | } 136 | setEditMode(false); 137 | } 138 | } else if (event.key === "Escape") { 139 | event.preventDefault(); 140 | setEditMode(false); 141 | } 142 | }} 143 | /> 144 | ) : ( 145 | <> 146 |
147 | 148 | {linkUrl} 149 | 150 | 160 |
161 | 162 | )} 163 |
164 | ); 165 | } 166 | 167 | export default FloatingLinkEditor; 168 | -------------------------------------------------------------------------------- /src/components/Startbar/items.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Toggle from "../Toggle"; 3 | import { 4 | StartItemIcon, 5 | StartItemName, 6 | Windows98ItemContainer, 7 | WindowsXPItemContainer, 8 | } from "./styles"; 9 | import { FlexContainer } from "../../styles/Layout"; 10 | import newWindowImg from "../../media/new-window-icon.png"; 11 | import newIconImg from "../../media/new-icon-icon.png"; 12 | import { addDataItem } from "../../functions/helpers"; 13 | import { useStore } from "../../Store"; 14 | 15 | function handleClick( 16 | identifier, 17 | toggleEditMode, 18 | setIsSettingsShowing, 19 | setIsStartWindowShowing, 20 | windowData, 21 | setWindowData, 22 | setFocusedWindow, 23 | iconData, 24 | setIconData 25 | ) { 26 | if (identifier === "Edit Mode") toggleEditMode(); 27 | else if (identifier === "Settings") { 28 | setIsSettingsShowing(true); 29 | setIsStartWindowShowing(false); 30 | } else if (identifier === "Create A New Window") { 31 | addDataItem(windowData, setWindowData, "window", setFocusedWindow); 32 | setIsStartWindowShowing(false); 33 | } else if (identifier === "Add Icon") { 34 | addDataItem(iconData, setIconData, "icon"); 35 | setIsStartWindowShowing(false); 36 | } 37 | } 38 | 39 | export function WindowsXPStartbarItem({ 40 | identifier, 41 | setIsStartWindowShowing, 42 | dataCy, 43 | }) { 44 | const windowData = useStore((state) => state.windowData); 45 | const setWindowData = useStore((state) => state.setWindowData); 46 | 47 | const iconData = useStore((state) => state.iconData); 48 | const setIconData = useStore((state) => state.setIconData); 49 | 50 | const setFocusedWindow = useStore((state) => state.setFocusedWindow); 51 | const isEditModeOn = useStore((state) => state.isEditModeOn); 52 | const toggleEditMode = useStore((state) => state.toggleEditMode); 53 | const setIsSettingsShowing = useStore((state) => state.setIsSettingsShowing); 54 | 55 | return ( 56 | 58 | handleClick( 59 | identifier, 60 | toggleEditMode, 61 | setIsSettingsShowing, 62 | setIsStartWindowShowing, 63 | windowData, 64 | setWindowData, 65 | setFocusedWindow, 66 | iconData, 67 | setIconData 68 | ) 69 | } 70 | data-cy={dataCy} 71 | > 72 | 77 | {identifier === "Edit Mode" && ( 78 | 79 | )} 80 | {identifier === "Settings" && ( 81 | 86 | )} 87 | {identifier === "Create A New Window" && ( 88 | 89 | )} 90 | {identifier === "Add Icon" && ( 91 | 92 | )} 93 | {identifier} 94 | 95 | 96 | ); 97 | } 98 | 99 | export function Windows98StartbarItem({ identifier, setIsStartWindowShowing }) { 100 | const windowData = useStore((state) => state.windowData); 101 | const setWindowData = useStore((state) => state.setWindowData); 102 | 103 | const iconData = useStore((state) => state.iconData); 104 | const setIconData = useStore((state) => state.setIconData); 105 | 106 | const setFocusedWindow = useStore((state) => state.setFocusedWindow); 107 | const isEditModeOn = useStore((state) => state.isEditModeOn); 108 | const toggleEditMode = useStore((state) => state.toggleEditMode); 109 | const setIsSettingsShowing = useStore((state) => state.setIsSettingsShowing); 110 | 111 | return ( 112 | 114 | handleClick( 115 | identifier, 116 | toggleEditMode, 117 | setIsSettingsShowing, 118 | setIsStartWindowShowing, 119 | windowData, 120 | setWindowData, 121 | setFocusedWindow, 122 | iconData, 123 | setIconData 124 | ) 125 | } 126 | > 127 | 133 | {identifier === "Edit Mode" && ( 134 | 135 | )} 136 | {identifier === "Settings" && ( 137 | 142 | )} 143 | {identifier === "Create A New Window" && ( 144 | 145 | )} 146 | {identifier === "Add Icon" && ( 147 | 148 | )} 149 | {identifier} 150 | 151 | 152 | ); 153 | } 154 | -------------------------------------------------------------------------------- /src/functions/helpers.js: -------------------------------------------------------------------------------- 1 | export function getDefaultValue( 2 | localStorageProperty = "", 3 | lookForStoredValue = true, 4 | id = 0 5 | ) { 6 | let defaultValue = false; 7 | if (localStorageProperty === "windowData") { 8 | defaultValue = [ 9 | { 10 | id: id, 11 | windowTitle: "Insert Title Here", 12 | xCoord: 30, 13 | yCoord: 30, 14 | hidden: false, 15 | items: [], 16 | isMaximized: false, 17 | size: { 18 | width: 480, 19 | height: 70, 20 | }, 21 | }, 22 | ]; 23 | } else if (localStorageProperty === "iconData") { 24 | defaultValue = [ 25 | { 26 | id: id, 27 | src: "https://via.placeholder.com/48", 28 | title: "Insert Title Here", 29 | xCoord: 30, 30 | yCoord: 30, 31 | redirect: "/", 32 | }, 33 | ]; 34 | } else if (localStorageProperty === "settingsData") { 35 | defaultValue = { 36 | backgroundColor: "#ffffff", 37 | backgroundImage: 38 | "https://etesam.nyc3.cdn.digitaloceanspaces.com/Windows-XP-Newtab/images/bliss.jpg", 39 | draggingGrid: "0px", 40 | stylesheet: "https://unpkg.com/xp.css", 41 | // 0 -> windowsXP; 1-> windows98; 2-> windows 7 42 | windowsOS: 0, 43 | }; 44 | } 45 | if (lookForStoredValue) { 46 | const propertyValue = JSON.parse( 47 | window.localStorage.getItem(localStorageProperty) 48 | ); 49 | if (propertyValue !== null) defaultValue = propertyValue; 50 | } 51 | 52 | return defaultValue; 53 | } 54 | 55 | export function getTranslateXY(element) { 56 | const style = window.getComputedStyle(element); 57 | const matrix = new DOMMatrixReadOnly(style.transform); 58 | return { 59 | translateX: matrix.m41, 60 | translateY: matrix.m42, 61 | }; 62 | } 63 | 64 | export function deleteDataItem(data, setData, dataItem) { 65 | const tempData = [...data]; 66 | if (data.indexOf(dataItem) === -1) { 67 | console.error("CAN'T FIND WINDOW TO DELETE"); 68 | return; 69 | } 70 | tempData.splice(tempData.indexOf(dataItem), 1); 71 | setData(tempData); 72 | } 73 | 74 | // Goes through the windowData and sets the matching id item in windowData to windowObj 75 | export function replaceDesiredWindowItem(windowData, windowObj) { 76 | for (let i = 0; i < windowData.length; i++) { 77 | if (windowData[i]["id"] === windowObj["id"]) { 78 | windowData[i] = windowObj; 79 | } 80 | } 81 | } 82 | 83 | // Used to create a new window or icon 84 | export function addDataItem(data, setData, useCase, setFocusedWindow) { 85 | const tempData = [...data]; 86 | const newId = getMaxId(data) + 1; 87 | if (setFocusedWindow) setFocusedWindow(newId); 88 | let newItem = {}; 89 | if (useCase === "window") { 90 | newItem = getDefaultValue("windowData", false, newId)[0]; 91 | } else if (useCase === "icon") { 92 | newItem = getDefaultValue("iconData", false, newId)[0]; 93 | } 94 | 95 | tempData.push(newItem); 96 | setData(tempData); 97 | } 98 | 99 | // Gets window based off of id 100 | export function getDesiredItem(windowData, id) { 101 | for (let i = 0; i < windowData.length; i++) { 102 | if (windowData[i]["id"] === id) { 103 | return windowData[i]; 104 | } 105 | } 106 | console.error("DESIRED WINDOW ITEM NOT FOUND"); 107 | return null; 108 | } 109 | 110 | // Use this for centering text elements 111 | export function convertJustifyContentToTextAlign(valueToConvert) { 112 | if (valueToConvert === "flex-start") { 113 | return "left"; 114 | } else if (valueToConvert === "center") { 115 | return "center"; 116 | } else if (valueToConvert === "flex-end") { 117 | return "right"; 118 | } 119 | } 120 | 121 | export function convertTextAlignToJustifyContent(valueToConvert) { 122 | if (valueToConvert === "left") { 123 | return "flex-start"; 124 | } else if (valueToConvert === "center") { 125 | return "center"; 126 | } else if (valueToConvert === "right") { 127 | return "flex-end"; 128 | } 129 | } 130 | 131 | export function getSelectionText() { 132 | if (window.getSelection) { 133 | try { 134 | let activeElement = document.activeElement; 135 | if (activeElement && activeElement.value) { 136 | // firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=85686 137 | return activeElement.value.substring( 138 | activeElement.selectionStart, 139 | activeElement.selectionEnd 140 | ); 141 | } else { 142 | return window.getSelection().toString(); 143 | } 144 | } catch (e) {} 145 | } else if (document.selection && document.selection.type !== "Control") { 146 | // For IE 147 | return document.selection.createRange().text; 148 | } 149 | } 150 | 151 | export function getTimeUnits() { 152 | let d = new Date(); 153 | let hour = d.getHours(); 154 | let minutes = d.getMinutes(); 155 | if (minutes < 10) { 156 | minutes = "0" + minutes; 157 | } 158 | let seconds = d.getSeconds(); 159 | if (seconds < 10) { 160 | seconds = "0" + seconds; 161 | } 162 | return { hour: hour, minutes: minutes, seconds: seconds }; 163 | } 164 | 165 | export function getTimePeriodName(hourNumber) { 166 | if (hourNumber > 11 && hourNumber < 24) { 167 | return "PM"; 168 | } else if (hourNumber === 24 || hourNumber < 12) { 169 | return "AM"; 170 | } 171 | } 172 | 173 | export function getTwelveHourTime(hourNumber) { 174 | if (hourNumber > 12) { 175 | return hourNumber - 12; 176 | } else if (hourNumber === 0) { 177 | return 12; 178 | } else { 179 | return hourNumber; 180 | } 181 | } 182 | 183 | export function getMaxId(windowData) { 184 | let maxId = 0; 185 | for (let i = 0; i < windowData.length; i++) { 186 | if (windowData[i]["id"] > maxId) { 187 | maxId = windowData[i]["id"]; 188 | } 189 | } 190 | return maxId; 191 | } 192 | 193 | export function updateSetting( 194 | settingsData, 195 | setSettingsData, 196 | propertyName, 197 | propertyValue 198 | ) { 199 | const tempSettingsData = { ...settingsData }; 200 | if (Array.isArray(propertyName) && Array.isArray(propertyValue)) { 201 | for (let i = 0; i < propertyValue.length; i++) { 202 | tempSettingsData[propertyName[i]] = propertyValue[i]; 203 | } 204 | } else { 205 | tempSettingsData[propertyName] = propertyValue; 206 | } 207 | setSettingsData(tempSettingsData); 208 | } 209 | 210 | export function toggleEditOnKeyPress(e, toggleEditMode) { 211 | if (e.metaKey && e.key === "e") { 212 | toggleEditMode(); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/components/Startbar/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef, Fragment } from "react"; 2 | import { 3 | Bar, 4 | TabContainer, 5 | WindowsXPStartButton, 6 | Windows98StartButton, 7 | Windows98Logo, 8 | WindowsXPTab, 9 | Windows98Tab, 10 | Windows7Tab, 11 | Windows98Bar, 12 | WindowsXPTimeSegment, 13 | Windows98TimeSegment, 14 | WindowsXPSegment, 15 | Windows98Segment, 16 | Windows7Segment, 17 | Windows7TimeSegment, 18 | Windows7StartButton, 19 | Windows7Divider, 20 | } from "./styles"; 21 | import windows98Logo from "../../media/windows98-logo.png"; 22 | import windows7Logo from "../../media/windows7-logo.png"; 23 | import normalImg from "../../media/start-button.png"; 24 | import pressedImg from "../../media/start-button-pressed.png"; 25 | import blueBarImg from "../../media/blue-bar-img.png"; 26 | import grayBarImg from "../../media/gray-bar-img.png"; 27 | import timeBarImg from "../../media/time-bar-img.png"; 28 | import tabBackgroundImg from "../../media/tab-background.png"; 29 | import { 30 | getTimePeriodName, 31 | getTimeUnits, 32 | getTwelveHourTime, 33 | } from "../../functions/helpers"; 34 | import { setDataProperty } from "../Window/helper"; 35 | import { useStore } from "../../Store"; 36 | import StartWindow from "./StartWindow"; 37 | 38 | function Startbar() { 39 | const [time, setTime] = useState(""); 40 | const [day, setDay] = useState(""); 41 | const startButton = useRef(null); 42 | const startWindow = useRef(null); 43 | const [isStartWindowShowing, setIsStartWindowShowing] = useState(false); 44 | const setFocusedWindow = useStore((state) => state.setFocusedWindow); 45 | const settingsData = useStore((state) => state.settingsData); 46 | 47 | const windowData = useStore((state) => state.windowData); 48 | const setWindowData = useStore((state) => state.setWindowData); 49 | 50 | const tabs = windowData.map((item, index) => { 51 | const windowItem = item; 52 | return ( 53 | 54 | {settingsData["windowsOS"] === 0 && ( 55 | { 59 | setFocusedWindow(item["id"]); 60 | setDataProperty( 61 | windowData, 62 | setWindowData, 63 | windowItem, 64 | "hidden", 65 | !item["hidden"] 66 | ); 67 | }} 68 | key={`tab-${index}`} 69 | data-cy={`tab-${index}`} 70 | > 71 | {item["windowTitle"]} 72 | 73 | )} 74 | {settingsData["windowsOS"] === 1 && ( 75 | { 79 | setFocusedWindow(item["id"]); 80 | setDataProperty( 81 | windowData, 82 | setWindowData, 83 | windowItem, 84 | "hidden", 85 | !item["hidden"] 86 | ); 87 | }} 88 | active={true} 89 | key={`tab-${index}`} 90 | > 91 | {item["windowTitle"]} 92 | 93 | )} 94 | 95 | {settingsData["windowsOS"] === 2 && ( 96 | { 100 | setFocusedWindow(item["id"]); 101 | setDataProperty( 102 | windowData, 103 | setWindowData, 104 | windowItem, 105 | "hidden", 106 | !item["hidden"] 107 | ); 108 | }} 109 | active={true} 110 | key={`tab-${index}`} 111 | > 112 | {item["windowTitle"]} 113 | 114 | )} 115 | 116 | ); 117 | }); 118 | 119 | function handleBlur(e) { 120 | if (startWindow.current) { 121 | if ( 122 | e.target !== startButton.current && 123 | !startWindow.current.contains(e.target) 124 | ) { 125 | setIsStartWindowShowing(false); 126 | } 127 | } 128 | } 129 | 130 | useEffect(() => { 131 | document.addEventListener("click", handleBlur); 132 | return () => { 133 | document.removeEventListener("click", handleBlur); 134 | }; 135 | }, []); 136 | 137 | useEffect(() => { 138 | let timeUnits = getTimeUnits(); 139 | let timeText = 140 | getTwelveHourTime(timeUnits["hour"]) + 141 | ":" + 142 | timeUnits["minutes"] + 143 | " " + 144 | getTimePeriodName(timeUnits["hour"]); 145 | setTime(timeText); 146 | 147 | const interval = setInterval(() => { 148 | timeUnits = getTimeUnits(); 149 | timeText = 150 | getTwelveHourTime(timeUnits["hour"]) + 151 | ":" + 152 | timeUnits["minutes"] + 153 | getTimePeriodName(timeUnits["hour"]); 154 | setTime(timeText); 155 | }, 1000); 156 | let currentDate = new Date(); 157 | let cDay = currentDate.getDate(); 158 | let cMonth = currentDate.getMonth() + 1; 159 | let cYear = currentDate.getFullYear(); 160 | setDay(cMonth + "/" + cDay + "/" + cYear); 161 | 162 | return () => clearInterval(interval); 163 | }, [setTime, setDay]); 164 | 165 | return ( 166 | 167 | {settingsData["windowsOS"] === 0 && ( 168 | { 175 | setIsStartWindowShowing(!isStartWindowShowing); 176 | }} 177 | /> 178 | )} 179 | {settingsData["windowsOS"] === 1 && ( 180 | 181 | { 184 | setIsStartWindowShowing(!isStartWindowShowing); 185 | }} 186 | > 187 | 188 | Start 189 | 190 | 191 | 192 | )} 193 | 194 | {settingsData["windowsOS"] === 2 && ( 195 | { 199 | setIsStartWindowShowing(!isStartWindowShowing); 200 | }} 201 | > 202 | )} 203 | 204 | {isStartWindowShowing && ( 205 | 210 | )} 211 | {settingsData["windowsOS"] === 0 && ( 212 | 213 | )} 214 | {settingsData["windowsOS"] === 1 && ( 215 | 216 | )} 217 | {settingsData["windowsOS"] === 2 && ( 218 | 219 | )} 220 | {tabs} 221 | 222 | {settingsData["windowsOS"] === 0 && ( 223 | 224 | {time} 225 | 226 | )} 227 | {settingsData["windowsOS"] === 1 && ( 228 | 229 | 230 | {time} 231 | 232 | )} 233 | {settingsData["windowsOS"] === 2 && ( 234 | 235 | 236 |
{time}
237 |
{day}
238 |
239 | 240 |
241 | )} 242 |
243 | ); 244 | } 245 | 246 | export default Startbar; 247 | -------------------------------------------------------------------------------- /src/components/KanbanBoard/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import styled from "styled-components"; 3 | import { 4 | closestCorners, 5 | DndContext, 6 | KeyboardSensor, 7 | PointerSensor, 8 | useSensor, 9 | useSensors, 10 | } from "@dnd-kit/core"; 11 | import { sortableKeyboardCoordinates, arrayMove } from "@dnd-kit/sortable"; 12 | import Column from "./Column"; 13 | import { snapCenterToCursor } from "@dnd-kit/modifiers"; 14 | import KanbanHeader from "./KanbanHeader"; 15 | import { useStore } from "../../Store"; 16 | import { changeItemProperty, handleDelete } from "../Window/helper"; 17 | import KanbanItemDragOverlay from "./KanbanItemOverlay"; 18 | 19 | function KanbanBoard({ componentObj, windowItem }) { 20 | const windowData = useStore((state) => state.windowData); 21 | const setWindowData = useStore((state) => state.setWindowData); 22 | const isEditModeOn = useStore((store) => store.isEditModeOn); 23 | const [items, setItems] = useState(componentObj["items"]); 24 | const [columnHeaders, setColumnHeaders] = useState( 25 | componentObj["columnHeaders"] 26 | ); 27 | 28 | useEffect(() => { 29 | changeItemProperty( 30 | windowItem, 31 | windowData, 32 | setWindowData, 33 | componentObj, 34 | "items", 35 | items 36 | ); 37 | }, [items]); 38 | 39 | useEffect(() => { 40 | changeItemProperty( 41 | windowItem, 42 | windowData, 43 | setWindowData, 44 | componentObj, 45 | "columnHeaders", 46 | columnHeaders 47 | ); 48 | }, [columnHeaders]); 49 | 50 | const columnContainer = useRef(null); 51 | const [activeId, setActiveId] = useState(null); 52 | const [dragOverlayWidth, setDragOverlayWidth] = useState(0); 53 | const sensors = useSensors( 54 | useSensor(PointerSensor), 55 | useSensor(KeyboardSensor, { 56 | coordinateGetter: sortableKeyboardCoordinates, 57 | }) 58 | ); 59 | 60 | const columnHeaderContent = Object.keys(columnHeaders).map( 61 | (columnKey, index) => { 62 | return ( 63 | 70 | ); 71 | } 72 | ); 73 | 74 | function getColumnMargin(index) { 75 | if (index === 0) return "0 0.5rem 0 0"; 76 | if (index === Object.keys(items).length - 1) return "0 0 0 0.5rem"; 77 | return "0 0.5rem"; 78 | } 79 | 80 | const columnContent = Object.keys(items).map((itemKey, index) => { 81 | return ( 82 | 89 | ); 90 | }); 91 | 92 | return ( 93 | 94 | 102 | 103 | {columnHeaderContent} 104 | {columnContent} 105 | 110 | 111 | {isEditModeOn && ( 112 | 113 | 126 | 127 | )} 128 | 129 | 130 | ); 131 | 132 | function findContainer(id) { 133 | if (id in items) { 134 | return id; 135 | } 136 | 137 | const keys = Object.keys(items); 138 | for (let i = 0; i < keys.length; i++) { 139 | const columnItems = items[keys[i]]; 140 | for (let j = 0; j < columnItems.length; j++) { 141 | if (columnItems[j].id === id) { 142 | return keys[i]; 143 | } 144 | } 145 | } 146 | } 147 | 148 | function handleDragStart(event) { 149 | const { active } = event; 150 | const { id } = active; 151 | setActiveId(id); 152 | const widthOfEachColumn = 153 | columnContainer.current.clientWidth / Object.keys(items).length; 154 | const widthOfColumnPadding = 16 * Object.keys(items).length; 155 | setDragOverlayWidth(widthOfEachColumn - widthOfColumnPadding); 156 | // setDragOverlayWidth(active.rect.current.translated.width); 157 | } 158 | 159 | function handleDragOver(event) { 160 | const { active, over } = event; 161 | const { id } = active; 162 | const { id: overId } = over; 163 | // Find the containers 164 | const activeContainer = findContainer(id); 165 | const overContainer = findContainer(overId); 166 | if ( 167 | !activeContainer || 168 | !overContainer || 169 | activeContainer === overContainer 170 | ) { 171 | return; 172 | } 173 | 174 | setItems((prev) => { 175 | const activeItems = prev[activeContainer]; 176 | const overItems = prev[overContainer]; 177 | 178 | // Find the indexes for the items 179 | const activeIndex = activeItems.findIndex((obj) => obj.id === id); 180 | const overIndex = overItems.findIndex((obj) => obj.id === overId); 181 | let newIndex; 182 | if (overId in prev) { 183 | // We're at the root droppable of a container 184 | newIndex = overItems.length + 1; 185 | } else { 186 | const isBelowLastItem = 187 | over && 188 | overIndex === overItems.length - 1 && 189 | active.rect.current.translated.top + 190 | active.rect.current.translated.height > 191 | over.rect.top + over.rect.height; 192 | 193 | const modifier = isBelowLastItem ? 1 : 0; 194 | 195 | newIndex = overIndex >= 0 ? overIndex + modifier : overItems.length + 1; 196 | } 197 | 198 | return { 199 | ...prev, 200 | [activeContainer]: [ 201 | ...prev[activeContainer].filter((item) => item.id !== active.id), 202 | ], 203 | [overContainer]: [ 204 | ...prev[overContainer].slice(0, newIndex), 205 | items[activeContainer][activeIndex], 206 | ...prev[overContainer].slice(newIndex, prev[overContainer].length), 207 | ], 208 | }; 209 | }); 210 | } 211 | 212 | function handleDragEnd(event) { 213 | const { active, over } = event; 214 | const { id } = active; 215 | const { id: overId } = over; 216 | 217 | const activeContainer = findContainer(id); 218 | const overContainer = findContainer(overId); 219 | 220 | if ( 221 | !activeContainer || 222 | !overContainer || 223 | activeContainer !== overContainer 224 | ) { 225 | return; 226 | } 227 | 228 | const activeIndex = items[activeContainer].findIndex( 229 | (obj) => obj.id === active.id 230 | ); 231 | const overIndex = items[overContainer].findIndex( 232 | (obj) => obj.id === overId 233 | ); 234 | 235 | if (activeIndex !== overIndex) { 236 | setItems((items) => ({ 237 | ...items, 238 | [overContainer]: arrayMove( 239 | items[overContainer], 240 | activeIndex, 241 | overIndex 242 | ), 243 | })); 244 | } 245 | 246 | setActiveId(null); 247 | } 248 | } 249 | 250 | const ColumnContainer = styled.div` 251 | display: grid; 252 | grid-template-columns: 1fr 1fr 1fr; 253 | margin-bottom: 0.5rem; 254 | `; 255 | 256 | const DeleteButtonContainer = styled.div` 257 | display: flex; 258 | justify-content: center; 259 | `; 260 | 261 | const Wrapper = styled.div` 262 | margin: 0.5rem 0; 263 | `; 264 | 265 | export default KanbanBoard; 266 | -------------------------------------------------------------------------------- /src/components/Image/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react"; 2 | import styled from "styled-components"; 3 | import { FlexContainer } from "../../styles/Layout"; 4 | import { TextAlignOptions } from "../ComponentOptions"; 5 | import { changeItemProperty, handleDelete } from "../Window/helper"; 6 | import BackButton from "../BackButton/index"; 7 | import { DeleteButton, OptionsButton } from "../../styles/StyledComponents"; 8 | import { useStore } from "../../Store"; 9 | 10 | function Image({ windowObj, windowItem }) { 11 | const [isImageFocused, setIsImageFocused] = useState(false); 12 | const [isRedirectClicked, setIsRedirectClicked] = useState(false); 13 | const srcInput = useRef(null); 14 | const redirectInput = useRef(null); 15 | const redirectButton = useRef(null); 16 | 17 | const windowData = useStore((state) => state.windowData); 18 | const setWindowData = useStore((state) => state.setWindowData); 19 | const isEditModeOn = useStore((store) => store.isEditModeOn); 20 | 21 | function handleOptions() { 22 | function setImageUrl() { 23 | changeItemProperty( 24 | windowObj, 25 | windowData, 26 | setWindowData, 27 | windowItem, 28 | "src", 29 | srcInput.current.value.trim() 30 | ); 31 | setIsImageFocused(false); 32 | } 33 | 34 | function setRedirectUrl() { 35 | changeItemProperty( 36 | windowObj, 37 | windowData, 38 | setWindowData, 39 | windowItem, 40 | "href", 41 | redirectInput.current.value 42 | ); 43 | setIsRedirectClicked(false); 44 | } 45 | 46 | function convertToImgWidth(e) { 47 | const sliderVal = e.target.value; 48 | const ratio = sliderVal / 20; 49 | const ratioPercentage = ratio * 100 + "%"; 50 | changeItemProperty( 51 | windowObj, 52 | windowData, 53 | setWindowData, 54 | windowItem, 55 | "imageWidth", 56 | ratioPercentage 57 | ); 58 | } 59 | 60 | if (!isImageFocused && !isRedirectClicked) { 61 | return ( 62 | 63 | 64 | 65 | 66 | { 74 | convertToImgWidth(e); 75 | }} 76 | /> 77 | 78 | { 81 | setIsRedirectClicked(true); 82 | }} 83 | width="105px" 84 | > 85 | Set Image Url 86 | 87 | 88 | ); 89 | } else if (isImageFocused) { 90 | return ( 91 | 92 | 99 | 104 | Set Redirect Url 105 | 106 | 107 | ); 108 | } else if (isRedirectClicked) { 109 | return ( 110 | 111 | { 116 | setIsRedirectClicked(false); 117 | }} 118 | /> 119 | 127 | 132 | Set Image Url 133 | 134 | 135 | ); 136 | } 137 | } 138 | 139 | function handleBlur() { 140 | // Timeout is needed to allow for enough time to check if the user clicked on the src input after the blur. 141 | // If they did, do not count that as removing image focus 142 | let redirectButtonClicked = false; 143 | if (redirectButton && redirectButton.current) { 144 | redirectButton.current.addEventListener("click", function () { 145 | redirectButtonClicked = true; 146 | }); 147 | } 148 | 149 | setTimeout(function () { 150 | if (redirectInput && redirectInput.current !== document.activeElement) { 151 | setIsImageFocused(false); 152 | } 153 | if (!redirectButtonClicked) { 154 | setIsRedirectClicked(false); 155 | } 156 | }, 200); 157 | if (redirectButton && redirectButton.current) { 158 | redirectButton.current.removeEventListener("click", function () { 159 | redirectButtonClicked = true; 160 | }); 161 | } 162 | } 163 | 164 | return ( 165 | 170 | {isEditModeOn && ( 171 | 172 | {handleOptions()} 173 | 174 | )} 175 | 176 | 181 | { 190 | setIsRedirectClicked(false); 191 | setIsImageFocused(true); 192 | }} 193 | onBlur={() => { 194 | handleBlur(); 195 | }} 196 | tabIndex={0} 197 | imageWidth={windowItem["imageWidth"]} 198 | > 199 | 203 | 204 | 205 | {isEditModeOn && ( 206 | { 210 | handleDelete( 211 | windowData, 212 | setWindowData, 213 | windowObj, 214 | windowItem["id"] 215 | ); 216 | }} 217 | > 218 | Delete 219 | 220 | )} 221 | 222 | ); 223 | } 224 | 225 | const ImageComponent = styled.img` 226 | width: 100%; 227 | height: 100%; 228 | 229 | :focus { 230 | border: 1px solid blue; 231 | } 232 | `; 233 | 234 | const ImageWrapper = styled.a` 235 | :focus { 236 | outline: ${(props) => 237 | props.href === "javascript:void(0)" ? "0" : "2px dotted gray"}; 238 | cursor: ${(props) => 239 | props.href === "javascript:void(0)" 240 | ? props.theme.cursors.auto 241 | : props.theme.cursors.wait}; 242 | } 243 | cursor: ${(props) => 244 | props.href === "javascript:void(0)" 245 | ? props.theme.cursors.auto 246 | : props.theme.cursors.pointer}; 247 | display: block; 248 | height: auto; 249 | width: ${(props) => props.imageWidth}; 250 | `; 251 | 252 | const Slider = styled.div` 253 | width: 9.5rem; 254 | margin: 0 0.5rem; 255 | `; 256 | 257 | export function convertToSliderWidth(valToConvert) { 258 | const percentageNum = parseInt(valToConvert); 259 | const ratioNum = percentageNum / 100; 260 | return ratioNum * 20; 261 | } 262 | 263 | export default Image; 264 | -------------------------------------------------------------------------------- /src/components/Window/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import styled, { css } from "styled-components"; 3 | import Draggable from "react-draggable"; 4 | import { handleComponentCreation, setDataProperty } from "./helper"; 5 | import { useStore } from "../../Store"; 6 | import WindowTitleBar from "../WindowTitleBar"; 7 | import { Resizable } from "re-resizable"; 8 | import RenderWindowComponents from "./RenderWindowComponents"; 9 | 10 | function Window({ width, windowItem, windowId }) { 11 | const windowRef = useRef(null); 12 | const componentsPanel = useRef(null); 13 | 14 | const windowData = useStore((state) => state.windowData); 15 | const setWindowData = useStore((state) => state.setWindowData); 16 | 17 | const settingsData = useStore((state) => state.settingsData); 18 | 19 | const focusedWindow = useStore((state) => state.focusedWindow); 20 | const setFocusedWindow = useStore((state) => state.setFocusedWindow); 21 | 22 | const isEditModeOn = useStore((state) => state.isEditModeOn); 23 | 24 | const componentData = [ 25 | "Text", 26 | "Image", 27 | "Kanban Board", 28 | "YouTube Video", 29 | "Search Bar", 30 | ]; 31 | const componentOptions = componentData.map((componentName, index) => { 32 | return ( 33 |
34 | 39 | 43 | {componentName} 44 | 45 |
46 | ); 47 | }); 48 | 49 | return ( 50 | { 67 | !windowItem["isMaximized"] && 68 | setDataProperty( 69 | windowData, 70 | setWindowData, 71 | windowItem, 72 | "position", 73 | windowRef 74 | ); 75 | }} 76 | > 77 | { 81 | setFocusedWindow(windowItem["id"]); 82 | }} 83 | notFocused={focusedWindow !== windowItem["id"]} 84 | ref={windowRef} 85 | width={width} 86 | className="window active" 87 | hidden={windowItem["hidden"]} 88 | isMaximized={windowItem["isMaximized"]} 89 | > 90 | 91 | { 113 | setDataProperty(windowData, setWindowData, windowItem, "size", { 114 | width: windowItem["size"]["width"] + d.width, 115 | height: windowItem["size"]["height"] + d.height, 116 | }); 117 | }} 118 | handleComponent={{ 119 | bottomRight: , 120 | }} 121 | > 122 | 123 | 127 | 131 | 132 | {isEditModeOn && ( 133 | 134 |
Select one component to add:
135 | {componentOptions} 136 | { 140 | handleComponentCreation( 141 | componentsPanel, 142 | windowData, 143 | setWindowData, 144 | windowItem 145 | ); 146 | }} 147 | > 148 | Add Component 149 | 150 |
151 | )} 152 |
153 |
154 |
155 |
156 |
157 | ); 158 | } 159 | 160 | function BottomRightHandle({ settingsData }) { 161 | return ; 162 | } 163 | 164 | const WindowContainer = styled.div` 165 | display: ${(props) => props.hidden && "none"}; 166 | // width: ${(props) => (props.width ? props.width : "35rem")}; 167 | height: auto; 168 | 169 | font-family: ${(props) => props.theme.fonts.primary}; 170 | position: absolute !important; 171 | box-sizing: border-box; 172 | box-shadow: ${(props) => 173 | props.notFocused && 174 | "inset -3px -3px #c7d3e7, inset 3px 3px #c7d3e7"} !important; 175 | z-index: ${(props) => (props.notFocused ? "2" : "3")} !important; 176 | :focus { 177 | outline: none; 178 | } 179 | 180 | ${(props) => 181 | props.isMaximized && 182 | css` 183 | width: 100vw; 184 | height: calc(100vh - 32px); 185 | z-index: 4 !important; 186 | `}; 187 | `; 188 | 189 | const ComponentsPanel = styled.fieldset` 190 | margin-top: 0.75rem; 191 | `; 192 | 193 | const AddComponent = styled(ComponentsPanel)``; 194 | const WindowBody = styled.div` 195 | height: calc(100% - 0.65rem); 196 | `; 197 | 198 | const WindowLabel = styled.label` 199 | cursor: ${(props) => props.theme.cursors.pointer}; 200 | `; 201 | 202 | const WindowPanel = styled.article` 203 | overflow-y: auto; 204 | box-sizing: border-box; 205 | height: 100%; 206 | `; 207 | 208 | const ResizableDots = styled.div` 209 | position: relative; 210 | ${(props) => 211 | props.windowsOS === 0 && 212 | css` 213 | width: 2px; 214 | height: 2px; 215 | 216 | right: 27%; 217 | bottom: -34%; 218 | box-shadow: rgba(0, 0, 0, 0.25) 2px 0, rgba(0, 0, 0, 0.25) 5.5px 0, 219 | rgba(0, 0, 0, 0.25) 9px 0, rgba(0, 0, 0, 0.25) 5.5px -3.5px, 220 | rgba(0, 0, 0, 0.25) 9px -3.5px, rgba(0, 0, 0, 0.25) 9px -7px, 221 | rgb(255, 255, 255) 3px 1px, rgb(255, 255, 255) 6.5px 1px, 222 | rgb(255, 255, 255) 10px 1px, rgb(255, 255, 255) 10px -2.5px, 223 | rgb(255, 255, 255) 10px -6px; 224 | `} 225 | ${(props) => 226 | props.windowsOS === 1 && 227 | css` 228 | width: 12.5px; 229 | height: 12.5px; 230 | background-image: linear-gradient( 231 | 135deg, 232 | rgb(254, 254, 254) 16.67%, 233 | rgb(198, 198, 198) 16.67%, 234 | rgb(198, 198, 198) 33.33%, 235 | rgb(132, 133, 132) 33.33%, 236 | rgb(132, 133, 132) 50%, 237 | rgb(254, 254, 254) 50%, 238 | rgb(254, 254, 254) 66.67%, 239 | rgb(198, 198, 198) 66.67%, 240 | rgb(198, 198, 198) 83.33%, 241 | rgb(132, 133, 132) 83.33%, 242 | rgb(132, 133, 132) 100% 243 | ); 244 | background-size: 5px 5px; 245 | clip-path: polygon(100% 0px, 0px 100%, 100% 100%); 246 | bottom: 10%; 247 | right: 12%; 248 | `} 249 | 250 | ${(props) => 251 | props.windowsOS === 2 && 252 | css` 253 | width: 2px; 254 | height: 2px; 255 | 256 | right: 27%; 257 | bottom: -34%; 258 | box-shadow: none; 259 | `} 260 | `; 261 | 262 | export default Window; 263 | -------------------------------------------------------------------------------- /src/components/SettingsWindow/Tabs.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Header1 } from "../../styles/Headers"; 3 | import { FlexContainer } from "../../styles/Layout"; 4 | import { HexColorPicker } from "react-colorful"; 5 | import styled from "styled-components"; 6 | import { updateSetting } from "../../functions/helpers"; 7 | import { useStore } from "../../Store"; 8 | 9 | export function AppearanceTab({ imageInput, colorInput }) { 10 | const settingsData = useStore((state) => state.settingsData); 11 | const setSettingsData = useStore((state) => state.setSettingsData); 12 | const [color, setColor] = useState(settingsData["backgroundColor"]); 13 | 14 | function handleColorInputEnter(e) { 15 | if (e.keyCode === 13) { 16 | updateSetting( 17 | settingsData, 18 | setSettingsData, 19 | "backgroundColor", 20 | e.target.value 21 | ); 22 | } 23 | } 24 | 25 | function handleImageInputEnter(e) { 26 | if (e.keyCode === 13) { 27 | updateSetting( 28 | settingsData, 29 | setSettingsData, 30 | "backgroundImage", 31 | e.target.value 32 | ); 33 | } 34 | } 35 | 36 | function handleKeyDown(e) { 37 | // Do not allow open and closed parenthesis 38 | if (e.which === 40 || e.which === 41) { 39 | e.preventDefault(); 40 | } 41 | } 42 | 43 | const defaultBackgroundImages = [ 44 | "https://etesam.nyc3.cdn.digitaloceanspaces.com/Windows-XP-Newtab/images/bliss.jpg", 45 | "https://etesam.nyc3.cdn.digitaloceanspaces.com/Windows-XP-Newtab/images/windows98-homepage.png", 46 | "https://etesam.nyc3.cdn.digitaloceanspaces.com/Windows-XP-Newtab/images/windows-7-homepage.jpg", 47 | ]; 48 | function handleOSChange(e) { 49 | const newOS = e.target.value; 50 | let windowsOS = null; 51 | let stylesheet = ""; 52 | 53 | if (newOS === "Windows XP") { 54 | windowsOS = 0; 55 | stylesheet = "https://unpkg.com/xp.css"; 56 | } 57 | if (newOS === "Windows 98") { 58 | windowsOS = 1; 59 | stylesheet = "https://unpkg.com/98.css"; 60 | } 61 | if (newOS === "Windows 7") { 62 | windowsOS = 2; 63 | stylesheet = "https://unpkg.com/7.css"; 64 | } 65 | 66 | const propertyNames = ["windowsOS", "stylesheet"]; 67 | const propertyValues = [windowsOS, stylesheet]; 68 | if (defaultBackgroundImages.includes(settingsData["backgroundImage"])) { 69 | propertyNames.push("backgroundImage"); 70 | propertyValues.push(defaultBackgroundImages[windowsOS]); 71 | imageInput.current.value = defaultBackgroundImages[windowsOS]; 72 | } 73 | updateSetting(settingsData, setSettingsData, propertyNames, propertyValues); 74 | } 75 | 76 | useEffect(() => { 77 | updateSetting(settingsData, setSettingsData, "backgroundColor", color); 78 | }, [color, setSettingsData]); 79 | 80 | return ( 81 |
82 | Change Background Image 83 | 88 | { 94 | handleImageInputEnter(e); 95 | }} 96 | /> 97 | 98 | 111 | { 114 | updateSetting( 115 | settingsData, 116 | setSettingsData, 117 | "backgroundImage", 118 | defaultBackgroundImages[settingsData["windowsOS"]] 119 | ); 120 | imageInput.current.value = 121 | defaultBackgroundImages[settingsData["windowsOS"]]; 122 | }} 123 | > 124 | Reset to Default 125 | 126 | { 129 | updateSetting( 130 | settingsData, 131 | setSettingsData, 132 | "backgroundImage", 133 | "" 134 | ); 135 | }} 136 | > 137 | Remove Image 138 | 139 | 140 | 141 | Change Background Color 142 | 147 | { 153 | handleKeyDown(e); 154 | }} 155 | onChange={(e) => { 156 | setColor(e.target.value + ""); 157 | }} 158 | onKeyDown={(e) => { 159 | handleColorInputEnter(e); 160 | }} 161 | /> 162 | 163 | 164 | Change Styles 165 | 166 | 171 | 172 |
173 | ); 174 | } 175 | 176 | const TabInput = styled.input` 177 | margin-right: 1rem; 178 | width: ${(props) => props.width}; 179 | @media only screen and (max-width: 768px) { 180 | margin-bottom: 1rem; 181 | width: 80%; 182 | } 183 | `; 184 | const MarginButton = styled.button` 185 | margin-top: 0.5rem; 186 | `; 187 | 188 | export function InfoTab() { 189 | return ( 190 |
191 | Windows XP New Tab 192 | 193 | This extension was created by Etesam Ansari using React.js, Styled 194 | Components, and the XP.css GitHub repo. 195 | 196 | 197 |

198 | GitHub Link:{" "} 199 |

200 | 201 | https://github.com/Etesam913/xp-newtab 202 | 203 | 204 |

205 | Firefox Addon Link:{" "} 206 |

207 | 208 | https://addons.mozilla.org/en-US/firefox/addon/xp-newtab/ 209 | 210 | 211 |

212 | Chrome Addon Link:{" "} 213 |

214 | 219 | https://chrome.google.com/webstore/detail/xp-newtab/ncfmlogaelpnniflgipmnnglhfiifkke 220 | 221 |
222 |
223 | ); 224 | } 225 | 226 | const InfoGrid = styled.div` 227 | margin-top: 0.45rem; 228 | display: inline-grid; 229 | grid-template-columns: auto auto; 230 | grid-auto-rows: auto auto auto; 231 | align-self: center; 232 | row-gap: 0.75rem; 233 | @media only screen and (max-width: 768px) { 234 | grid-template-columns: auto; 235 | } 236 | `; 237 | 238 | const InfoParagraph = styled.p` 239 | margin: 0.5rem 0; 240 | font-size: 1.1em; 241 | `; 242 | 243 | // Allows user to set settings such as grid, icon size 244 | export function MiscTab() { 245 | const settingsData = useStore((state) => state.settingsData); 246 | const setSettingsData = useStore((state) => state.setSettingsData); 247 | 248 | return ( 249 |
250 | Change Dragging Grid 251 | 268 |
269 | ); 270 | } 271 | -------------------------------------------------------------------------------- /src/components/Editor/Plugins/ToolbarPlugins/index.js: -------------------------------------------------------------------------------- 1 | import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; 2 | import { 3 | useCallback, 4 | useEffect, 5 | useMemo, 6 | useRef, 7 | useState, 8 | } from "react"; 9 | import { 10 | CAN_REDO_COMMAND, 11 | CAN_UNDO_COMMAND, 12 | REDO_COMMAND, 13 | UNDO_COMMAND, 14 | SELECTION_CHANGE_COMMAND, 15 | FORMAT_TEXT_COMMAND, 16 | FORMAT_ELEMENT_COMMAND, 17 | $getSelection, 18 | $isRangeSelection, 19 | $getNodeByKey, 20 | } from "lexical"; 21 | import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"; 22 | import { $isAtNodeEnd } from "@lexical/selection"; 23 | import { $getNearestNodeOfType, mergeRegister } from "@lexical/utils"; 24 | import { $isListNode, ListNode } from "@lexical/list"; 25 | import { createPortal } from "react-dom"; 26 | import { $isHeadingNode } from "@lexical/rich-text"; 27 | import { 28 | $isCodeNode, 29 | getDefaultCodeLanguage, 30 | getCodeLanguages, 31 | } from "@lexical/code"; 32 | import { BlockOptionsDropdownList } from "./BlockOptions"; 33 | import FloatingLinkEditor from "./FloatingLinkEditor"; 34 | import styled from "styled-components"; 35 | import { FlexContainer } from "../../../../styles/Layout"; 36 | 37 | const LowPriority = 1; 38 | 39 | const supportedBlockTypes = new Set([ 40 | "paragraph", 41 | "quote", 42 | "code", 43 | "h1", 44 | "h2", 45 | "h3", 46 | "h4", 47 | "h5", 48 | "h6", 49 | "ul", 50 | "ol", 51 | ]); 52 | 53 | function Select({ options, value, onChange }) { 54 | return ( 55 | 56 | 61 | ))} 62 | 63 | ); 64 | } 65 | 66 | export function getSelectedNode(selection) { 67 | const anchor = selection.anchor; 68 | const focus = selection.focus; 69 | const anchorNode = selection.anchor.getNode(); 70 | const focusNode = selection.focus.getNode(); 71 | if (anchorNode === focusNode) { 72 | return anchorNode; 73 | } 74 | const isBackward = selection.isBackward(); 75 | if (isBackward) { 76 | return $isAtNodeEnd(focus) ? anchorNode : focusNode; 77 | } else { 78 | return $isAtNodeEnd(anchor) ? focusNode : anchorNode; 79 | } 80 | } 81 | 82 | export default function ToolbarPlugins() { 83 | const [editor] = useLexicalComposerContext(); 84 | const toolbarRef = useRef(null); 85 | const [canUndo, setCanUndo] = useState(false); 86 | const [canRedo, setCanRedo] = useState(false); 87 | const [blockType, setBlockType] = useState("paragraph"); 88 | const [selectedElementKey, setSelectedElementKey] = useState(null); 89 | 90 | const [codeLanguage, setCodeLanguage] = useState(""); 91 | 92 | const [isLink, setIsLink] = useState(false); 93 | const [isBold, setIsBold] = useState(false); 94 | const [isItalic, setIsItalic] = useState(false); 95 | const [isUnderline, setIsUnderline] = useState(false); 96 | const [isStrikethrough, setIsStrikethrough] = useState(false); 97 | 98 | 99 | const updateToolbar = useCallback(() => { 100 | const selection = $getSelection(); 101 | if ($isRangeSelection(selection)) { 102 | const anchorNode = selection.anchor.getNode(); 103 | const element = 104 | anchorNode.getKey() === "root" 105 | ? anchorNode 106 | : anchorNode.getTopLevelElementOrThrow(); 107 | const elementKey = element.getKey(); 108 | const elementDOM = editor.getElementByKey(elementKey); 109 | if (elementDOM !== null) { 110 | setSelectedElementKey(elementKey); 111 | if ($isListNode(element)) { 112 | const parentList = $getNearestNodeOfType(anchorNode, ListNode); 113 | const type = parentList ? parentList.getTag() : element.getTag(); 114 | setBlockType(type); 115 | } else { 116 | const type = $isHeadingNode(element) 117 | ? element.getTag() 118 | : element.getType(); 119 | setBlockType(type); 120 | if ($isCodeNode(element)) { 121 | setCodeLanguage(element.getLanguage() || getDefaultCodeLanguage()); 122 | } 123 | } 124 | } 125 | // Update text format 126 | setIsBold(selection.hasFormat("bold")); 127 | setIsItalic(selection.hasFormat("italic")); 128 | setIsUnderline(selection.hasFormat("underline")); 129 | setIsStrikethrough(selection.hasFormat("strikethrough")); 130 | 131 | // Update links 132 | const node = getSelectedNode(selection); 133 | const parent = node.getParent(); 134 | if ($isLinkNode(parent) || $isLinkNode(node)) { 135 | setIsLink(true); 136 | } else { 137 | setIsLink(false); 138 | } 139 | } 140 | }, [editor]); 141 | 142 | useEffect(() => { 143 | return mergeRegister( 144 | editor.registerUpdateListener(({ editorState }) => { 145 | editorState.read(() => { 146 | updateToolbar(); 147 | }); 148 | }), 149 | editor.registerCommand( 150 | SELECTION_CHANGE_COMMAND, 151 | (_payload, newEditor) => { 152 | updateToolbar(); 153 | return false; 154 | }, 155 | LowPriority 156 | ), 157 | editor.registerCommand( 158 | CAN_UNDO_COMMAND, 159 | (payload) => { 160 | setCanUndo(payload); 161 | return false; 162 | }, 163 | LowPriority 164 | ), 165 | editor.registerCommand( 166 | CAN_REDO_COMMAND, 167 | (payload) => { 168 | setCanRedo(payload); 169 | return false; 170 | }, 171 | LowPriority 172 | ) 173 | ); 174 | }, [editor, updateToolbar]); 175 | 176 | const codeLanguages = useMemo(() => getCodeLanguages(), []); 177 | const onCodeLanguageSelect = useCallback( 178 | (e) => { 179 | editor.update(() => { 180 | if (selectedElementKey !== null) { 181 | const node = $getNodeByKey(selectedElementKey); 182 | if ($isCodeNode(node)) { 183 | node.setLanguage(e.target.value); 184 | } 185 | } 186 | }); 187 | }, 188 | [editor, selectedElementKey] 189 | ); 190 | 191 | const insertLink = useCallback(() => { 192 | if (!isLink) { 193 | editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://"); 194 | } else { 195 | editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); 196 | } 197 | }, [editor, isLink]); 198 | 199 | return ( 200 |
201 | 202 | 212 | 223 | 224 | 225 | {supportedBlockTypes.has(blockType) && ( 226 | 227 | )} 228 | {blockType === "code" ? ( 229 |