├── .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 |
4 |
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 |
5 |
6 |
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 |
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
57 | {options.map((option) => (
58 |
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 |
235 | ) : (
236 | <>
237 |
246 |
255 |
264 |
275 |
282 | {isLink &&
283 | createPortal(
284 | ,
285 | document.body
286 | )}
287 |
288 |
297 |
306 |
315 | >
316 | )}
317 |
318 |
319 | );
320 | }
321 | const LanguagesDropdown = styled.select`
322 | width: 6rem;
323 | padding: 0 0.25rem;
324 | margin-left: 0.35rem;
325 | `;
326 |
327 | const UnderlineIcon = styled.i`
328 | background-image: url("https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/icons/underline.svg");
329 | `;
330 |
331 | const BoldIcon = styled.i`
332 | background-image: url("https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/icons/bold.svg");
333 | `;
334 |
335 | const UndoIcon = styled.i`
336 | background-image: url("https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/icons/undo.svg");
337 | `;
338 |
339 | const RedoIcon = styled.i`
340 | background-image: url("https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/icons/redo.svg");
341 | `;
342 |
343 | const StrikethroughIcon = styled.i`
344 | background-image: url("https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/icons/strike-through.svg");
345 | `;
346 |
347 | const LinkIcon = styled.i`
348 | background-image: url("https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/icons/link.svg");
349 | `;
350 |
351 | const ItalicIcon = styled.i`
352 | background-image: url("https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/icons/italic.svg");
353 | `;
354 |
355 | const LeftAlignIcon = styled.i`
356 | background-image: url("https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/icons/left-align.svg");
357 | `;
358 |
359 | const RightAlignIcon = styled.i`
360 | background-image: url("https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/icons/right-align.svg");
361 | `;
362 |
363 | const CenterAlignIcon = styled.i`
364 | background-image: url("https://etesam.nyc3.digitaloceanspaces.com/Windows-XP-Newtab/icons/center-align.svg");
365 | `;
366 |
--------------------------------------------------------------------------------
/src/components/Startbar/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from "styled-components";
2 |
3 | export const Bar = styled.div`
4 | width: 100%;
5 | display: flex;
6 | z-index: 4;
7 | position: absolute;
8 | align-items: center;
9 | height: ${(props) => (props.windowsOS === 2 ? "47px" : "30px")};
10 | `;
11 |
12 | export const WindowsXPStartButton = styled.button`
13 | border: none !important;
14 | min-width: 99px !important;
15 | min-height: 30px !important;
16 | width: 99px;
17 | height: 30px;
18 | box-shadow: none !important;
19 | -webkit-backface-visibility: hidden;
20 | -moz-backface-visibility: hidden;
21 | border-radius: 0 !important;
22 | background-image: url(${(props) => props.normalImg});
23 |
24 | :focus {
25 | outline: none !important;
26 | }
27 |
28 | :active {
29 | background-image: url(${(props) => props.pressedImg}) !important;
30 | }
31 |
32 | ${(props) =>
33 | props.isPressed &&
34 | css`
35 | background-image: url(${(props) => props.pressedImg}) !important;
36 | `}
37 | `;
38 |
39 | export const Windows98StartButton = styled.button`
40 | font-weight: bold;
41 | height: 23px;
42 | margin: 0 4px 0 4px;
43 | min-width: 60px !important;
44 | display: flex;
45 | align-items: center;
46 | padding: 0 0 0 5px;
47 |
48 | &:active {
49 | padding: 0 0 0 5px !important;
50 | }
51 | `;
52 |
53 | export const Windows98Bar = styled.div`
54 | display: inline-block;
55 | box-sizing: border-box;
56 | height: 20px;
57 | width: 5px;
58 | border-color: rgb(254, 254, 254) rgb(132, 133, 132) rgb(132, 133, 132)
59 | rgb(254, 254, 254);
60 | border-style: solid;
61 | border-width: 2px;
62 | background: rgb(198, 198, 198) none repeat scroll 0 0;
63 | `;
64 |
65 | export const Windows7StartButton = styled.button`
66 | height: 100%;
67 | min-width: 72px !important;
68 | background-image: url(${(props) => props.backgroundImage});
69 | padding: 0;
70 | background-position-x: 7px;
71 | background-position-y: -7px;
72 | &:hover {
73 | background-color: none !important;
74 | background-position-x: 7px !important;
75 | background-position-y: -60px !important;
76 | background-image: url(${(props) => props.backgroundImage}) !important;
77 | }
78 |
79 | &:active {
80 | background-color: none !important;
81 | background-position-x: 7px !important;
82 | background-position-y: -114px !important;
83 | background-image: url(${(props) => props.backgroundImage}) !important;
84 | }
85 | `;
86 |
87 | export const Windows98Logo = styled.img`
88 | height: 18px;
89 | width: 18px;
90 | margin-right: 2px;
91 | pointer-events: none;
92 | `;
93 |
94 | export const WindowsXPSegment = styled.div`
95 | background-image: url(${(props) => props.blueBarImg});
96 | width: 100%;
97 | position: absolute;
98 | height: 100%;
99 | z-index: -1;
100 | `;
101 |
102 | export const Windows98Segment = styled.div`
103 | width: 100%;
104 | position: absolute;
105 | height: 100%;
106 | z-index: -1;
107 | border-style: solid;
108 | border-width: 2px;
109 | border-color: rgb(254, 254, 254) rgb(10, 10, 10) rgb(10, 10, 10)
110 | rgb(254, 254, 254);
111 | box-shadow: rgb(223, 223, 223) 1px 1px 0px 1px inset,
112 | rgb(132, 133, 132) -1px -1px 0px 1px inset;
113 | box-sizing: border-box;
114 | background: rgb(198, 198, 198) none repeat scroll 0 0;
115 | `;
116 |
117 | export const Windows7Segment = styled.div`
118 | background: linear-gradient(
119 | 90deg,
120 | hsla(0, 0%, 100%, 0.4),
121 | rgba(0, 0, 0, 0.1),
122 | hsla(0, 0%, 100%, 0.2)
123 | ),
124 | linear-gradient(
125 | 55deg,
126 | transparent,
127 | rgba(0, 0, 0, 0.1) 20%,
128 | rgba(0, 0, 0, 0.1) 50%,
129 | transparent 53%
130 | ),
131 | #4580c4cf;
132 | height: 100%;
133 | width: 100%;
134 | position: absolute;
135 | z-index: -1;
136 | box-shadow: inset 0 0.5px #fff;
137 | `;
138 |
139 | export const Windows7Divider = styled.div`
140 | width: 0.8rem;
141 | height: 47px;
142 | background: linear-gradient(
143 | 180deg,
144 | hsla(0, 0%, 100%, 0.65),
145 | rgba(0, 0, 0, 0.1),
146 | hsla(0, 0%, 100%, 0.2)
147 | ),
148 | linear-gradient(
149 | 0deg,
150 | transparent,
151 | rgba(0, 0, 0, 0.1) 20%,
152 | rgba(0, 0, 0, 0.1) 50%,
153 | transparent 53%
154 | ),
155 | #4580c4cf;
156 | `;
157 |
158 | export const WindowsXPTimeSegment = styled.div`
159 | width: 6rem;
160 | background-image: url(${(props) => props.timeBarImg});
161 | text-align: center;
162 | color: white;
163 | font-family: ${(props) => props.theme.fonts.primary};
164 | margin-left: auto;
165 | height: 30px;
166 | display: flex;
167 | align-items: center;
168 | justify-content: center;
169 | `;
170 |
171 | export const Windows98TimeSegment = styled.div`
172 | display: flex;
173 | align-items: center;
174 | justify-content: center;
175 | text-align: right;
176 | font-family: ${(props) => props.theme.fonts.primary};
177 | border-style: solid;
178 | border-width: 1px;
179 | border-color: rgb(128, 128, 128) rgb(255, 255, 255) rgb(255, 255, 255)
180 | rgb(128, 128, 128);
181 | height: 20px;
182 | box-sizing: border-box;
183 | margin: 0 8px 0 4px;
184 | padding: 0 10px;
185 | white-space: nowrap;
186 | `;
187 |
188 | export const Windows7TimeSegment = styled.div`
189 | display: flex;
190 | align-items: center;
191 | justify-content: center;
192 | text-align: right;
193 | font-family: "Helvetica";
194 | box-sizing: border-box;
195 | margin: 0 12px 0 4px;
196 | white-space: nowrap;
197 | height: 100%;
198 | color: white;
199 | display: flex;
200 | flex-direction: column;
201 | align-items: flex-end;
202 | font-size: 0.7rem;
203 | justify-content: space-evenly;
204 | `;
205 |
206 | export const TabContainer = styled.div`
207 | display: flex;
208 | align-items: center;
209 | padding: 0 ${(props) => (props.windowsOS === 2 ? "0" : "4px")};
210 | color: white;
211 | width: 100%;
212 | font-family: ${(props) => props.theme.fonts.primary};
213 | overflow-x: auto;
214 | height: 100%;
215 | `;
216 |
217 | export const WindowsXPTab = styled.button`
218 | width: 8rem;
219 | min-width: 1rem;
220 | height: 80%;
221 | border-radius: 4px;
222 | color: white;
223 | text-align: center;
224 | font-size: 11px;
225 | border: 1px solid #164ef7;
226 | background: #397df3 url(${(props) => props.tabBackgroundImg}) no-repeat 0
227 | ${(props) => (props.pressed ? "-75px" : "-7px")};
228 | white-space: nowrap;
229 | overflow: hidden;
230 | box-shadow: none !important;
231 | text-overflow: ellipsis;
232 | :focus {
233 | outline: none;
234 | }
235 | :hover {
236 | box-shadow: none !important;
237 | }
238 |
239 | :active {
240 | background: #397df3 url(${(props) => props.tabBackgroundImg}) no-repeat 0 -75px !important;
241 | }
242 | `;
243 |
244 | export const Windows98Tab = styled.button`
245 | width: 8rem;
246 | min-width: 1rem;
247 | text-align: left;
248 | white-space: nowrap;
249 | overflow: hidden;
250 | text-overflow: ellipsis;
251 | padding: 0 6px;
252 | margin: 0 2px;
253 |
254 | :active {
255 | padding: 0 6px !important;
256 | }
257 |
258 | ${(props) =>
259 | props.pressed &&
260 | css`
261 | box-sizing: border-box;
262 | background-repeat: repeat;
263 | background-attachment: scroll;
264 | background-origin: padding-box;
265 | background-clip: border-box;
266 | border-style: solid;
267 | border-width: 2px;
268 | border-color: rgb(45, 45, 45) rgb(254, 254, 254) rgb(254, 254, 254)
269 | rgb(77, 77, 77);
270 | box-shadow: rgb(132, 133, 132) 1px 1px 0px 1px inset,
271 | rgb(223, 223, 223) -1px -1px 0px 1px inset;
272 | background-image: linear-gradient(
273 | 45deg,
274 | rgb(198, 198, 198) 25%,
275 | transparent 25%,
276 | transparent 75%,
277 | rgb(198, 198, 198) 75%
278 | ),
279 | linear-gradient(
280 | 45deg,
281 | rgb(198, 198, 198) 25%,
282 | transparent 25%,
283 | transparent 75%,
284 | rgb(198, 198, 198) 75%
285 | );
286 | background-color: rgb(254, 254, 254);
287 | background-size: 4px 4px;
288 | background-position: 0px 0px, 2px 2px;
289 | `}
290 | `;
291 |
292 | export const Windows7Tab = styled.button`
293 | height: 100%;
294 | background: none;
295 | color: white;
296 | width: 8rem;
297 | min-width: 1rem;
298 | text-align: left;
299 | white-space: nowrap;
300 | overflow: hidden;
301 | text-overflow: ellipsis;
302 | text-align: center;
303 | border: 0;
304 | border-top: 1px solid #3c7fb1;
305 | border-radius: 0;
306 | &:hover {
307 | color: #343434;
308 | }
309 | ${(props) =>
310 | props.pressed &&
311 | css`
312 | background: linear-gradient(180deg, #e5f4fd 45%, #b3e0f9 0);
313 | color: #343434;
314 | border-color: #72a2c5;
315 | `};
316 | `;
317 |
318 | export const WindowsXPStartWindow = styled.div`
319 | position: absolute;
320 | height: 30rem;
321 | width: 14rem;
322 | background: white;
323 | bottom: 1.8rem;
324 | border-top-right-radius: 0.35rem;
325 | border-top-left-radius: 0.35rem;
326 | display: flex;
327 | flex-direction: column;
328 | overflow: hidden;
329 | z-index: 4;
330 | box-shadow: 10px 10px 30px -17px rgba(0, 0, 0, 0.4);
331 | `;
332 |
333 | export const WindowsXPStartHeader = styled.header`
334 | height: 4rem;
335 | background-image: url(${(props) => props.image});
336 | font-weight: bold;
337 | display: flex;
338 | align-items: center;
339 | color: white;
340 | font-size: 1.15rem;
341 | text-shadow: 0.5px 0.5px 2px #165ba3;
342 | text-align: center;
343 | padding: 0 0.5rem;
344 | `;
345 |
346 | export const WindowsXPStartBody = styled.div`
347 | height: 23.5rem;
348 | display: flex;
349 | flex-direction: column;
350 | background: white;
351 | border-radius: 0.15rem;
352 | `;
353 |
354 | export const StartItemName = styled.span`
355 | font-family: ${(props) => props.theme.fonts.primary};
356 | font-size: 0.9rem;
357 | margin: ${(props) => (props.margin ? props.margin : "0 0 0 0.5rem")};
358 | `;
359 |
360 | export const StartItemIcon = styled.img`
361 | width: ${(props) => props.width};
362 | height: ${(props) => props.height};
363 | `;
364 |
365 | export const WindowsXPStartFooter = styled.footer`
366 | height: 2.5rem;
367 | background-image: url(${(props) => props.image});
368 | font-weight: bold;
369 | display: flex;
370 | `;
371 |
372 | export const Windows7StartWindow = styled.div`
373 | position: absolute;
374 | height: 23rem;
375 | width: 14rem;
376 | background: white;
377 | bottom: 3rem;
378 | display: flex;
379 | flex-direction: column;
380 | overflow: hidden;
381 | z-index: 4;
382 | background: linear-gradient(
383 | 0deg,
384 | hsla(0, 0%, 100%, 0.4),
385 | rgba(0, 0, 0, 0.1),
386 | hsla(0, 0%, 100%, 0.2)
387 | ),
388 | linear-gradient(
389 | 55deg,
390 | transparent,
391 | rgba(0, 0, 0, 0.1) 20%,
392 | rgba(0, 0, 0, 0.1) 50%,
393 | transparent 53%
394 | ),
395 | #4580c4cf;
396 | padding: 0.8rem;
397 | border-radius: 0 0.8rem 0 0;
398 | `;
399 |
400 | export const Windows98StartWindow = styled.div`
401 | position: absolute;
402 | height: 23rem;
403 | width: 14rem;
404 | background: #bfc7c9;
405 | bottom: 1.8rem;
406 | border-top-right-radius: 0.35rem;
407 | border-top-left-radius: 0.35rem;
408 | display: flex;
409 | flex-direction: column;
410 | overflow: hidden;
411 | z-index: 4;
412 | box-shadow: 10px 10px 30px -17px rgba(0, 0, 0, 0.4);
413 | `;
414 |
415 | export const Windows98BlueStripe = styled.div`
416 | height: 100%;
417 | background: linear-gradient(
418 | 360deg,
419 | rgba(0, 0, 156, 1) 0%,
420 | rgba(33, 33, 223, 1) 14%,
421 | rgba(0, 0, 124, 1) 38%,
422 | rgba(0, 0, 124, 1) 100%
423 | );
424 | color: white;
425 | font-size: 1.5rem;
426 | writing-mode: tb-rl;
427 | transform: rotate(-180deg);
428 | `;
429 |
430 | export const Windows98StartBody = styled.div`
431 | display: flex;
432 | flex-direction: column;
433 | width: 100%;
434 | `;
435 | export const Windows98BoldText = styled.b`
436 | padding-top: 0.5rem;
437 | `;
438 |
439 | export const LoginButton = styled.button`
440 | border: 0;
441 | background: transparent;
442 | :hover {
443 | background: transparent;
444 | box-shadow: none !important;
445 | border: 0;
446 | }
447 | min-width: auto;
448 | :active {
449 | background: transparent !important;
450 | }
451 | :focus {
452 | background: transparent;
453 | outline: 0 !important;
454 | }
455 | display: flex;
456 | align-items: center;
457 | padding: 0 0.5rem;
458 | color: white;
459 | `;
460 |
461 | export const LoginImg = styled.img`
462 | margin: 0 0.4rem;
463 | `;
464 |
465 | export const WindowsXPItemContainer = styled.div`
466 | width: 100%;
467 | height: 48px;
468 | display: flex;
469 | flex-direction: column;
470 | justify-content: center;
471 | cursor: pointer;
472 | :hover {
473 | background: #2d6ac2;
474 | color: white;
475 | }
476 | `;
477 |
478 | export const Windows98ItemContainer = styled(WindowsXPItemContainer)`
479 | :hover {
480 | background: #010080;
481 | color: white;
482 | }
483 | `;
484 |
--------------------------------------------------------------------------------