├── .firebaserc ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── background.jpg ├── setupTests.js ├── App.test.js ├── styles │ ├── IPAddressWidget.css │ ├── WhiteBoardWidget.css │ ├── QuoteWidget.css │ ├── NewsWidget.css │ ├── VideoRecorderWidget.css │ ├── AlarmWidget.css │ ├── DictionaryWidget.css │ ├── BookWidget.css │ ├── WeatherWidget.css │ ├── PomodoroWidget.css │ ├── PasswordGenerator.css │ ├── GameWidget.css │ └── styles.css ├── widgets │ ├── CalendarWidget.js │ ├── components │ │ ├── Square.jsx │ │ ├── Board.jsx │ │ └── StatusMessage.jsx │ ├── winner.js │ ├── QuoteWidget.js │ ├── ClockWidget.js │ ├── IPAddressWidget.js │ ├── NewWidget.js │ ├── DictionaryWidget │ │ ├── Dictionary.js │ │ └── DictionaryContainer.jsx │ ├── WhiteBoardWidget.js │ ├── WeatherWidget.js │ ├── GameWidget.js │ ├── BookWidget.js │ ├── ReminderListWidget.js │ ├── AlarmWidget.js │ ├── TimerWidget.js │ ├── VideoRecorder.js │ ├── Pass.js │ └── PomodoroWidget.js ├── reportWebVitals.js ├── index.js ├── index.css ├── logo.svg ├── modals │ └── WidgetGalleryModal.js ├── App.css └── App.js ├── firebase.json ├── Dockerfile ├── .gitignore ├── LICENSE ├── package.json ├── .firebase └── hosting.YnVpbGQ.cache ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml └── README.md /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "mohitahlawat-planner-app" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohitahlawat2001/shiny-octo-planner-app/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohitahlawat2001/shiny-octo-planner-app/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohitahlawat2001/shiny-octo-planner-app/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohitahlawat2001/shiny-octo-planner-app/HEAD/src/background.jpg -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /app 4 | COPY package*.json ./ 5 | 6 | RUN npm install 7 | 8 | COPY . . 9 | 10 | RUN npm run build 11 | 12 | EXPOSE 3000 13 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/styles/IPAddressWidget.css: -------------------------------------------------------------------------------- 1 | .ip-container{ 2 | padding: 0.8rem; 3 | } 4 | 5 | .ip-head{ 6 | font-size: 1.2rem; 7 | font-weight: bold; 8 | color: rgba(0, 0, 255, 0.457); 9 | text-align: center; 10 | margin-bottom: 0.5rem; 11 | } 12 | 13 | .ip-subhead{ 14 | font-weight:bold; 15 | } -------------------------------------------------------------------------------- /src/widgets/CalendarWidget.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import Calendar from 'react-calendar'; 3 | import "../styles/styles.css" 4 | 5 | export default function CalendarWidget() { 6 | return ( 7 |
8 | 11 |
12 | ) 13 | } -------------------------------------------------------------------------------- /src/widgets/components/Square.jsx: -------------------------------------------------------------------------------- 1 | const Square = ({ value, onClick, isWinningSquare }) => { 2 | return ( 3 | 11 | ); 12 | }; 13 | 14 | export default Square; 15 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /.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 | 25 | # Local Netlify folder 26 | .netlify 27 | .firebase -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/widgets/winner.js: -------------------------------------------------------------------------------- 1 | export function calculateWinner(squares) { 2 | const lines = [ 3 | [0, 1, 2], 4 | [3, 4, 5], 5 | [6, 7, 8], 6 | [0, 3, 6], 7 | [1, 4, 7], 8 | [2, 5, 8], 9 | [0, 4, 8], 10 | [2, 4, 6], 11 | ]; 12 | for (let i = 0; i < lines.length; i++) { 13 | const [a, b, c] = lines[i]; 14 | if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { 15 | return { 16 | winner: squares[a], 17 | winningSquares: lines[i], 18 | }; 19 | } 20 | } 21 | return { 22 | winner: null, 23 | winningSquares: [], 24 | }; 25 | } -------------------------------------------------------------------------------- /src/styles/WhiteBoardWidget.css: -------------------------------------------------------------------------------- 1 | .whiteboard-container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | margin: 10px; 7 | padding: 10px; 8 | width: 400px; 9 | } 10 | 11 | .whiteboard { 12 | border: 1px solid #fff; 13 | background-color: transparent; 14 | cursor: crosshair; 15 | width: 300px; 16 | } 17 | 18 | .reset-button { 19 | margin-top: 10px; 20 | padding: 8px 16px; 21 | background-color: #fff; 22 | border: 1px solid #000; 23 | border-radius: 4px; 24 | font-size: 14px; 25 | cursor: pointer; 26 | } 27 | 28 | .reset-button:hover { 29 | background-color: #f2f2f2; 30 | } 31 | -------------------------------------------------------------------------------- /src/styles/QuoteWidget.css: -------------------------------------------------------------------------------- 1 | /* styles/QuoteWidget.css */ 2 | 3 | .quote-widget { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | width: 400px; 9 | height: 200px; 10 | padding: 20px; 11 | } 12 | 13 | .quote-container { 14 | margin-bottom: 20px; 15 | } 16 | 17 | .quote-content { 18 | font-size: 18px; 19 | font-weight: bold; 20 | } 21 | 22 | .quote-author { 23 | font-style: italic; 24 | } 25 | 26 | .quote-button { 27 | padding: 10px 20px; 28 | font-size: 16px; 29 | background-color: #4caf50; 30 | color: white; 31 | border: none; 32 | border-radius: 4px; 33 | cursor: pointer; 34 | } 35 | 36 | .quote-button:hover { 37 | background-color: #45a049; 38 | } 39 | -------------------------------------------------------------------------------- /src/styles/NewsWidget.css: -------------------------------------------------------------------------------- 1 | .news-widget { 2 | width: 400px; 3 | height: 400px; 4 | overflow-y: scroll; 5 | background-color: transparent; 6 | padding: 20px; 7 | } 8 | 9 | .news-widget h2 { 10 | font-size: 1.5rem; 11 | margin-bottom: 10px; 12 | } 13 | 14 | .news-widget ul { 15 | list-style: none; 16 | padding: 0; 17 | margin: 0; 18 | } 19 | 20 | .news-widget li { 21 | margin-bottom: 10px; 22 | } 23 | 24 | .news-widget a { 25 | text-decoration: none; 26 | color: #333; 27 | } 28 | 29 | .news-widget a:hover { 30 | text-decoration: underline; 31 | } 32 | 33 | .img-container { 34 | width: 100%; 35 | height: 200px; 36 | overflow: hidden; 37 | margin-bottom: 20px; 38 | margin-top: 20px; 39 | padding: auto; 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/widgets/components/Board.jsx: -------------------------------------------------------------------------------- 1 | import Square from "./Square"; 2 | 3 | const Board = ({ squares, handleSquareClick, winningSquares }) => { 4 | const renderSquare = (position) => { 5 | const isWinningSquare = winningSquares.includes(position); 6 | return ( 7 | handleSquareClick(position)} 10 | isWinningSquare={isWinningSquare} 11 | /> 12 | ); 13 | }; 14 | 15 | return ( 16 |
17 |
18 | {renderSquare(0)} 19 | {renderSquare(1)} 20 | {renderSquare(2)} 21 |
22 |
23 | {renderSquare(3)} 24 | {renderSquare(4)} 25 | {renderSquare(5)} 26 |
27 |
28 | {renderSquare(6)} 29 | {renderSquare(7)} 30 | {renderSquare(8)} 31 |
32 |
33 | ); 34 | }; 35 | 36 | export default Board; 37 | -------------------------------------------------------------------------------- /src/widgets/QuoteWidget.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import "../styles/QuoteWidget.css"; 3 | 4 | const QuoteWidget = () => { 5 | const [quote, setQuote] = useState(null); 6 | 7 | useEffect(() => { 8 | fetchQuote(); 9 | }, []); 10 | 11 | const fetchQuote = async () => { 12 | try { 13 | const response = await fetch("https://dummyjson.com/quotes/random"); 14 | const data = await response.json(); 15 | setQuote(data); 16 | } catch (error) { 17 | console.log("Error fetching quote:", error); 18 | } 19 | }; 20 | 21 | return ( 22 |
23 | {quote && ( 24 |
25 |

{quote.quote}

26 |

— {quote.author}

27 |
28 | )} 29 | 32 |
33 | ); 34 | }; 35 | 36 | export default QuoteWidget; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mohit Ahlawat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/widgets/components/StatusMessage.jsx: -------------------------------------------------------------------------------- 1 | const StatusMessage = ({ winner, gamingBoard }) => { 2 | const { squares, isXNext } = gamingBoard; 3 | 4 | const noMovesLeft = squares.every((el) => el !== null); 5 | const nextPlayer = isXNext ? "X" : "O"; 6 | 7 | const renderStatusMessage = () => { 8 | if (winner) { 9 | return ( 10 |
11 | Winner is{" "} 12 | 13 | {winner} 14 | 15 |
16 | ); 17 | } else if (noMovesLeft) { 18 | return ( 19 |
20 | X and{" "} 21 | O tied 22 |
23 | ); 24 | } else { 25 | return ( 26 |
27 | Next player is{" "} 28 | 29 | {nextPlayer} 30 | 31 |
32 | ); 33 | } 34 | }; 35 | 36 | return

{renderStatusMessage()}

; 37 | }; 38 | 39 | export default StatusMessage; 40 | -------------------------------------------------------------------------------- /src/widgets/ClockWidget.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | 3 | 4 | export default function ClockWidget() { 5 | const [time, setTime] = useState(new Date().toLocaleTimeString()) 6 | const [date, setDate] = useState(new Date()) 7 | useEffect(() => { 8 | const interval = setInterval(() => { 9 | setTime(new Date().toLocaleTimeString()) 10 | setDate(new Date()) 11 | }, 1000) 12 | return () => clearInterval(interval) 13 | }, []) 14 | 15 | const tidyDate = (date) => { 16 | const dateArray = date 17 | const day = dateArray.getDay() 18 | const dateNumber = dateArray.getDate() 19 | const month = dateArray.getMonth() + 1 20 | const year = dateArray.getFullYear() 21 | 22 | const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] 23 | const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] 24 | 25 | return `${days[day]}, ${dateNumber} ${months[month - 1]} ${year}` 26 | 27 | } 28 | 29 | return ( 30 |
31 |

{time}

32 |

{tidyDate(date)}

33 |
34 | ) 35 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | *, 15 | *::before, 16 | *::after { 17 | box-sizing: border-box; 18 | } 19 | 20 | img, 21 | svg, 22 | video, 23 | canvas, 24 | audio, 25 | iframe, 26 | embed, 27 | object { 28 | display: block; 29 | max-width: 100%; 30 | } 31 | 32 | input, 33 | button, 34 | textarea, 35 | select { 36 | font: inherit; 37 | color: black; 38 | } 39 | 40 | * { 41 | margin: 0; 42 | padding: 0; 43 | box-sizing: border-box; 44 | font: inherit; 45 | color: white; 46 | } 47 | 48 | /* width */ 49 | ::-webkit-scrollbar { 50 | width: 10px; 51 | } 52 | 53 | /* Track */ 54 | ::-webkit-scrollbar-track { 55 | background: #363636; 56 | } 57 | 58 | /* Handle */ 59 | ::-webkit-scrollbar-thumb { 60 | background: #777; 61 | } 62 | 63 | /* Handle on hover */ 64 | ::-webkit-scrollbar-thumb:hover { 65 | background: #1e1e1e; 66 | } 67 | -------------------------------------------------------------------------------- /src/styles/VideoRecorderWidget.css: -------------------------------------------------------------------------------- 1 | #video-recorder-widget { 2 | max-width: 600px; 3 | margin: 0 auto; 4 | padding: 20px; 5 | border-radius: 8px; 6 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 7 | } 8 | 9 | #video-recorder-widget h2 { 10 | text-align: center; 11 | color: #333; 12 | } 13 | 14 | video { 15 | width: 100%; 16 | max-width: 100%; 17 | border-radius: 4px; 18 | } 19 | 20 | .controls { 21 | display: flex; 22 | justify-content: center; 23 | margin: 10px 0; 24 | } 25 | 26 | .record-button { 27 | padding: 10px 15px; 28 | border: none; 29 | border-radius: 4px; 30 | background-color: #007bff; 31 | color: white; 32 | cursor: pointer; 33 | transition: background-color 0.3s; 34 | } 35 | 36 | .record-button:hover { 37 | background-color: #0056b3; 38 | } 39 | 40 | #preview-section { 41 | margin-top: 20px; 42 | text-align: center; 43 | } 44 | 45 | #preview-video { 46 | width: 100%; 47 | max-width: 100%; 48 | border: 1px solid #ddd; 49 | border-radius: 4px; 50 | } 51 | 52 | #download-btn { 53 | margin-top: 10px; 54 | padding: 10px 15px; 55 | background-color: #28a745; 56 | } 57 | 58 | #download-btn:hover { 59 | background-color: #218838; 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "planner-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "axios": "^1.7.7", 10 | "boxicons": "^2.1.4", 11 | "react": "^18.2.0", 12 | "react-calendar": "^4.3.0", 13 | "react-circular-progressbar": "^2.1.0", 14 | "react-dom": "^18.2.0", 15 | "react-dotenv": "^0.1.3", 16 | "react-draggable": "^4.4.5", 17 | "react-icons": "^5.3.0", 18 | "react-scripts": "5.0.1", 19 | "react-spinners": "^0.14.1", 20 | "rsuite": "^5.35.1", 21 | "web-vitals": "^2.1.4" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/styles/AlarmWidget.css: -------------------------------------------------------------------------------- 1 | .alarm-clock{ 2 | display: flex; 3 | flex-direction: column; 4 | gap: 0.5rem; 5 | margin-top: 10px; 6 | align-items: center; 7 | justify-content: center; 8 | width: fit-content; 9 | } 10 | 11 | .alarm-input{ 12 | border-radius: 5px; 13 | padding: 0.5rem; 14 | border: 0.10rem blue solid; 15 | width: 100%; 16 | } 17 | 18 | .alarm-inp-container{ 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | gap: 0.5rem; 23 | margin-top: 0.4rem; 24 | margin-bottom: 0.4rem; 25 | } 26 | 27 | .alarm-button{ 28 | padding: 0.6rem; 29 | border: 1px blue solid; 30 | background-color: blue; 31 | color: white; 32 | border-radius: 10px; 33 | 34 | } 35 | 36 | :hover.alarm-button{ 37 | background-color: rgba(0, 0, 255, 0.598); 38 | } 39 | 40 | .delete-alarm{ 41 | padding: 0.6rem; 42 | border: 1px red solid; 43 | background-color: red; 44 | color: white; 45 | border-radius: 10px; 46 | } 47 | 48 | :hover.delete-alarm{ 49 | background-color: rgba(255, 0, 0, 0.781); 50 | } 51 | 52 | .set-alarm-list{ 53 | margin-top: 0.8rem; 54 | display: flex; 55 | gap: 0.5rem; 56 | align-items: center; 57 | justify-content: center; 58 | border: 0.2rem rgba(128, 128, 128, 0.436) dotted; 59 | padding: 0.4rem; 60 | } -------------------------------------------------------------------------------- /src/styles/DictionaryWidget.css: -------------------------------------------------------------------------------- 1 | .dic-container{ 2 | display: flex; 3 | margin-top: 1.25rem; 4 | flex-direction: column; 5 | gap: 1rem; 6 | justify-content: center; 7 | align-items: center; 8 | width: 100%; 9 | } 10 | 11 | #dic-input{ 12 | padding-top: 0.25rem; 13 | padding-bottom: 0.25rem; 14 | padding-left: 0.5rem; 15 | padding-right: 0.5rem; 16 | border-radius: 0.375rem; 17 | border-width: 2px; 18 | border-color: #2563EB; 19 | } 20 | 21 | .dic-btn{ 22 | padding: 0.75rem; 23 | border-radius: 0.375rem; 24 | color: #ffffff; 25 | background-color: #3B82F6; 26 | } 27 | 28 | .w-fit{ 29 | width: fit-content; 30 | } 31 | 32 | .dic-head{ 33 | display: flex; 34 | align-items: center; 35 | gap: 2rem; 36 | } 37 | 38 | .dic-subhead{ 39 | font-weight: 700; 40 | color: #6B7280; 41 | } 42 | 43 | .meaning-container{ 44 | padding: 0.75rem; 45 | margin-top: 0.5rem; 46 | margin-bottom: 0.5rem; 47 | border-radius: 0.375rem; 48 | background-color: rgba(247, 244, 244, 0.331); 49 | } 50 | 51 | .pronunciation-icon{ 52 | color: #93C5FD; 53 | } 54 | 55 | 56 | .pronunciation-icon:hover { 57 | cursor: pointer; 58 | } 59 | 60 | .dic-word{ 61 | font-size: 1.125rem; 62 | line-height: 1.75rem; 63 | font-weight: 700; 64 | color: blue 65 | } -------------------------------------------------------------------------------- /src/widgets/IPAddressWidget.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import axios from 'axios'; 3 | import '../styles/IPAddressWidget.css' 4 | 5 | const IPAddressWidget = () => { 6 | const [loading, setLoading] = useState(true); 7 | const [ipv4, setIpv4] = useState(""); 8 | const [ipv6, setIpv6] = useState(""); 9 | const [country, setCountry] = useState(""); 10 | 11 | const ipv4Url = 'https://api.ipify.org?format=json' 12 | const ipv6Url = 'https://api6.ipify.org?format=json' 13 | const countryUrl = 'https://speed.cloudflare.com/meta' 14 | 15 | const getData = () => { 16 | axios.get(ipv4Url).then((res) => setIpv4(res.data)) 17 | axios.get(ipv6Url).then((res) => setIpv6(res.data)) 18 | axios.get(countryUrl).then((res) => setCountry(res.data)) 19 | } 20 | 21 | useEffect(() => getData(), []) 22 | 23 | return ( 24 |
25 |

Your IP Address Details!

26 |

IPv4 Address: {ipv4 && ipv4["ip"]}

27 |

IPv6 Address: {ipv6 && ipv6["ip"]}

28 |

Location: {country && country["city"]}, {country && country["country"]}

29 |
30 | ) 31 | } 32 | 33 | export default IPAddressWidget -------------------------------------------------------------------------------- /src/widgets/NewWidget.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import '../styles/NewsWidget.css'; 3 | 4 | const NewsWidget = () => { 5 | const [news, setNews] = useState([]); 6 | 7 | useEffect(() => { 8 | const fetchNews = async () => { 9 | try { 10 | const url = 11 | 'https://newsdata.io/api/1/news?apikey=pub_25246012b74356fb025cdf50f3354a4f989ea&language=en'; 12 | 13 | const response = await fetch(url); 14 | const data = await response.json(); 15 | 16 | setNews(data.results); 17 | } catch (error) { 18 | console.error('Failed to fetch news data:', error); 19 | } 20 | }; 21 | 22 | fetchNews(); 23 | }, []); 24 | 25 | return ( 26 |
27 |

Top Headlines

28 | {news && news.length === 0 ? ( 29 |

Loading news...

30 | ) : ( 31 |
    32 | {news && 33 | news.map((item) => ( 34 |
  • 35 | 36 | {item.title} 37 | 38 |

    {item.description}

    39 |

    {item.pubDate}

    40 |

    {item.source}

    41 |
  • 42 | ))} 43 |
44 | )} 45 |
46 | ); 47 | }; 48 | 49 | export default NewsWidget; 50 | -------------------------------------------------------------------------------- /src/widgets/DictionaryWidget/Dictionary.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import axios from 'axios'; 3 | import DictionaryContainer from './DictionaryContainer'; 4 | import '../../styles/DictionaryWidget.css' 5 | 6 | // WARNING: Do not change the entry componenet name 7 | function Dictionary(props) { 8 | const [word, setWord] = useState(''); 9 | const [dicData, setDicData] = useState([]); 10 | const [loading, setLoading] = useState(true); 11 | 12 | const url = `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`; 13 | 14 | 15 | const handleClick = () => { 16 | axios.get(url).then((res) => { 17 | setDicData(res.data); 18 | setLoading(false); 19 | }); 20 | }; 21 | 22 | return ( 23 | <> 24 |
25 |

Dictionary

26 | 27 | 28 | setWord(e.target.value)} 33 | /> 34 | 35 | 38 | 39 | {dicData.length > 0 && } 40 |
41 | 42 | ); 43 | } 44 | 45 | export default Dictionary; 46 | -------------------------------------------------------------------------------- /.firebase/hosting.YnVpbGQ.cache: -------------------------------------------------------------------------------- 1 | favicon.ico,1687838732704,eae62e993eb980ec8a25058c39d5a51feab118bd2100c4deebb2a9c158ec11f9 2 | logo192.png,1687838738692,3ee59515172ee198f3be375979df15ac5345183e656720a381b8872b2a39dc8b 3 | logo512.png,1687838738833,ee7e2f3fdb8209c4b6fd7bef6ba50d1b9dba30a25bb5c3126df057e1cb6f5331 4 | manifest.json,1687838737131,aff3449bdc238776f5d6d967f19ec491b36aed5fb7f23ccff6500736fd58494a 5 | robots.txt,1687838739847,bfe106a3fb878dc83461c86818bf74fc1bdc7f28538ba613cd3e775516ce8b49 6 | asset-manifest.json,1687874017542,ed8503cb0777790ec85eabd407bdff832d10e80db950565d488cf84724ed7a3b 7 | index.html,1687874017542,b5be4dbe239cff9f213937eb54b1f8c86b3341e33c629ee5b1599199359f7ac5 8 | static/css/main.17ce7e41.css,1687874017747,dd1ea7846f63abb13f27f7eb98ef014a9f197e04dd6a6f20ab642941afe20e89 9 | static/js/787.11e20981.chunk.js,1687874017747,d68f7aa007181cbc6c4f82841c78c6211182ff80f584a72d42fd4d7a1b75b4c3 10 | static/js/787.11e20981.chunk.js.map,1687874017746,5901ebd23ddb1f0d20e8f33870a20df7e95e1828c9be9771990239ba2ddef220 11 | static/js/main.60c45a59.js.LICENSE.txt,1687874017747,9530dc777d839e6b17e9c6c7b8fddbabe83254d611fb7cea500419b31de797b6 12 | static/css/main.17ce7e41.css.map,1687874017746,a56f7d833d8cb24ab73767b3bec88b8884780e8bf7bba4fe39ec1a82dc0cf6e9 13 | static/js/main.60c45a59.js,1687874017744,1d8be4f84cd3e129ed07e4bbca7d590a0f246d3c53b5bb8c8cfa996b8281229c 14 | static/media/background.214310b5719e274f9e19.jpg,1687874017747,cfc6980e7e523bdbc10eaa8522b149abdaca600f66a0a4c70d2359e0cc09293f 15 | static/js/main.60c45a59.js.map,1687874017748,27522cee932a4cafea8d34a5bea62a1b9ee8e4fd9cdba079441437a37b91ba1f 16 | -------------------------------------------------------------------------------- /src/styles/BookWidget.css: -------------------------------------------------------------------------------- 1 | .book-widget { 2 | padding: 20px; 3 | max-width: 400px; 4 | margin: 0 auto; 5 | max-height: 250px; 6 | overflow: hidden; 7 | } 8 | 9 | .book-form { 10 | display: flex; 11 | gap: 10px; 12 | } 13 | 14 | .book-form input[type="text"] { 15 | flex-grow: 1; 16 | padding: 5px; 17 | min-width: 25ch; 18 | border: 1px solid black; 19 | border-radius: 3px; 20 | } 21 | 22 | .book-button { 23 | appearance: none; 24 | background-color: #4caf50; 25 | border: 1px solid rgba(27, 31, 35, .15); 26 | border-radius: 6px; 27 | box-shadow: rgba(27, 31, 35, .1) 0 1px 0; 28 | box-sizing: border-box; 29 | color: #fff; 30 | cursor: pointer; 31 | font-family: -apple-system,system-ui,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; 32 | font-size: 14px; 33 | font-weight: 600; 34 | line-height: 20px; 35 | padding: 6px 16px; 36 | position: relative; 37 | text-align: center; 38 | text-decoration: none; 39 | user-select: none; 40 | -webkit-user-select: none; 41 | touch-action: manipulation; 42 | vertical-align: middle; 43 | white-space: nowrap; 44 | } 45 | 46 | .book-button:hover { 47 | background-color: #2c974b; 48 | } 49 | 50 | .book-results-container { 51 | max-height: 250px; 52 | overflow-y: scroll; 53 | margin-top: 10px; 54 | border: 1px solid #ddd; 55 | padding: 10px; 56 | border-radius: 3px; 57 | } 58 | 59 | .book-results { 60 | margin-bottom: 5px; 61 | flex: 1 1 auto; 62 | 63 | } 64 | 65 | .book-results hr { 66 | margin: 5px 0; 67 | } 68 | 69 | .book-isbn-container { 70 | display: flex; 71 | justify-content: space-between; 72 | } 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/styles/WeatherWidget.css: -------------------------------------------------------------------------------- 1 | #weather-widget { 2 | max-width: 400px; 3 | margin: 0 auto; 4 | padding: 20px; 5 | background-color: transparent; 6 | border-radius: 5px; 7 | } 8 | 9 | #weather-widget h2 { 10 | margin-top: 0; 11 | } 12 | 13 | #weather-widget form { 14 | display: flex; 15 | gap: 10px; 16 | margin-bottom: 10px; 17 | } 18 | 19 | #weather-widget input[type="text"] { 20 | flex-grow: 1; 21 | padding: 5px; 22 | border: 1px solid black; 23 | border-radius: 3px; 24 | } 25 | 26 | #weather-widget button { 27 | padding: 5px 10px; 28 | border: 1px solid black; 29 | border-radius: 3px; 30 | background-color: #fff; 31 | color : #000; 32 | } 33 | 34 | #weather-widget #weather-content { 35 | margin-top: 10px; 36 | } 37 | 38 | #weather-widget #weather-location { 39 | font-size: 18px; 40 | font-weight: bold; 41 | } 42 | 43 | #weather-widget #weather-date { 44 | margin-top: 5px; 45 | font-size: 14px; 46 | } 47 | 48 | #weather-widget #weather-icon { 49 | margin-top: 10px; 50 | } 51 | 52 | #weather-widget #weather-icon img { 53 | width: 100px; 54 | height: 100px; 55 | float: left; 56 | } 57 | 58 | #weather-widget #weather-description { 59 | font-size: 20px; 60 | } 61 | #weather-widget #weather-description :hover { 62 | cursor: pointer; 63 | } 64 | 65 | #weather-widget #weather-temperature { 66 | margin-top: 5px; 67 | font-size: 24px; 68 | font-weight: bold; 69 | } 70 | 71 | #weather-widget #weather-humidity, 72 | #weather-widget #weather-wind { 73 | margin-top: 5px; 74 | font-size: 16px; 75 | } 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/styles/PomodoroWidget.css: -------------------------------------------------------------------------------- 1 | .pomodro-h1 { 2 | font-size: 20px; 3 | margin-bottom: 3px; 4 | text-align: center; 5 | font-family: monospace; 6 | font-weight: 700; 7 | color: rgb(26, 22, 22); 8 | } 9 | 10 | .pomodro-timer { 11 | font-size: 50px; 12 | margin-bottom: 1rem; 13 | font-family: monospace; 14 | font-weight: 700; 15 | background-color: rgb(71, 68, 68); 16 | border-radius: 50%; 17 | width: 125px; 18 | 19 | } 20 | 21 | .pomodro-buttons { 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | gap: 1rem; 26 | } 27 | 28 | .start-btn { 29 | padding: 10px 20px; 30 | background-color: #2ecc71; 31 | color: white; 32 | border: none; 33 | font-family: monospace; 34 | font-weight: 700; 35 | border-radius: 5px; 36 | font-size: 12px; 37 | 38 | } 39 | 40 | .start-btn:hover { 41 | background-color: #27ae60; 42 | } 43 | 44 | .pause-btn { 45 | padding: 10px 20px; 46 | background-color: #1f43c5; 47 | color: white; 48 | border: none; 49 | font-family: monospace; 50 | font-weight: 700; 51 | border-radius: 5px; 52 | font-size: 12px; 53 | } 54 | 55 | .pause-btn:hover { 56 | background-color: #1a3ac5; 57 | } 58 | 59 | .restart-btn { 60 | padding: 10px 20px; 61 | background-color: #da481b; 62 | color: white; 63 | border: none; 64 | font-family: monospace; 65 | font-weight: 700; 66 | border-radius: 5px; 67 | font-size: 12px; 68 | } 69 | 70 | .restart-btn:hover { 71 | background-color: #c53e1f; 72 | } 73 | 74 | .CircularProgressbar { 75 | height: 125px; 76 | } 77 | 78 | .pomodro-container{ 79 | display: flex; 80 | flex-direction: column; 81 | justify-content: center; 82 | align-items: center; 83 | gap: 2px; 84 | margin-top: 1rem; 85 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/widgets/DictionaryWidget/DictionaryContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AiFillSound } from 'react-icons/ai'; 3 | import "../../styles/DictionaryWidget.css" 4 | 5 | const handlePronunciation = (data) => { 6 | const audioUrl = data[0]['phonetics'][0]['audio']; 7 | const audio = new Audio(audioUrl); 8 | 9 | audio.play(); 10 | }; 11 | 12 | const DictionaryContainer = ({ data }) => { 13 | return ( 14 |
15 | 16 |

{data[0]['word'].toUpperCase()}

17 | handlePronunciation(data)} 20 | /> 21 |
22 | {data[0]['meanings'].map((item, index) => { 23 | return ( 24 |
25 |

{item['partOfSpeech']}

26 | {item['definitions'].map((defn, index) => { 27 | return ( 28 |
29 |

: {defn['definition']}

30 |
31 | ); 32 | })} 33 |

Synonyms:

34 | {item['synonyms'] && 35 | item['synonyms'].map((syn, index) => { 36 | return ( 37 | 38 | {syn}, 39 | 40 | ); 41 | })} 42 |

Antonyms:

43 | {item['antonyms'] && 44 | item['antonyms'].map((ant, index) => { 45 | return ( 46 | 47 | {ant}, 48 | 49 | ); 50 | })} 51 |
52 | ); 53 | })} 54 |
55 | ); 56 | }; 57 | 58 | export default DictionaryContainer; 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug 2 | description: Report an issue to help improve the project. 3 | title: "[BUG] " 4 | labels: ["bug", "status: awaiting triage"] 5 | body: 6 | - type: checkboxes 7 | id: duplicates 8 | attributes: 9 | label: Has this bug been raised before? 10 | description: Increase the chances of your issue being accepted by making sure it has not been raised before. 11 | options: 12 | - label: I have checked "open" AND "closed" issues and this is not a duplicate 13 | required: true 14 | - type: textarea 15 | id: description 16 | attributes: 17 | label: Description 18 | description: A clear description of the bug you have found. Please include relevant information and resources (for example the steps to reproduce the bug) 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: steps 23 | attributes: 24 | label: Steps to Reproduce 25 | description: To help us recreate the bug, provide a numbered list of the exact steps taken to trigger the buggy behavior. 26 | value: | 27 | Include any relevant details like: 28 | 29 | - What page you were on... 30 | - What you were trying to do... 31 | - What went wrong... 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: screenshots 36 | attributes: 37 | label: Screenshots 38 | description: Please add screenshots if applicable 39 | validations: 40 | required: false 41 | - type: dropdown 42 | id: assignee 43 | attributes: 44 | label: Do you want to work on this issue? 45 | multiple: false 46 | options: 47 | - "No" 48 | - "Yes" 49 | default: 0 50 | validations: 51 | required: false 52 | - type: textarea 53 | id: extrainfo 54 | attributes: 55 | label: If "yes" to above, please explain how you would technically implement this 56 | description: For example reference any existing code 57 | validations: 58 | required: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 💡 General Feature Request 2 | description: Have a new idea/feature? Let us know... 3 | title: "[FEATURE] " 4 | labels: ["enhancement", "feature", "status: awaiting triage"] 5 | body: 6 | - type: checkboxes 7 | id: duplicates 8 | attributes: 9 | label: Is this a unique feature? 10 | description: Increase the chances of your issue being accepted by making sure it has not been raised before. 11 | options: 12 | - label: I have checked "open" AND "closed" issues and this is not a duplicate 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Is your feature request related to a problem/unavailable functionality? Please describe. 17 | description: A clear and concise description of what the problem is (for example "I'm always frustrated when [...]"). 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: description 22 | attributes: 23 | label: Proposed Solution 24 | description: A clear description of the enhancement you propose. Please include relevant information and resources (for example another project's implementation of this feature). 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: screenshots 29 | attributes: 30 | label: Screenshots 31 | description: Please add screenshots of the before and/or after the proposed changes. 32 | validations: 33 | required: false 34 | - type: dropdown 35 | id: assignee 36 | attributes: 37 | label: Do you want to work on this issue? 38 | multiple: false 39 | options: 40 | - "No" 41 | - "Yes" 42 | default: 0 43 | validations: 44 | required: false 45 | - type: textarea 46 | id: extrainfo 47 | attributes: 48 | label: If "yes" to above, please explain how you would technically implement this (issue will not be assigned if this is skipped) 49 | description: For example reference any existing code or library 50 | validations: 51 | required: false -------------------------------------------------------------------------------- /src/widgets/WhiteBoardWidget.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { useRef, useState } from "react"; 3 | import '../styles/WhiteBoardWidget.css'; 4 | 5 | const WhiteBoardWidget = () => { 6 | const canvasRef = useRef(null); 7 | const ctxRef = useRef(null); 8 | const [isDrawing, setDrawing] = useState(false); 9 | 10 | const handleMouseDown = (event) => { 11 | const canvas = canvasRef.current; 12 | const ctx = canvas.getContext("2d"); 13 | const rect = canvas.getBoundingClientRect(); 14 | const x = event.clientX - rect.left; 15 | const y = event.clientY - rect.top; 16 | 17 | ctx.moveTo(x, y); 18 | ctx.beginPath(); 19 | 20 | setDrawing(true); 21 | }; 22 | 23 | const handleMouseMove = (event) => { 24 | if (!isDrawing) return; 25 | 26 | const canvas = canvasRef.current; 27 | const ctx = canvas.getContext("2d"); 28 | const rect = canvas.getBoundingClientRect(); 29 | const x = event.clientX - rect.left; 30 | const y = event.clientY - rect.top; 31 | 32 | ctx.lineTo(x, y); 33 | ctx.stroke(); 34 | }; 35 | 36 | const handleMouseUp = () => { 37 | setDrawing(false); 38 | }; 39 | 40 | const handleMouseLeave = () => { 41 | setDrawing(false); 42 | }; 43 | 44 | const handleReset = () => { 45 | const canvas = canvasRef.current; 46 | const ctx = canvas.getContext("2d"); 47 | ctx.clearRect(0, 0, canvas.width, canvas.height); 48 | }; 49 | 50 | return ( 51 |
52 | 62 | 65 |
66 | ); 67 | }; 68 | 69 | export default WhiteBoardWidget; 70 | -------------------------------------------------------------------------------- /src/widgets/WeatherWidget.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Divider } from 'rsuite'; 3 | import '../styles/WeatherWidget.css'; 4 | 5 | const WeatherWidget = () => { 6 | const [location, setLocation] = useState(''); 7 | const [weatherData, setWeatherData] = useState(null); 8 | const apiKey = 'e9dd92430c41fa069cd1d9606f663ad5'; 9 | 10 | const fetchWeatherData = async () => { 11 | try { 12 | const response = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${location}&appid=${apiKey}`); 13 | const data = await response.json(); 14 | setWeatherData(data); 15 | } catch (error) { 16 | alert('Failed to fetch weather data. Please try again later.') 17 | console.error('Failed to fetch weather data:', error); 18 | } 19 | }; 20 | 21 | const handleLocationChange = (event) => { 22 | setLocation(event.target.value); 23 | }; 24 | 25 | const handleFormSubmit = (event) => { 26 | event.preventDefault(); 27 | fetchWeatherData(); 28 | }; 29 | 30 | return ( 31 |
32 |

Weather

33 |
34 | 35 | 36 |
37 | {weatherData && ( 38 |
39 |
{weatherData.name}
40 | 41 | {/*
{new Date().toDateString()}
*/} 42 |
43 | Weather Icon 47 |
48 |
{weatherData.weather[0].description}
49 |
{(weatherData.main.temp - 273.15).toFixed(1)}°C
50 |
{weatherData.main.humidity}% Humidity
51 |
{weatherData.wind.speed} m/s Wind
52 |
53 | )} 54 |
55 | ); 56 | }; 57 | 58 | export default WeatherWidget; 59 | -------------------------------------------------------------------------------- /src/widgets/GameWidget.js: -------------------------------------------------------------------------------- 1 | import '../styles/GameWidget.css' 2 | import { useState } from 'react'; 3 | import Board from './components/Board'; 4 | import StatusMessage from './components/StatusMessage'; 5 | // import History from './components/History'; 6 | import { calculateWinner } from './winner'; 7 | 8 | const NEWGAME = [{ squares: Array(9).fill(null), isXNext: false }]; 9 | function App() { 10 | const [history, setHistory] = useState(NEWGAME); 11 | const [currentMove, setCurrentMove] = useState(0); 12 | 13 | const gamingBoard = history[currentMove]; 14 | const { winner, winningSquares } = calculateWinner(gamingBoard.squares); 15 | 16 | // console.log({ history, currentMove }); 17 | const handleSquareClick = position => { 18 | if (gamingBoard.squares[position] || winner) { 19 | return; 20 | } 21 | 22 | setHistory(prev => { 23 | const isTraversing = prev.length - 1 !== currentMove; 24 | 25 | const last = isTraversing ? prev[currentMove] : prev[prev.length - 1]; 26 | const nextSquareState = last.squares.map((square, pos) => { 27 | if (pos === position) { 28 | return last.isXNext ? 'X' : 'O'; 29 | } 30 | return square; 31 | }); 32 | 33 | const base = isTraversing ? prev.slice(0, currentMove + 1) : prev; 34 | 35 | return base.concat({ 36 | squares: nextSquareState, 37 | isXNext: !last.isXNext, 38 | }); 39 | }); 40 | 41 | setCurrentMove(prev => prev + 1); 42 | }; 43 | 44 | const moveTo = move => { 45 | setCurrentMove(move); 46 | }; 47 | 48 | const onNewGameStart = () => { 49 | setHistory(NEWGAME); 50 | setCurrentMove(0); 51 | }; 52 | return ( 53 |
54 |

TIC TAC TOE

55 | 56 | 57 | 62 | 63 | 70 |
71 | ); 72 | } 73 | 74 | export default App; 75 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/PasswordGenerator.css: -------------------------------------------------------------------------------- 1 | .password-container { 2 | width: 100%; 3 | max-width: 28rem; 4 | margin: 2rem auto; 5 | padding: 1rem; 6 | background-color: #374151; 7 | border-radius: 0.5rem; 8 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 9 | transition: transform 0.2s ease; 10 | } 11 | 12 | .password-container:hover { 13 | transform: translateY(-2px); 14 | } 15 | 16 | .password-title { 17 | color: white; 18 | text-align: center; 19 | margin: 0.75rem 0; 20 | font-size: 1.5rem; 21 | font-weight: 600; 22 | } 23 | 24 | .password-input-group { 25 | display: flex; 26 | border-radius: 0.5rem; 27 | overflow: hidden; 28 | margin-bottom: 1rem; 29 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 30 | } 31 | 32 | .password-input { 33 | width: 100%; 34 | padding: 0.5rem 0.75rem; 35 | outline: none; 36 | border: none; 37 | background-color: white; 38 | color: #1f2937; 39 | font-family: monospace; 40 | } 41 | 42 | .copy-button { 43 | padding: 0.5rem 1rem; 44 | background-color: #1d4ed8; 45 | color: white; 46 | border: none; 47 | cursor: pointer; 48 | transition: background-color 0.2s ease; 49 | flex-shrink: 0; 50 | font-weight: 500; 51 | } 52 | 53 | .copy-button:hover { 54 | background-color: #1e40af; 55 | } 56 | 57 | .copy-button:active { 58 | transform: scale(0.98); 59 | } 60 | 61 | .controls-container { 62 | display: flex; 63 | gap: 0.5rem; 64 | align-items: center; 65 | flex-wrap: wrap; 66 | padding: 0.5rem 0; 67 | } 68 | 69 | .length-control { 70 | display: flex; 71 | align-items: center; 72 | gap: 0.25rem; 73 | } 74 | 75 | .length-slider { 76 | cursor: pointer; 77 | accent-color: #1d4ed8; 78 | } 79 | 80 | .length-label { 81 | color: white; 82 | font-size: 0.875rem; 83 | } 84 | 85 | .checkbox-control { 86 | display: flex; 87 | align-items: center; 88 | gap: 0.25rem; 89 | padding: 0 0.5rem; 90 | } 91 | 92 | .checkbox-input { 93 | cursor: pointer; 94 | accent-color: #1d4ed8; 95 | } 96 | 97 | .checkbox-label { 98 | color: white; 99 | font-size: 0.875rem; 100 | user-select: none; 101 | } 102 | 103 | @media (max-width: 640px) { 104 | .controls-container { 105 | flex-direction: column; 106 | align-items: flex-start; 107 | gap: 1rem; 108 | } 109 | 110 | .checkbox-control { 111 | padding: 0; 112 | } 113 | } 114 | 115 | .password-input { 116 | transition: background-color 0.2s ease; 117 | } 118 | 119 | .password-input:focus { 120 | background-color: #f3f4f6; 121 | } -------------------------------------------------------------------------------- /src/widgets/BookWidget.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import axios from 'axios'; 3 | import 'boxicons'; 4 | import '../styles/BookWidget.css'; 5 | 6 | const BookWidget = () => { 7 | const [query, setQuery] = useState(''); 8 | const [books, setBooks] = useState([]); 9 | 10 | const handleSearch = async (e) => { 11 | e.preventDefault(); 12 | if (query.trim() === '') return; 13 | 14 | try { 15 | const response = await axios.get(`https://www.googleapis.com/books/v1/volumes?q=${query}`); 16 | setBooks(response.data.items || []); 17 | setQuery(''); 18 | } catch (error) { 19 | console.error('Error fetching books:', error); 20 | } 21 | }; 22 | 23 | const copyToClipboard = (isbn) => { 24 | navigator.clipboard.writeText(isbn).then(() => { 25 | alert('ISBN 13 copied to clipboard!'); 26 | }).catch(err => { 27 | console.error('Failed to copy text: ', err); 28 | }); 29 | }; 30 | 31 | return ( 32 |
33 |
34 | setQuery(e.target.value)} 38 | placeholder="Search for books by title" 39 | className='book-input' 40 | /> 41 | 42 |
43 |
44 | {books.map((book) => { 45 | const isbn13 = book.volumeInfo.industryIdentifiers?.find(identifier => identifier.type === 'ISBN_13')?.identifier || 'No ISBN_13 available'; 46 | return ( 47 |
48 |

{book.volumeInfo.title}

49 |

{book.volumeInfo.authors?.join(', ')}

50 |

{book.volumeInfo.publishedDate}

51 |
52 | 53 |

ISBN 13: {isbn13}

54 | 56 |
57 |
58 |
59 | ); 60 | })} 61 |
62 |
63 | ); 64 | }; 65 | 66 | export default BookWidget; -------------------------------------------------------------------------------- /src/styles/GameWidget.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Roboto', sans-serif; 3 | background-color:transparent; 4 | color: #fff; 5 | padding: 0; 6 | margin: 0; 7 | 8 | } 9 | 10 | button { 11 | background: none; 12 | border: none; 13 | outline: none; 14 | } 15 | 16 | button:hover { 17 | cursor: pointer; 18 | } 19 | 20 | .app { 21 | font-size: 20px; 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | justify-content: center; 26 | width: 300px; 27 | } 28 | 29 | .text-green { 30 | color: #12e177; 31 | } 32 | 33 | .text-orange { 34 | color: #ffc72a; 35 | } 36 | 37 | .history-wrapper { 38 | width: 300px; 39 | text-align: center; 40 | margin-bottom: 20px; 41 | } 42 | 43 | .history-wrapper .history { 44 | display: inline-block; 45 | padding: 0; 46 | margin: 0; 47 | } 48 | 49 | .history-wrapper .history li { 50 | list-style: none; 51 | text-align: left; 52 | } 53 | 54 | .history-wrapper .history li:before { 55 | content: ''; 56 | border-radius: 50%; 57 | display: inline-block; 58 | height: 5px; 59 | width: 5px; 60 | background-color: #12e177; 61 | margin-right: 4px; 62 | margin-bottom: 1px; 63 | } 64 | 65 | .history-wrapper .history li .btn-move { 66 | color: #fff; 67 | } 68 | 69 | .history-wrapper .history li .btn-move.active { 70 | font-weight: bold; 71 | color: #12e177; 72 | } 73 | 74 | .status-message { 75 | margin-bottom: 30px; 76 | font-size: 1.2rem; 77 | font-weight: lighter; 78 | } 79 | 80 | .status-message span { 81 | font-weight: normal; 82 | } 83 | 84 | .btn-reset { 85 | font-size: 0.8rem; 86 | color: #fff; 87 | border-radius: 15px; 88 | padding: 12px 18px; 89 | margin-top: 25px; 90 | transition: all 0.2s; 91 | background-color: #58f638; 92 | box-shadow: 0px 0px 0px 1px #ffc72a; 93 | } 94 | 95 | .btn-reset.active { 96 | background-color: #ffc72a; 97 | box-shadow: none; 98 | } 99 | 100 | .board .board-row { 101 | display: flex; 102 | flex-direction: row; 103 | border-bottom: 2px solid #fff; 104 | } 105 | 106 | .board .board-row:last-child { 107 | border-bottom: none; 108 | } 109 | 110 | .board .board-row .square { 111 | width: 80px; 112 | height: 80px; 113 | border-right: 2px solid #fff; 114 | font-size: 2.5rem; 115 | padding: 0; 116 | overflow: hidden; 117 | transition: all 0.2s; 118 | } 119 | 120 | .board .board-row .square:last-child { 121 | border-right: none; 122 | } 123 | 124 | .board .board-row .square.winning { 125 | animation: scaleText 1.4s infinite; 126 | } 127 | 128 | @keyframes scaleText { 129 | 0% { 130 | transform: 2.5rem; 131 | } 132 | 50% { 133 | font-size: 3.5rem; 134 | } 135 | 100% { 136 | font-size: 2.5rem; 137 | } 138 | } 139 | 140 | 141 | -------------------------------------------------------------------------------- /src/widgets/ReminderListWidget.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | 3 | export default function ReminderListWidget() { 4 | const [reminderListData, setReminderListData] = useState() 5 | 6 | useEffect(() => { 7 | let reminderList = JSON.parse(localStorage.getItem('reminderList')) || [] 8 | if (!reminderList || reminderList == "undefined") { 9 | reminderList = [] 10 | } 11 | setReminderListData(reminderList) 12 | }, []) 13 | 14 | useEffect(() => { 15 | if (!reminderListData) { 16 | return 17 | } 18 | localStorage.setItem('reminderList', JSON.stringify(reminderListData)) 19 | }, [reminderListData]) 20 | 21 | return ( 22 |
23 |
24 |

Reminder List

25 | 40 |
41 | {reminderListData?.length > 0 && reminderListData.map((reminder, index) => { 42 | return ( 43 |
44 |
45 | { 47 | if (e.key === 'Enter') { 48 | let reminderList = [...reminderListData] 49 | reminderList[index].title = e.target.value 50 | reminderList[index].datestamp = new Date() 51 | setReminderListData([...reminderList, { 52 | title: '', 53 | datestamp: '', 54 | }]) 55 | } 56 | }} 57 | onChange={(e) => { 58 | let reminderList = [...reminderListData] 59 | reminderList[index].title = e.target.value 60 | reminderList[index].datestamp = new Date() 61 | setReminderListData(reminderList) 62 | }} /> 63 | 77 |
78 |
79 | ) 80 | })} 81 |
82 | ) 83 | } -------------------------------------------------------------------------------- /src/widgets/AlarmWidget.js: -------------------------------------------------------------------------------- 1 | import '../styles/AlarmWidget.css'; 2 | import { useState } from 'react'; 3 | 4 | const AlarmWidget = () => { 5 | const [date, setDate] = useState(""); 6 | const [time, setTime] = useState(""); 7 | const [isAlarm, setIsAlarm] = useState(false); 8 | 9 | let interval = 0 10 | let container = document.getElementById("alarms"); 11 | let alarmDiv = document.createElement("div"); 12 | let selectedDate = new Date(date + "T" + time); 13 | 14 | const setAlarm = (e) => { 15 | e.preventDefault(); 16 | let now = new Date(); 17 | if (selectedDate <= now) { 18 | //not allowed to set alarm for time before current date 19 | alert(`Invalid time. Please select 20 | a future date and time.`); 21 | return; 22 | } 23 | if (isAlarm){ 24 | //implies that alarm is already set and another cannot be set 25 | alert(`Only one alarm can be set! `); 26 | return; 27 | } 28 | else{ 29 | //no alarm has been set 30 | setIsAlarm(true) 31 | let timeUntilAlarm = selectedDate - now; 32 | alarmDiv.classList.add("alarm"); 33 | alarmDiv.innerHTML = ` 34 |
35 | ${date} 36 | ${time} 37 | 38 |
39 | `; 40 | alarmDiv 41 | .querySelector(".delete-alarm") 42 | .addEventListener("click", () => { 43 | setIsAlarm(false); 44 | alarmDiv.remove(); 45 | clearTimeout(interval); 46 | }); 47 | interval = setTimeout(() => { 48 | alert("Time to wake up!"); 49 | setIsAlarm(false); 50 | alarmDiv.remove(); 51 | }, timeUntilAlarm); 52 | container.appendChild(alarmDiv); 53 | } 54 | } 55 | 56 | return ( 57 | <> 58 |
59 |
60 |
61 | 62 | setDate(e.target.value)} 67 | value={date} 68 | /> 69 |
70 |
71 | 72 | setTime(e.target.value)} 77 | value={time} 78 | /> 79 |
80 | 81 | 82 |
83 |
84 |
85 | 86 | ); 87 | } 88 | 89 | export default AlarmWidget -------------------------------------------------------------------------------- /src/widgets/TimerWidget.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | 3 | export default function TimerWidget() { 4 | const [timerListData, setTimerListData] = useState({ 5 | time: 0, 6 | isRunning: false, 7 | }) 8 | 9 | const tidyTime = (time) => { 10 | let hours = Math.floor(time / 3600) 11 | let minutes = Math.floor((time - (hours * 3600)) / 60) 12 | let seconds = time - (hours * 3600) - (minutes * 60) 13 | 14 | if (hours < 10) { hours = "0" + hours } 15 | if (minutes < 10) { minutes = "0" + minutes } 16 | if (seconds < 10) { seconds = "0" + seconds } 17 | 18 | return hours + ':' + minutes + ':' + seconds 19 | } 20 | 21 | useEffect(() => { 22 | // timer logic 23 | let interval = null 24 | if (timerListData.isRunning && timerListData.time > 0) { 25 | interval = setInterval(() => { 26 | setTimerListData({ 27 | ...timerListData, 28 | time: parseInt(timerListData.time) - 1, 29 | }) 30 | }, 1000) 31 | } else if (!timerListData.isRunning && timerListData.time !== 0) { 32 | setTimerListData({ 33 | ...timerListData, 34 | isRunning: false, 35 | }) 36 | clearInterval(interval) 37 | } else if (timerListData.isRunning && timerListData.time === 0) { 38 | setTimerListData({ 39 | ...timerListData, 40 | isRunning: false, 41 | }) 42 | clearInterval(interval) 43 | alert('Timer is up!') 44 | } 45 | 46 | return () => clearInterval(interval) 47 | }, [timerListData.isRunning, timerListData.time]) 48 | 49 | return ( 50 |
51 |
52 |

Timer

53 |
54 |
55 |
56 | {timerListData.isRunning ? 57 |

{tidyTime(timerListData.time)}

58 | : 59 | { 60 | setTimerListData({ 61 | ...timerListData, 62 | time: e.target.value, 63 | }) 64 | }} /> 65 | } 66 |
67 |
68 | 83 | 98 |
99 |
100 |
101 | ) 102 | } -------------------------------------------------------------------------------- /src/widgets/VideoRecorder.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | import "../styles/VideoRecorderWidget.css"; 3 | 4 | const VideoRecorder = () => { 5 | const [isRecording, setIsRecording] = useState(false); 6 | const [videoBlob, setVideoBlob] = useState(null); 7 | const streamRef = useRef(null); 8 | const videoRef = useRef(null); 9 | const recordedChunks = useRef([]); 10 | const mediaRecorderRef = useRef(null); 11 | 12 | 13 | const startCamera = async () => { 14 | try { 15 | const videoStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); 16 | streamRef.current = videoStream; 17 | videoRef.current.srcObject = videoStream; 18 | } catch (err) { 19 | console.error('Error accessing media devices.', err); 20 | alert('Unable to access camera and microphone. Please check permissions.'); 21 | } 22 | }; 23 | 24 | 25 | const stopCamera = () => { 26 | if (streamRef.current) { 27 | streamRef.current.getTracks().forEach((track) => track.stop()); 28 | streamRef.current = null; 29 | } 30 | }; 31 | 32 | 33 | const startRecording = () => { 34 | if (!streamRef.current) { 35 | startCamera(); 36 | } 37 | 38 | 39 | if (streamRef.current) { 40 | setIsRecording(true); 41 | recordedChunks.current = []; 42 | 43 | const recorder = new MediaRecorder(streamRef.current); 44 | mediaRecorderRef.current = recorder; 45 | 46 | recorder.ondataavailable = (event) => { 47 | if (event.data.size > 0) { 48 | recordedChunks.current.push(event.data); 49 | } 50 | }; 51 | 52 | recorder.onstop = () => { 53 | const blob = new Blob(recordedChunks.current, { type: 'video/webm' }); 54 | setVideoBlob(blob); 55 | stopCamera(); 56 | }; 57 | 58 | recorder.start(); 59 | } else { 60 | alert('Unable to start recording. Please try again.'); 61 | } 62 | }; 63 | 64 | 65 | const stopRecording = () => { 66 | setIsRecording(false); 67 | if (mediaRecorderRef.current) { 68 | mediaRecorderRef.current.stop(); 69 | } 70 | }; 71 | 72 | 73 | const downloadVideo = () => { 74 | if (videoBlob) { 75 | const url = URL.createObjectURL(videoBlob); 76 | const a = document.createElement('a'); 77 | a.style.display = 'none'; 78 | a.href = url; 79 | a.download = 'recorded-video.webm'; 80 | document.body.appendChild(a); 81 | a.click(); 82 | window.URL.revokeObjectURL(url); 83 | } 84 | }; 85 | 86 | return ( 87 |
88 |

Video Recorder

89 | 90 | 91 |
92 | {!isRecording ? ( 93 | 96 | ) : ( 97 | 98 | )} 99 |
100 | 101 | {videoBlob && ( 102 |
103 |

Preview

104 | 105 |
106 | 107 |
108 | )} 109 |
110 | ); 111 | }; 112 | 113 | export default VideoRecorder; 114 | -------------------------------------------------------------------------------- /src/widgets/Pass.js: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useEffect, useRef } from 'react'; 2 | import '../styles/PasswordGenerator.css'; 3 | 4 | function Pass() { 5 | const [password, setPassword] = useState(''); 6 | const [symbols, setSymbols] = useState(false); 7 | const [numbers, setNumbers] = useState(false); 8 | const [length, setLength] = useState(8); 9 | const passwordRef = useRef(null); 10 | 11 | const passwordGenerator = useCallback(() => { 12 | let pass = ''; 13 | let str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 14 | if (numbers) str += '0123456789'; 15 | if (symbols) str += '!@#$%^&*(){}[]_+=-?/>,<.;:|'; 16 | for (let i = 0; i < length; i++) { 17 | let char = Math.floor(Math.random() * str.length); 18 | pass += str.charAt(char); 19 | } 20 | setPassword(pass); 21 | }, [length, symbols, numbers]); 22 | 23 | const copyPasswordToClipboard = useCallback(() => { 24 | passwordRef.current?.select(); 25 | window.navigator.clipboard.writeText(password); 26 | }, [password]); 27 | 28 | useEffect(() => { 29 | passwordGenerator(); 30 | }, [length, symbols, numbers, passwordGenerator]); 31 | 32 | return ( 33 |
34 |

Password Generator

35 | 36 |
37 | 45 | 51 |
52 | 53 |
54 |
55 | { setLength(Number(e.target.value)) }} 62 | /> 63 | 64 |
65 | 66 |
67 | setNumbers((prev) => !prev)} 73 | /> 74 | 77 |
78 | 79 |
80 | setSymbols((prev) => !prev)} 86 | /> 87 | 90 |
91 |
92 |
93 | ); 94 | } 95 | 96 | export default Pass; -------------------------------------------------------------------------------- /src/styles/styles.css: -------------------------------------------------------------------------------- 1 | .react-calendar { 2 | width: 350px; 3 | max-width: 100%; 4 | background: transparent; 5 | font-family: Arial, Helvetica, sans-serif; 6 | line-height: 1.125em; 7 | } 8 | 9 | .react-calendar--doubleView { 10 | width: 700px; 11 | } 12 | 13 | .react-calendar--doubleView .react-calendar__viewContainer { 14 | display: flex; 15 | margin: -0.5em; 16 | } 17 | 18 | .react-calendar--doubleView .react-calendar__viewContainer > * { 19 | width: 50%; 20 | margin: 0.5em; 21 | } 22 | 23 | .react-calendar, 24 | .react-calendar *, 25 | .react-calendar *:before, 26 | .react-calendar *:after { 27 | -moz-box-sizing: border-box; 28 | -webkit-box-sizing: border-box; 29 | box-sizing: border-box; 30 | } 31 | 32 | .react-calendar button { 33 | margin: 0; 34 | border: 0; 35 | outline: none; 36 | } 37 | 38 | .react-calendar button:enabled:hover { 39 | cursor: pointer; 40 | } 41 | 42 | .react-calendar__navigation { 43 | display: flex; 44 | height: 44px; 45 | margin-bottom: 1em; 46 | } 47 | 48 | .react-calendar__navigation button { 49 | min-width: 44px; 50 | background: none; 51 | } 52 | 53 | .react-calendar__navigation button:disabled { 54 | background-color: #f0f0f0; 55 | } 56 | 57 | .react-calendar__navigation button:enabled:hover, 58 | .react-calendar__navigation button:enabled:focus { 59 | background-color: #e6e6e6; 60 | } 61 | 62 | .react-calendar__month-view__weekdays { 63 | text-align: center; 64 | text-transform: uppercase; 65 | font-weight: bold; 66 | font-size: 0.75em; 67 | } 68 | 69 | .react-calendar__month-view__weekdays__weekday { 70 | padding: 0.5em; 71 | } 72 | 73 | .react-calendar__month-view__weekNumbers .react-calendar__tile { 74 | display: flex; 75 | align-items: center; 76 | justify-content: center; 77 | font-size: 0.75em; 78 | font-weight: bold; 79 | } 80 | 81 | .react-calendar__month-view__days__day--weekend { 82 | color: #d10000; 83 | } 84 | 85 | .react-calendar__month-view__days__day--neighboringMonth { 86 | color: #757575; 87 | } 88 | 89 | .react-calendar__year-view .react-calendar__tile, 90 | .react-calendar__decade-view .react-calendar__tile, 91 | .react-calendar__century-view .react-calendar__tile { 92 | padding: 2em 0.5em; 93 | } 94 | 95 | .react-calendar__tile { 96 | max-width: 100%; 97 | padding: 10px 6.6667px; 98 | background: none; 99 | text-align: center; 100 | line-height: 16px; 101 | } 102 | 103 | .react-calendar__tile:disabled { 104 | background-color: #f0f0f0; 105 | } 106 | 107 | .react-calendar__tile:enabled:hover, 108 | .react-calendar__tile:enabled:focus { 109 | background-color: #e6e6e6; 110 | } 111 | 112 | .react-calendar__tile--now { 113 | background: gray; 114 | } 115 | 116 | .react-calendar__tile--now:enabled:hover, 117 | .react-calendar__tile--now:enabled:focus { 118 | background: gray 119 | } 120 | 121 | .react-calendar__tile--hasActive { 122 | background: #76baff; 123 | } 124 | 125 | .react-calendar__tile--hasActive:enabled:hover, 126 | .react-calendar__tile--hasActive:enabled:focus { 127 | background: #a9d4ff; 128 | } 129 | 130 | .react-calendar__tile--active { 131 | background: #006edc; 132 | color: white; 133 | } 134 | 135 | .react-calendar__tile--active:enabled:hover, 136 | .react-calendar__tile--active:enabled:focus { 137 | background: #1087ff; 138 | } 139 | 140 | .react-calendar--selectRange .react-calendar__tile--hover { 141 | background-color: #e6e6e6; 142 | } 143 | -------------------------------------------------------------------------------- /src/modals/WidgetGalleryModal.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import ClockWidget from "../widgets/ClockWidget"; 3 | import ReminderListWidget from "../widgets/ReminderListWidget"; 4 | import TimerWidget from "../widgets/TimerWidget"; 5 | import CalendarWidget from "../widgets/CalendarWidget"; 6 | import WeatherWidget from "../widgets/WeatherWidget"; 7 | import GameWidget from "../widgets/GameWidget"; 8 | import WhiteBoardWidget from "../widgets/WhiteBoardWidget"; 9 | import NewsWidget from "../widgets/NewWidget"; 10 | import QuoteWidget from "../widgets/QuoteWidget"; 11 | import VideoRecorder from "../widgets/VideoRecorder"; 12 | import PomodoroWidget from "../widgets/PomodoroWidget"; 13 | import Pass from "../widgets/Pass"; 14 | export default function WidgetGalleryModal({ 15 | setShowWidgetModal, 16 | selectedWidgetArea, 17 | widgets, 18 | setWidgets, 19 | }) { 20 | const [galleryWidgets, setGalleryWidgets] = useState([ 21 | { component: , name: "Date and Time" }, 22 | { component: , name: "Reminder List" }, 23 | { component: , name: "Timer" }, 24 | { component: , name: "Calendar" }, 25 | { component: , name: "Weather" }, 26 | { component: , name: "Game" }, 27 | { component: , name: "Whiteboard" }, 28 | { component: , name: "News" }, 29 | { component: , name: "Quote" }, 30 | {component: , name : "Video Recorder" }, 31 | { component: , name: "Pomodoro" }, 32 | { component: , name: "Pass"} 33 | ]); 34 | return ( 35 |
{ 38 | setShowWidgetModal(false); 39 | }} 40 | > 41 |

67 | Widget Gallery 68 |

69 | 70 |
71 |
72 | 73 | 74 |
75 | {galleryWidgets.map((widget, index) => { 76 | return ( 77 |
{ 81 | let flag = false; 82 | for (let i = 0; i < widgets.length; i++) { 83 | if (widgets[i].name === widget.name) { 84 | flag = true; 85 | break; 86 | } 87 | } 88 | if (!flag) { 89 | setWidgets([ 90 | ...widgets, 91 | { 92 | id: new Date().getTime(), 93 | component: widget.component, 94 | area: selectedWidgetArea, 95 | name: widget.name, 96 | }, 97 | ]); 98 | setShowWidgetModal(false); 99 | } else { 100 | alert("You can only add one of each widget"); 101 | } 102 | }} 103 | > 104 |
105 |
106 | {widget.name} 107 |
108 |
+
109 |
110 | {widget.component} 111 |
112 | ); 113 | })} 114 |
115 |
116 |
117 |
118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /src/widgets/PomodoroWidget.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useState } from 'react'; 3 | import '../styles/PomodoroWidget.css' 4 | import { CircularProgressbar,buildStyles } from 'react-circular-progressbar'; 5 | import 'react-circular-progressbar/dist/styles.css' 6 | const PomodoroWidget = () => { 7 | const [pomordroSession, setPomordroSession] = useState({ 8 | sessionTime: 1500,//25 minutes 9 | breakTime: 300,//5 minutes 10 | isStudying: false, 11 | isBreak: false, 12 | }); 13 | 14 | const calculateGradient = () => { 15 | const totalTime = pomordroSession.isBreak ? pomordroSession.breakTime : 1500; // Total time in seconds 16 | const remainingTime = pomordroSession.sessionTime; 17 | const percentage = (remainingTime / totalTime) * 100; // Calculate percentage of remaining time 18 | return `${percentage}%`; // Return the percentage for the gradient 19 | }; 20 | 21 | useEffect(()=>{ 22 | let timer = null; 23 | if(pomordroSession.isStudying && pomordroSession.sessionTime>0){ 24 | timer=setInterval(()=>{ 25 | setPomordroSession((prevTime)=>({ 26 | ...prevTime, 27 | sessionTime: prevTime.sessionTime-1, 28 | })) 29 | },1000) 30 | }else if(pomordroSession.isStudying && pomordroSession.sessionTime===0 && !pomordroSession.isBreak){ 31 | //start break 32 | alert('Time to take a break!') 33 | setPomordroSession((prevTime)=>({ 34 | ...prevTime, 35 | isBreak: true, 36 | sessionTime: prevTime.breakTime 37 | })) 38 | clearInterval(timer) 39 | }else if(pomordroSession.isStudying&&pomordroSession.sessionTime===0&&pomordroSession.isBreak){ 40 | //end break 41 | alert('Break is over! Time to start studying again!') 42 | setPomordroSession((prevTime)=>({ 43 | sessionTime:1500, 44 | breakTime:300, 45 | isStudying:false, 46 | isBreak:false 47 | })) 48 | clearInterval(timer) 49 | } 50 | 51 | 52 | return ()=>clearInterval(timer) 53 | },[pomordroSession.isStudying,pomordroSession.sessionTime,pomordroSession.isBreak,pomordroSession.breakTime]) 54 | const remainingTime= (time)=>{ 55 | let minutes = Math.floor(time / 60); 56 | let seconds = time % 60; 57 | if(minutes<10){ 58 | minutes = `0${minutes}`; 59 | } 60 | if(seconds<10){ 61 | seconds = `0${seconds}`; 62 | } 63 | return `${minutes}:${seconds}`; 64 | } 65 | 66 | const timeLeftPercentage = pomordroSession.isBreak 67 | ? (pomordroSession.sessionTime / pomordroSession.breakTime) * 100 68 | : (pomordroSession.sessionTime / 1500) * 100; 69 | 70 | return ( 71 |
72 |

Pomodoro Timer

73 |
76 |
77 | 86 |
87 |
88 | 98 | 108 |
109 |
110 |
111 | 112 | ) 113 | } 114 | 115 | export default PomodoroWidget -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | background: url("background.jpg"); 3 | min-height: 100vh; 4 | display: flex; 5 | justify-content: flex-start; 6 | align-items: center; 7 | } 8 | 9 | .container { 10 | display: flex; 11 | flex-direction: row; 12 | align-items: flex-start; 13 | justify-content: space-between; 14 | gap: 10px; 15 | width: 100%; 16 | padding: 10px; 17 | min-height: 100vh; 18 | } 19 | 20 | .item { 21 | background-color: rgba(255, 255, 255, 0.8); 22 | border: 1px solid rgba(0, 0, 0); 23 | padding: 20px; 24 | font-size: 30px; 25 | text-align: center; 26 | } 27 | 28 | .time { 29 | font-size: 80px; 30 | color: white; 31 | text-align: center; 32 | } 33 | 34 | .date { 35 | font-size: 20px; 36 | color: white; 37 | text-align: center; 38 | font-weight: bold; 39 | } 40 | 41 | .add-widget-button { 42 | background-color: transparent; 43 | color: white; 44 | border: none; 45 | font-size: 15px; 46 | text-align: center; 47 | cursor: pointer; 48 | } 49 | 50 | .add-widget-button:hover { 51 | color: #1e1e1e; 52 | } 53 | 54 | .right-widget, 55 | .left-widget, 56 | .main-widget { 57 | display: flex; 58 | justify-content: center; 59 | align-items: center; 60 | flex-direction: column; 61 | gap: 10px; 62 | height: 100%; 63 | width: 33.33%; 64 | max-width: 500px; 65 | } 66 | 67 | .none-widget { 68 | position: fixed; 69 | top: 0px; 70 | left: 0px; 71 | } 72 | 73 | select { 74 | background-color: #efaeae; 75 | } 76 | 77 | /* Modal */ 78 | .modal { 79 | position: fixed; 80 | z-index: 1; 81 | padding-top: 100px; 82 | left: 0; 83 | top: 0; 84 | width: 100%; 85 | height: 100%; 86 | overflow: auto; 87 | background-color: rgba(0, 0, 0, 0.4); 88 | } 89 | 90 | .modal-content { 91 | margin: auto; 92 | padding: 20px; 93 | width: 50%; 94 | max-height: 80vh; 95 | overflow: auto; 96 | background: rgba(255, 255, 255, 0.3); 97 | box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); 98 | backdrop-filter: blur(20px); 99 | -webkit-backdrop-filter: blur(20px); 100 | border-radius: 10px; 101 | border: 1px solid rgba(255, 255, 255, 0.18); 102 | } 103 | 104 | .modal-header { 105 | margin-top: 30px; 106 | margin-left: 17%; 107 | display: flex; 108 | justify-content: space-between; 109 | align-items: flex-start; 110 | } 111 | 112 | 113 | .row { 114 | display: flex; 115 | justify-content: space-between; 116 | align-items: center; 117 | gap: 10px; 118 | } 119 | 120 | .widget-gallery-item { 121 | color: black; 122 | border: 1px solid white; 123 | padding: 10px; 124 | margin-bottom: 10px; 125 | } 126 | 127 | .text-input { 128 | outline: none; 129 | border: none; 130 | border-bottom: 1px solid white; 131 | width: 100%; 132 | background-color: transparent; 133 | color: white; 134 | font-size: 20px; 135 | margin-top: 10px; 136 | padding-bottom: 3px; 137 | } 138 | 139 | .widget-container { 140 | background: rgba(255, 255, 255, 0.3); 141 | box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); 142 | backdrop-filter: blur(20px); 143 | -webkit-backdrop-filter: blur(20px); 144 | border-radius: 10px; 145 | border: 1px solid rgba(255, 255, 255, 0.18); 146 | } 147 | 148 | .widget-gallery-item-name { 149 | font-size: 20px; 150 | font-weight: bold; 151 | margin-bottom: 10px; 152 | } 153 | 154 | /* News widget styles */ 155 | .news-widget { 156 | color: rgba(255, 182, 193, 0.75); 157 | } 158 | 159 | .news-widget h2 { 160 | color: rgba(255, 182, 193, 0.75); 161 | } 162 | 163 | .news-widget .news-item { 164 | border-bottom: 1px solid rgba(255, 182, 193, 0.75); 165 | padding: 10px 0; 166 | } 167 | 168 | .news-widget .news-item:last-child { 169 | border-bottom: none; 170 | } 171 | 172 | .news-widget .news-date { 173 | color: rgba(255, 182, 193, 0.75); 174 | } 175 | 176 | .dashboard-container { 177 | position: relative; 178 | width: 100%; 179 | min-height: 100vh; 180 | overflow: hidden; 181 | } 182 | 183 | .floating-widget-wrapper { 184 | position: absolute; 185 | width: auto; 186 | height: auto; 187 | } 188 | 189 | .widget-container { 190 | background-color: rgba(255, 182, 193, 0.25); 191 | border-radius: 10px; 192 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 193 | margin-bottom: 20px; 194 | } 195 | 196 | .floating-widget-wrapper .widget-container { 197 | z-index: 1000; 198 | } 199 | 200 | /* Responsive adjustments */ 201 | @media screen and (max-width: 1200px) { 202 | .container { 203 | flex-wrap: wrap; 204 | } 205 | 206 | .right-widget, 207 | .left-widget, 208 | .main-widget { 209 | width: 48%; 210 | max-width: none; 211 | } 212 | } 213 | 214 | @media screen and (max-width: 768px) { 215 | .container { 216 | flex-direction: column; 217 | align-items: center; 218 | } 219 | 220 | .right-widget, 221 | .left-widget, 222 | .main-widget { 223 | width: 100%; 224 | max-width: 500px; 225 | } 226 | 227 | .time { 228 | font-size: 60px; 229 | } 230 | 231 | .date { 232 | font-size: 18px; 233 | } 234 | 235 | .item { 236 | font-size: 24px; 237 | padding: 15px; 238 | } 239 | 240 | .modal-content { 241 | width: 80%; 242 | } 243 | } 244 | 245 | @media screen and (max-width: 480px) { 246 | .time { 247 | font-size: 40px; 248 | } 249 | 250 | .date { 251 | font-size: 16px; 252 | } 253 | 254 | .item { 255 | font-size: 20px; 256 | padding: 10px; 257 | } 258 | 259 | .modal-header h2 { 260 | font-size: 20px; 261 | } 262 | 263 | .widget-gallery-item-name { 264 | font-size: 18px; 265 | } 266 | 267 | .modal-content { 268 | width: 90%; 269 | } 270 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Planner App 2 | 3 | ## Repository Stats 4 | 5 |
6 | 7 | ![Static Badge](https://img.shields.io/badge/BUILD_WITH-%E2%99%A5-orange?style=flat-square&labelColor=orange) 8 | ![Static Badge](https://img.shields.io/badge/PRS_WELCOME-green?style=flat-square) 9 | ![GitHub watchers](https://img.shields.io/github/watchers/mohitahlawat2001/shiny-octo-planner-app?style=flat-square&label=WATCHERS) 10 | ![GitHub repo size](https://img.shields.io/github/repo-size/mohitahlawat2001/shiny-octo-planner-app?style=flat-square&label=REPO+SIZE) 11 | ![GitHub forks](https://img.shields.io/github/forks/mohitahlawat2001/shiny-octo-planner-app?style=flat-square&label=FORKS) 12 | ![GitHub Repo stars](https://img.shields.io/github/stars/mohitahlawat2001/shiny-octo-planner-app?style=flat-square&label=STARS) 13 | ![GitHub top language](https://img.shields.io/github/languages/top/mohitahlawat2001/shiny-octo-planner-app?style=flat-square&logo=javascript&label=TOP%20LANGUAGE) 14 | ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues-pr-closed/mohitahlawat2001/shiny-octo-planner-app?style=flat-square&label=PULL+REQUESTS) 15 | ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues-closed-raw/mohitahlawat2001/shiny-octo-planner-app?style=flat-square&label=CLOSED%20ISSUES) 16 | ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/mohitahlawat2001/shiny-octo-planner-app?style=flat-square&label=ISSUES) 17 | ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues-pr/mohitahlawat2001/shiny-octo-planner-app?style=flat-square&label=PULL%20REQUESTS) 18 | ![GitHub contributors](https://img.shields.io/github/contributors/mohitahlawat2001/shiny-octo-planner-app?style=flat-square&label=CONTRIBUTORS) 19 | ![GitHub last commit](https://img.shields.io/github/last-commit/mohitahlawat2001/shiny-octo-planner-app?display_timestamp=committer&style=flat-square&label=LAST%20COMMIT) 20 | ![GitHub License](https://img.shields.io/github/license/mohitahlawat2001/shiny-octo-planner-app?style=flat-square&label=LICENSE) 21 | 22 |
23 | 24 | ## Socials 25 | 26 | [with minimal functionality](https://mohitahlawat-planner-app.netlify.app/) 27 | [with mild functionality](https://mohitahlawat-planner-app.web.app/) 28 | 29 | ## How to Use Locally 30 | 31 | Follow these steps to run the planner application on your local machine: 32 | 33 | 1. **Clone the Repository** 34 | ``` 35 | git clone https://github.com/mohitahlawat2001/shiny-octo-planner-app.git 36 | ``` 37 | 2. **Navigate to the Project Directory** 38 | ``` 39 | cd shiny-octo-planner-app 40 | ``` 41 | 3. **Install Dependencies** 42 | ``` 43 | npm install 44 | ``` 45 | 4. **Start the Application** 46 | ``` 47 | npm start 48 | ``` 49 | 5. **Open in Browser** 50 | - Open your web browser and go to [http://localhost:3000](http://localhost:3000) 51 | - You should now see the calculator application running locally on your machine. 52 | 6. **Make Sure add what you did in readme** 53 | - detail about feature 54 | 55 | Follow these steps to run the planner application on your local machine using Docker: 56 | 57 | 1.**Create Docker Image** 58 | ``` 59 | docker build -t my-app . 60 | ``` 61 | 2.**Run Docker Image** 62 | ``` 63 | docker run -p 3000:3000 my-app 64 | ``` 65 | 3. **Open in Browser** 66 | - Open your web browser and go to [http://localhost:3000](http://localhost:3000) 67 | - You should now see the calculator application running locally on your machine inside Docker. 68 | 69 | 70 | 71 | ## Project Setup 72 | 3 folders widgets, styles and modals. As the names suggest, 73 | - we will place all the JavaScript files we create for the widgets into the widgets folder 74 | - all the modals into the modals folder 75 | - a particular CSS file from one of the components into the styles folder. Essentially, we will copy the styles that comes built into one of the packages we have installed in Step 1 Node Package Manager (NPM). This is so that we can customise the styles of the packages instead of being limited to the default style. 76 | 77 | 78 | ## Weather Widget 79 | 80 | The custom-made **Weather Widget** is a valuable addition to the planner app as it provides users with real-time weather information for a specific location. By retrieving data from the OpenWeatherMap API, the widget allows users to plan their activities effectively by considering temperature, humidity, wind speed, and weather description. 81 | 82 | function fetchWeatherData() 83 | 84 | This function is responsible for fetching the weather data from the API based on the user's specified location. It uses an asynchronous HTTP request to retrieve the data and updates the state with the received weather information. 85 | 86 | With its user-friendly interface and clear presentation of weather data, the Weather Widget enhances the app's functionality and provides users with the essential information they need to make informed decisions and optimize their daily schedules. 87 | 88 | 89 | ## Custom Game Widget 90 | 91 | The custom-made **Game Widget** is an ideal addition to the planner app, providing users with an interactive and enjoyable gaming experience. 92 | 93 | ```javascript 94 | function handleSquareClick(position) { 95 | // Logic for handling square click 96 | // Check if square is occupied or if there is a winner 97 | // Update game history and determine next player's turn 98 | } 99 | ``` 100 | 101 | The highlighted main function `handleSquareClick` is responsible for managing the logic when a user clicks on a square on the game board. It ensures that the square is not already occupied and there is no winner before updating the game history and determining the next player's turn. 102 | 103 | Featuring an appealing design and a responsive user interface, the Game Widget enables users to play Tic Tac Toe effortlessly. It incorporates game history tracking, allowing users to undo moves and start a new game with a simple button click. 104 | 105 | By integrating the Game Widget into the planner app, users can take a break from their tasks and engage in a fun and strategic game within the app itself. This widget offers entertainment, promotes user engagement, and enhances the overall user experience of the planner app. 106 | 107 | 108 | ## WhiteBoardWidget 109 | 110 | The WhiteBoardWidget is a custom-made widget that provides a whiteboard functionality within the planner app. It allows users to draw and sketch their ideas directly on the screen. The main functionality of the widget includes handling mouse events to track drawing actions, such as mouse down, mouse move, and mouse up. It utilizes HTML5 canvas and the 2D drawing context to create a drawing area. The widget provides a responsive and interactive drawing experience, allowing users to freely express their thoughts and visualize their plans. Additionally, it offers a reset button to clear the canvas and start a new drawing session. The WhiteBoardWidget is a valuable addition to the planner app as it enhances creativity, facilitates visual thinking, and provides a convenient platform for brainstorming and organizing ideas. 111 | 112 | 113 | ## NewsWidget 114 | 115 | 116 | The `NewsWidget` component is an appropriate widget to have in the planner app because it provides users with quick access to top headlines from various news sources. It keeps users informed and updated without leaving the planner app interface. The widget fetches news data using the NewsAPI, specifically retrieving top headlines from the US. 117 | 118 | To use the `NewsWidget`, simply add it to the planner app's interface. Once rendered, it will display a list of top headlines along with their descriptions and images. The component makes use of React's `useState` and `useEffect` hooks to fetch the news data asynchronously. While the data is being fetched, a loading message is displayed. Once the data is retrieved, it is mapped over to generate the list of news items with clickable links to the full articles. 119 | 120 | The `NewsWidget` is designed with a clean and organized layout. The headlines are displayed as clickable links, allowing users to open the articles in a new tab. Each news item also includes a brief description and an image, providing a visual context to the headlines. The component's CSS file ensures proper styling, including a specific class for the image container to maintain consistent dimensions. 121 | 122 | Overall, the `NewsWidget` enhances the planner app's functionality by integrating relevant news updates, making it convenient for users to stay informed while managing their schedules and tasks. 123 | 124 | 125 | ## QuoteWidget 126 | 127 | 128 | The `QuoteWidget` component is a valuable addition to the planner app as it offers users a source of inspiration and motivation through random quotes. By integrating this widget, users can receive a new quote with a click of a button, enhancing their planning experience with meaningful and uplifting content. The widget fetches quotes from the Quotable API, presenting them in a visually appealing layout with clear separation between the quote content and author. The widget's design is complemented by the provided CSS file, ensuring proper styling and a consistent look. By incorporating the `QuoteWidget`, users can access daily doses of inspiration, empowering them to stay focused and positive throughout their planning endeavors. 129 | 130 | 131 | 132 | ## Contributing 133 | 134 | If you'd like to contribute, please fork the repository and create a pull request. You can also open an issue for any bug reports or feature requests. 135 | 136 | --- 137 | 138 | Feel free to reach out with any questions or feedback. Happy contributing!✨ 139 | 140 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import "./App.css"; 3 | import WidgetGalleryModal from "./modals/WidgetGalleryModal"; 4 | import Draggable from "react-draggable"; 5 | import WeatherWidget from "./widgets/WeatherWidget"; 6 | import GameWidget from "./widgets/GameWidget"; 7 | import WhiteBoardWidget from "./widgets/WhiteBoardWidget"; 8 | import NewsWidget from "./widgets/NewWidget"; 9 | import QuoteWidget from "./widgets/QuoteWidget"; 10 | import ClockWidget from "./widgets/ClockWidget"; 11 | import BookWidget from "./widgets/BookWidget"; 12 | import VideoRecorder from "./widgets/VideoRecorder"; 13 | import PomodoroWidget from './widgets/PomodoroWidget'; 14 | import Pass from './widgets/Pass'; 15 | import AlarmWidget from './widgets/AlarmWidget'; 16 | import IPAddressWidget from './widgets/IPAddressWidget'; 17 | import Dictionary from './widgets/DictionaryWidget/Dictionary'; 18 | 19 | function App() { 20 | const [widgets, setWidgets] = useState([ 21 | { 22 | id: new Date().getTime(), 23 | component: , 24 | area: "right-widget", 25 | name: "Weather", 26 | }, 27 | { 28 | id: new Date().getTime() + 4, 29 | component: , 30 | area: "left-widget", 31 | name: "Quote", 32 | }, 33 | { 34 | id: new Date().getTime() + 5, 35 | component: , 36 | area: "main-widget", 37 | name: "Date and Time", 38 | }, 39 | { 40 | id: new Date().getTime() + 1, 41 | component: , 42 | area: "main-widget", 43 | name: "Game", 44 | }, 45 | { 46 | id: new Date().getTime() + 2, 47 | component: , 48 | area: "left-widget", 49 | name: "Whiteboard", 50 | }, 51 | { 52 | id: new Date().getTime() + 6, 53 | component: , 54 | area: "right-widget", 55 | name: "Book", 56 | }, 57 | { 58 | id: new Date().getTime() + 7, 59 | component: , 60 | area: "left-widget", 61 | name: "VideoRecorder", 62 | }, 63 | { 64 | id: new Date().getTime() + 8, 65 | component: , 66 | area: "main-widget", 67 | name: "Pomodoro Timer", 68 | }, 69 | { 70 | id: new Date().getTime() + 9, 71 | component: , 72 | area: "right-widget", 73 | name: "Pass", 74 | }, 75 | { 76 | id: new Date().getTime() + 10, 77 | component: , 78 | area: "right-widget", 79 | name: "Alarm Clock" 80 | }, 81 | { 82 | id: new Date().getTime() + 11, 83 | component: , 84 | area: "left-widget", 85 | name: "IP Address" 86 | }, 87 | { 88 | id: new Date().getTime() + 12, 89 | component: , 90 | area: "main-widget", 91 | name: "Dictionary" 92 | } 93 | 94 | ]); 95 | 96 | const [showWidgetModal, setShowWidgetModal] = useState(false); 97 | const [selectedWidgetArea, setSelectedWidgetArea] = useState(""); 98 | 99 | const removeWidget = (index) => { 100 | try { 101 | setWidgets(widgets.filter((widget) => widget.id !== index)); 102 | } catch (error) { 103 | console.log(error); 104 | } 105 | }; 106 | 107 | return ( 108 | <> 109 | {showWidgetModal && ( 110 | 116 | )} 117 |
118 |
119 | {widgets.length > 0 && 120 | widgets.map((widget, index) => { 121 | if (widget.area === "none-widget") { 122 | return ( 123 | 128 |
129 |
130 |
131 | 144 | 159 |
160 | {widget.component} 161 |
162 |
163 |
164 | ); 165 | } 166 | return null; 167 | })} 168 |
169 |
170 | {widgets.length > 0 && 171 | widgets.map((widget, index) => { 172 | if (widget.area === "left-widget") { 173 | return ( 174 |
175 |
176 | 189 | 204 |
205 | {widget.component} 206 |
207 | ); 208 | } 209 | })} 210 | {widgets.filter((widget) => widget.area === "left-widget").length < 211 | 3 && 212 | widgets.filter((widget) => widget.area === "left-widget").length > 213 | 0 && ( 214 | 223 | )} 224 |
225 | 226 |
227 | {widgets.length > 0 && 228 | widgets.map((widget, index) => { 229 | if (widget.area === "main-widget") { 230 | return ( 231 |
232 |
233 | 246 | 261 |
262 | {widget.component} 263 |
264 | ); 265 | } 266 | })} 267 | {widgets.filter((widget) => widget.area === "main-widget").length < 268 | 2 && ( 269 | 278 | )} 279 |
280 | 281 |
282 | {widgets.length > 0 && 283 | widgets.map((widget, index) => { 284 | if (widget.area === "right-widget") { 285 | return ( 286 |
287 |
288 | 301 | 316 |
317 | {widget.component} 318 |
319 | ); 320 | } 321 | })} 322 | {widgets.filter((widget) => widget.area === "right-widget").length < 323 | 3 && 324 | widgets.filter((widget) => widget.area === "right-widget") 325 | .length > 0 && ( 326 | 335 | )} 336 |
337 |
338 |
339 |
340 | 341 | ); 342 | } 343 | 344 | export default App; 345 | --------------------------------------------------------------------------------