├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docs ├── DarkSouls-theme.png ├── default-theme.png └── pop!os-theme.png ├── index.html ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── Startpage │ ├── LinkContainer │ │ ├── Accordion │ │ │ └── Accordion.tsx │ │ └── LinkContainer.tsx │ ├── Searchbar │ │ └── Searchbar.tsx │ ├── Settings │ │ ├── Changelog │ │ │ └── Changelog.tsx │ │ ├── DesignSettings │ │ │ └── DesignSettings.tsx │ │ ├── LinkSettings │ │ │ ├── LinkSettings.tsx │ │ │ └── OptionTextArea.tsx │ │ ├── SearchSettings │ │ │ ├── FastForwardSearch.tsx │ │ │ └── SearchSettings.tsx │ │ ├── Settings.tsx │ │ ├── SettingsWindow.tsx │ │ └── settingsHandler.ts │ └── Startpage.tsx ├── base │ ├── FiraCode-Light.woff │ ├── FiraCode-Medium.woff │ ├── FiraCode-Regular.woff │ ├── FuturaLTLight.woff │ ├── animations.css │ ├── index.css │ └── variables.css ├── components │ ├── ColorPicker.tsx │ ├── Dropdown.tsx │ ├── IconButton.tsx │ ├── OptionSlider.tsx │ └── OptionTextInput.tsx ├── data │ ├── changelog.tsx │ ├── data.ts │ └── pictures │ │ ├── duckduckgo.svg │ │ ├── ecosia.svg │ │ ├── google.svg │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── pic_1.jpg │ │ ├── pic_2.jpg │ │ ├── pic_3.jpg │ │ ├── pic_4.jpg │ │ ├── pic_5.jpg │ │ ├── pic_6.jpg │ │ ├── pic_7.jpg │ │ ├── pic_8.png │ │ ├── qwant.svg │ │ └── sauce.txt ├── index.tsx └── vite-env.d.ts ├── tsconfig.json └── vite.config.ts /.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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #-- BUILD 2 | FROM node:18-alpine AS build 3 | 4 | USER node 5 | WORKDIR /home/node 6 | 7 | ##-- Copy everything into the container 8 | ADD --chown=node:node ./public ./public 9 | ADD --chown=node:node ./src ./src 10 | ADD --chown=node:node ./package.json . 11 | ADD --chown=node:node ./tsconfig.json . 12 | 13 | ##-- Build the app 14 | RUN npm install 15 | RUN npm run build 16 | 17 | 18 | #-- DEPLOYMENT 19 | FROM nginx:alpine 20 | 21 | ##-- Copy app build into nginx 22 | COPY --from=build /home/node/build /usr/share/nginx/html 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 PrettyCoffee 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](https://github.com/PrettyCoffee/fluidity/blob/main/public/logo192.png) 2 | 3 | # Fluidity - An accordion based startpage 4 | Here you can find the startpage I created for my browser :) 5 | 6 | If you have any problems or miss a feature, create an issue and I will take a look at it! Of course, if you want to add a feature yourself you can just create a fork and contribute ;) 7 | 8 | ## Showcase 9 | ### The startpage in action 10 | I created a [reddit post](https://www.reddit.com/r/startpages/comments/m82izg/my_new_startpage_any_ideas_for_names/) on r/startpages. There you can see a short video where I show all available features. 11 | 12 | You can also just take a look at the [Live Demo](https://prettycoffee.github.io/fluidity/). 13 | 14 | ### Themes 15 | ![Default theme](https://github.com/PrettyCoffee/fluidity/blob/main/docs/default-theme.png) 16 | ![Dark Souls theme](https://github.com/PrettyCoffee/fluidity/blob/main/docs/DarkSouls-theme.png) 17 | ![Pop!OS theme](https://github.com/PrettyCoffee/fluidity/blob/main/docs/pop!os-theme.png) 18 | **If you created a theme and want to see it here, hit me up!** 19 | 20 | ## Usage 21 | You can apply startpages by using several methods. To keep it simple, I will only cover one (the easiest) here: 22 | 1. Download a New Tab Override Plugin (e.g. [Chrome](https://chrome.google.com/webstore/detail/new-tab-redirect/icpgjfneehieebagbmdbhnlpiopdcmna) | [Firefox](https://addons.mozilla.org/en-US/firefox/addon/new-tab-override/)) 23 | 1. Open the Plugins Settings 24 | 1. Paste `https://prettycoffee.github.io/fluidity/` into the text field to set it up as your startpage 25 | 26 | ## Local Setup 27 | If you do not want to rely on my github page, thats totally okay! 28 | You can set it up locally yourself with the following steps: 29 | 1. Switch into the gh-pages branch 30 | 1. Download / Clone the repository files 31 | 1. Set it up like explained in [usage](#usage), but instead of the link use the filepath to the `/index.html` file. 32 | 33 | If you have a github account you can of course also just fork the repo and create a github page yourself ;) 34 | 35 | ## Docker setup 36 | If you are familiar with Docker, you can use the provided docker file which will build the app and deploy it with nginx. 37 | 38 | You can use the following commands to deploy a container: 39 | 40 | ```bash 41 | # build 42 | $ docker build ./ -t fluidity 43 | 44 | # run 45 | $ docker run -d --name fluidity -p 8080:80 fluidity 46 | ``` 47 | 48 | It will be deployed on port 8080. (`http:\\localhost:8080`) 49 | 50 | ## Advanced: Changing the code 51 | Since this project is programmed with React and TypeScript, you will first need to set it up: 52 | 53 | 0. (Download and install [nodejs](https://nodejs.org/en/) if you dont have it) 54 | 1. Clone the git repository, this time use the main branch 55 | 1. Open a terminal in the project folder (If you execute the command `ls` here, there should be a package.json) 56 | 1. Execute `npm i` to install all dependencies 57 | 1. Execute `npm run start` to validate that everything ids working. A browser tab with the URL `http://localhost:3000` and the startpage should open. 58 | 1. Now you can change the code, for example write your own default values into `/src/data/data.ts` 59 | 1. Compile the project by executing `npm run build` if everything is done 60 | 1. Your startpage is now located in the `/build/` folder 61 | 1. Optional: If you host it with github pages yourself, you can use the command `npm run deploy` to push a fresh build into the gh-pages branch 62 | 63 | ## Sources 64 | 65 | * [Pictures - DeathAndMilk](https://www.instagram.com/deathandmilk_/) 66 | * [Icons - FontAwesome](https://fontawesome.com/icons) 67 | * [Text Flicker - CodeMyUI](https://codemyui.com/crt-screen-text-flicker-animation-in-pure-css/) 68 | * [Wave Animation - mburakerman](https://codepen.io/mburakerman/pen/eRZZEv) 69 | -------------------------------------------------------------------------------- /docs/DarkSouls-theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/docs/DarkSouls-theme.png -------------------------------------------------------------------------------- /docs/default-theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/docs/default-theme.png -------------------------------------------------------------------------------- /docs/pop!os-theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/docs/pop!os-theme.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | Fluidity 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fluidity", 3 | "description": "An accordion based startpage.", 4 | "version": "0.6.0", 5 | "private": true, 6 | "homepage": "./", 7 | "scripts": { 8 | "predeploy": "npm run build", 9 | "deploy": "gh-pages -d build", 10 | "start": "vite", 11 | "build": "vite build", 12 | "serve": "vite preview", 13 | "lint": "eslint ./src --ignore-path .gitignore", 14 | "lint:fix": "npm run lint -- --fix" 15 | }, 16 | "dependencies": { 17 | "@emotion/react": "^11.11.1", 18 | "@emotion/styled": "^11.11.0", 19 | "@fortawesome/free-solid-svg-icons": "^6.4.2", 20 | "@fortawesome/react-fontawesome": "^0.2.0", 21 | "react": "^18.2.0", 22 | "react-color": "^2.19.3", 23 | "react-dom": "^18.2.0" 24 | }, 25 | "devDependencies": { 26 | "@pretty-cozy/eslint-config": "^0.2.0", 27 | "@types/react": "^18.2.18", 28 | "@types/react-color": "^3.0.6", 29 | "@types/react-dom": "^18.2.7", 30 | "@vitejs/plugin-react": "^4.0.4", 31 | "gh-pages": "^4.0.0", 32 | "typescript": "^4.9.5", 33 | "vite": "^4.4.8", 34 | "vite-plugin-checker": "^0.6.1" 35 | }, 36 | "browserslist": [ 37 | "defaults" 38 | ], 39 | "eslintConfig": { 40 | "extends": [ 41 | "@pretty-cozy/eslint-config/base-ts", 42 | "@pretty-cozy/eslint-config/react" 43 | ], 44 | "rules": { 45 | "check-file/folder-naming-convention": "off", 46 | "react/jsx-max-depth": "off" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Fluidity", 4 | "description": "An accordion based browser startpage.", 5 | "developer": { 6 | "name": "PrettyCoffee", 7 | "url": "https://prettycoffee.github.io" 8 | }, 9 | "permissions": [ 10 | "management", 11 | "history", 12 | "storage" 13 | ], 14 | "content_security_policy": "script-src 'self' 'sha256-xuejLQ29qZc344LEr80Z2fLQc/Q/4XaQRaIDtWFnz2Y='; object-src 'self'", 15 | "chrome_url_overrides": { 16 | "newtab": "./index.html" 17 | } 18 | } -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import "./base/variables.css" 4 | 5 | import * as Settings from "./Startpage/Settings/settingsHandler" 6 | import { Startpage } from "./Startpage/Startpage" 7 | 8 | const App = () => { 9 | //Apply colors 10 | const root = document.documentElement 11 | const colors = Settings.Design.getWithFallback().colors 12 | Object.keys(colors).forEach(key => { 13 | root.style.setProperty(key, colors[key]) 14 | }) 15 | 16 | return 17 | } 18 | 19 | export default App 20 | -------------------------------------------------------------------------------- /src/Startpage/LinkContainer/Accordion/Accordion.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | MouseEvent, 3 | PropsWithChildren, 4 | useEffect, 5 | useRef, 6 | useState, 7 | } from "react" 8 | 9 | import styled from "@emotion/styled" 10 | 11 | const StyledAccordionContainer = styled.div` 12 | margin-left: 100px; 13 | display: flex; 14 | width: calc(100% - 400px - 100px); 15 | ` 16 | 17 | export const AccordionContainer = ({ children }: PropsWithChildren) => ( 18 | {children} 19 | ) 20 | 21 | const StyledAccordionGroup = styled.div` 22 | height: 400px; 23 | display: flex; 24 | padding: 0 10px; 25 | flex-direction: row; 26 | border-right: 3px solid var(--default-color); 27 | :first-of-type { 28 | border-left: 3px solid var(--default-color); 29 | } 30 | ` 31 | 32 | const AccordionContent = styled.div<{ width: number }>` 33 | height: 100%; 34 | width: ${({ width }) => `${width}px`}; 35 | display: flex; 36 | flex-direction: column; 37 | justify-content: center; 38 | overflow: hidden; 39 | transition: 0.3s; 40 | ` 41 | 42 | const AccordionTitleWrapper = styled.button<{ active: boolean }>` 43 | padding: 0; 44 | background-color: var(--bg-color); 45 | border: 4px solid var(--accent-color); 46 | height: 100%; 47 | width: 90px; 48 | cursor: ${({ active }) => (active ? "default" : "pointer")}; 49 | display: flex; 50 | align-items: center; 51 | justify-content: center; 52 | opacity: 0.8; 53 | position: relative; 54 | ::before { 55 | content: ""; 56 | position: absolute; 57 | bottom: 0px; 58 | width: 100%; 59 | height: ${({ active }) => (active ? "390px" : "0")}; 60 | background-color: var(--accent-color); 61 | transition: ${({ active }) => (active ? "1s" : ".5s")}; 62 | } 63 | :hover, 64 | :focus { 65 | outline: none; 66 | ${({ active }) => 67 | !active && 68 | ` 69 | ::before { 70 | height: 50%; 71 | } 72 | > .wave { 73 | top: 180px; 74 | ::before{ 75 | animation: wave 12s infinite cubic-bezier(0.71, 0.33, 0.33, 0.68); 76 | top: -25%; 77 | left: 50%; 78 | } 79 | } 80 | `} 81 | } 82 | 83 | > .wave { 84 | /* Waves Source: https://codepen.io/mburakerman/pen/eRZZEv */ 85 | width: 82px; 86 | height: 50px; 87 | position: absolute; 88 | top: ${({ active }) => (active ? "0px" : "350px")}; 89 | overflow: hidden; 90 | transition: ${({ active }) => (active ? "1s" : ".5s")}; 91 | ::before { 92 | content: ""; 93 | width: 180px; 94 | height: 185px; 95 | position: absolute; 96 | top: -25%; 97 | left: 50%; 98 | margin-left: -90px; 99 | margin-top: -140px; 100 | border-radius: 37%; 101 | background-color: var(--bg-color); 102 | animation: wave 12s infinite cubic-bezier(0.71, 0.33, 0.33, 0.68); 103 | } 104 | @keyframes wave { 105 | from { 106 | transform: rotate(0deg); 107 | } 108 | from { 109 | transform: rotate(360deg); 110 | } 111 | } 112 | } 113 | 114 | ${({ active }) => 115 | !active && 116 | ` 117 | :hover{ 118 | > * { 119 | color: var(--bg-color); 120 | text-shadow: 121 | 5px 0px 0 var(--accent-color), 122 | 4px 0px 0 var(--accent-color), 123 | 3px 0px 0 var(--accent-color), 124 | 2px 0px 0 var(--accent-color), 125 | 1px 0px 0 var(--accent-color), 126 | -1px 0px 0 var(--accent-color), 127 | 0px 1px 0 var(--accent-color), 128 | 0px -1px 0 var(--accent-color); 129 | } 130 | } 131 | `}; 132 | ` 133 | 134 | const AccordionTitle = styled.h1<{ title: string; active: boolean }>` 135 | transform: rotate(90deg); 136 | min-width: max-content; 137 | color: ${({ active }) => 138 | active ? "var(--bg-color)" : "var(--default-color)"}; 139 | transition: 0.5s; 140 | letter-spacing: 5px; 141 | ` 142 | 143 | type groupProps = PropsWithChildren<{ 144 | active: boolean 145 | title: string 146 | onClick: () => void 147 | onMouseDown: (e: MouseEvent) => void 148 | }> 149 | 150 | export const AccordionGroup = ({ 151 | active, 152 | title, 153 | children, 154 | onClick, 155 | onMouseDown, 156 | }: groupProps) => { 157 | const ref = useRef(null) 158 | const [contentWidth, setContentWidth] = useState(active ? 500 : 0) 159 | useEffect(() => { 160 | const parent = ref.current?.parentElement 161 | if (parent && active) { 162 | setContentWidth(parent.clientWidth - parent.children.length * 113 - 3) 163 | } else { 164 | setContentWidth(0) 165 | } 166 | }, [active]) 167 | 168 | return ( 169 | 170 | 176 |
177 | 178 | {title} 179 | 180 | 181 | 182 | {children} 183 | 184 | 185 | ) 186 | } 187 | -------------------------------------------------------------------------------- /src/Startpage/LinkContainer/LinkContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEvent, useState } from "react" 2 | 3 | import styled from "@emotion/styled" 4 | 5 | import { AccordionContainer, AccordionGroup } from "./Accordion/Accordion" 6 | import * as Settings from "../Settings/settingsHandler" 7 | 8 | const LinkItem = styled.a` 9 | width: fit-content; 10 | white-space: nowrap; 11 | position: relative; 12 | padding: 10px 0 10px 30px; 13 | font-size: 1rem; 14 | 15 | ::before { 16 | position: absolute; 17 | left: 0px; 18 | bottom: 5px; 19 | z-index: 0; 20 | content: ""; 21 | height: 5px; 22 | width: 100%; 23 | background-color: var(--accent-color); 24 | transition: 0.5s; 25 | opacity: 0.7; 26 | } 27 | 28 | :hover, 29 | :focus { 30 | color: var(--accent-color2); 31 | animation: text-flicker 0.01s ease 0s infinite alternate; 32 | outline: none; 33 | } 34 | ` 35 | 36 | export const LinkContainer = () => { 37 | const [active, setActive] = useState(0) 38 | const linkGroups = Settings.Links.getWithFallback() 39 | 40 | const middleMouseHandler = (event: MouseEvent, groupIndex: number) => { 41 | setActive(groupIndex) 42 | if (event.button === 1) { 43 | linkGroups[groupIndex].links.forEach(link => { 44 | window.open(link.value, "_blank") 45 | }) 46 | } 47 | } 48 | 49 | return ( 50 | 51 | {linkGroups.map((group, groupIndex) => ( 52 | setActive(groupIndex)} 57 | onMouseDown={e => middleMouseHandler(e, groupIndex)} 58 | > 59 | {group.links.map(link => ( 60 | 65 | {link.label} 66 | 67 | ))} 68 | 69 | ))} 70 | 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /src/Startpage/Searchbar/Searchbar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import styled from "@emotion/styled" 4 | 5 | import duckduckgo from "../../data/pictures/duckduckgo.svg" 6 | import ecosia from "../../data/pictures/ecosia.svg" 7 | import google from "../../data/pictures/google.svg" 8 | import qwant from "../../data/pictures/qwant.svg" 9 | import * as Settings from "../Settings/settingsHandler" 10 | 11 | export const queryToken = "{{query}}" 12 | 13 | const StyledSearchbarContainer = styled.div` 14 | position: absolute; 15 | left: calc(100px - 2.9rem - 10px); 16 | right: 100px; 17 | bottom: 40px; 18 | height: min-content; 19 | display: flex; 20 | align-items: flex-start; 21 | justify-content: center; 22 | ` 23 | const StyledSearchbar = styled.input` 24 | width: 100%; 25 | font-size: 30pt; 26 | 27 | background-color: rgba(0, 0, 0, 0); 28 | color: var(--default-color); 29 | transition: 0.3s; 30 | border: none; 31 | border-bottom: 2px solid var(--default-color); 32 | opacity: 0.3; 33 | 34 | ::placeholder { 35 | color: var(--default-color); 36 | } 37 | 38 | :hover, 39 | :focus { 40 | opacity: 1; 41 | outline: none; 42 | } 43 | ` 44 | 45 | const SearchIcon = styled.div<{ src: string }>` 46 | height: 2.9rem; 47 | width: 3.1rem; 48 | margin: auto 10px auto 0; 49 | 50 | background: var(--default-color); 51 | 52 | mask-size: cover; 53 | mask-image: url(${({ src }) => src}); 54 | ` 55 | 56 | export const Searchbar = () => { 57 | const searchSettings = Settings.Search.getWithFallback() 58 | const engine: string = searchSettings.engine || "duckduckgo.com/" 59 | 60 | let searchSymbol = undefined 61 | if (engine.includes("duckduckgo")) searchSymbol = duckduckgo 62 | else if (engine.includes("google")) searchSymbol = google 63 | else if (engine.includes("qwant")) searchSymbol = qwant 64 | else if (engine.includes("ecosia")) searchSymbol = ecosia 65 | 66 | const redirectToSearch = (query: string) => { 67 | if (searchSettings.fastForward[query]) 68 | window.location.href = searchSettings.fastForward[query] 69 | else { 70 | // for compatibility with old engine urls before fluidity 0.5.0 71 | if (!engine.includes(queryToken)) 72 | window.location.href = "https://" + engine + "?q=" + query 73 | else window.location.href = engine.replace(queryToken, query) 74 | } 75 | } 76 | 77 | return ( 78 | 79 | {searchSymbol && } 80 | e.which === 13 && redirectToSearch(e.currentTarget.value)} 84 | // eslint-disable-next-line jsx-a11y/no-autofocus 85 | autoFocus 86 | /> 87 | 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /src/Startpage/Settings/Changelog/Changelog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import styled from "@emotion/styled" 4 | 5 | import { changelog, ChangelogVersion } from "../../../data/changelog" 6 | import logo from "../../../data/pictures/logo.png" 7 | 8 | const ChangelogWrapper = styled.div` 9 | width: 100%; 10 | height: 100%; 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | overflow-y: auto; 15 | > h1 { 16 | font-weight: 500; 17 | } 18 | > img { 19 | width: 180px; 20 | height: 180px; 21 | } 22 | ` 23 | const StyledVersion = styled.div` 24 | width: 600px; 25 | > p { 26 | margin-bottom: 10px; 27 | } 28 | ` 29 | const ChangeItem = styled.li` 30 | white-space: nowrap; 31 | ` 32 | 33 | const Version = ({ version, description, changes }: ChangelogVersion) => { 34 | return ( 35 | 36 |

v{version}

37 | {description &&

{description}

} 38 | {changes &&

Changes:

} 39 | {changes?.map((change, index) => ( 40 | // eslint-disable-next-line react/no-array-index-key 41 | {change} 42 | ))} 43 |
44 | ) 45 | } 46 | 47 | export const Changelog = () => { 48 | return ( 49 | 50 | logo 51 |

Changelog

52 | {changelog.map((version: ChangelogVersion) => ( 53 | 54 | ))} 55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/Startpage/Settings/DesignSettings/DesignSettings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react" 2 | 3 | import styled from "@emotion/styled" 4 | import { faPlus, faMinus, faSave } from "@fortawesome/free-solid-svg-icons" 5 | 6 | import { ColorPicker } from "../../../components/ColorPicker" 7 | import { Dropdown } from "../../../components/Dropdown" 8 | import { OptionSlider } from "../../../components/OptionSlider" 9 | import { OptionTextInput } from "../../../components/OptionTextInput" 10 | import { Theme, colorsType, images } from "../../../data/data" 11 | import { 12 | StyledSettingsContent, 13 | SettingElement, 14 | SettingsButton, 15 | SettingsLabel, 16 | } from "../SettingsWindow" 17 | 18 | const DesignPreview = styled.div<{ name: string; colors: colorsType }>` 19 | ${({ colors }) => { 20 | return ( 21 | Object.keys(colors) 22 | .map((key: string) => key + `:` + colors[key]) 23 | .toString() 24 | .replaceAll(",", ";") + ";" 25 | ) 26 | }} 27 | 28 | background-color: var(--bg-color); 29 | display: flex; 30 | justify-content: space-evenly; 31 | align-items: center; 32 | border: 2px solid var(--accent-color); 33 | width: calc(100% - 400px); 34 | height: 100%; 35 | position: relative; 36 | ::before { 37 | content: "${({ name }) => name}"; 38 | color: var(--accent-color); 39 | position: absolute; 40 | top: 10px; 41 | left: 15px; 42 | font-size: 0.8rem; 43 | } 44 | ::after { 45 | content: "Design Preview"; 46 | color: var(--accent-color); 47 | position: absolute; 48 | top: 10px; 49 | right: 15px; 50 | font-size: 0.8rem; 51 | } 52 | @media screen and (max-width: 1400px) { 53 | > img { 54 | width: 200px; 55 | height: 200px; 56 | } 57 | > div > div { 58 | width: 50px; 59 | height: 200px; 60 | > h2 { 61 | font-size: 1rem; 62 | } 63 | > .wave { 64 | width: 50px; 65 | } 66 | } 67 | } 68 | @media screen and (max-width: 1200px) { 69 | > img { 70 | width: 150px; 71 | height: 150px; 72 | } 73 | > div > div { 74 | width: 1rem; 75 | margin-left: 0.5rem; 76 | height: 150px; 77 | > h2 { 78 | font-size: 0.8rem; 79 | } 80 | > .wave { 81 | display: none; 82 | } 83 | } 84 | } 85 | ` 86 | const ImagePreview = styled.img` 87 | margin: 10px; 88 | height: 300px; 89 | width: 300px; 90 | border: 1px solid var(--default-color); 91 | padding: 5px; 92 | object-fit: cover; 93 | 94 | animation: circling-shadow-small 4s ease 0s infinite normal; 95 | ` 96 | const StyledAccordionPreview = styled.div<{ colorVar: string }>` 97 | border: 4px solid ${({ colorVar }) => `var(${colorVar})`}; 98 | height: 300px; 99 | width: 80px; 100 | display: flex; 101 | align-items: center; 102 | justify-content: center; 103 | position: relative; 104 | ::before { 105 | content: ""; 106 | position: absolute; 107 | bottom: 0px; 108 | width: 100%; 109 | height: 100%; 110 | background-color: ${({ colorVar }) => `var(${colorVar})`}; 111 | } 112 | 113 | > .wave { 114 | width: 80px; 115 | height: 50px; 116 | position: absolute; 117 | top: 0px; 118 | overflow: hidden; 119 | ::before { 120 | content: ""; 121 | width: 180px; 122 | height: 185px; 123 | position: absolute; 124 | top: -25%; 125 | left: 50%; 126 | margin-left: -90px; 127 | margin-top: -140px; 128 | border-radius: 37%; 129 | background: var(--bg-color); 130 | animation: wave 12s infinite cubic-bezier(0.71, 0.33, 0.33, 0.68); 131 | } 132 | @keyframes wave { 133 | from { 134 | transform: rotate(0deg); 135 | } 136 | from { 137 | transform: rotate(360deg); 138 | } 139 | } 140 | } 141 | ` 142 | const SectionDivider = styled.div` 143 | width: calc(100% - 80px); 144 | padding: 20px 40px; 145 | position: relative; 146 | :before { 147 | content: ""; 148 | width: calc(100% - 80px); 149 | position: absolute; 150 | } 151 | ` 152 | const AccordionPreviewTitle = styled.h2` 153 | transform: rotate(90deg); 154 | min-width: max-content; 155 | color: var(--bg-color); 156 | transition: 0.5s; 157 | letter-spacing: 5px; 158 | ` 159 | const AccordionPreviewContainer = styled.div` 160 | display: flex; 161 | justify-content: center; 162 | align-items: center; 163 | margin: 10px; 164 | > * { 165 | margin-left: 30px; 166 | } 167 | ` 168 | 169 | export const SettingButtonRow = styled.div` 170 | display: flex; 171 | justify-content: space-between; 172 | ` 173 | 174 | const AccordionPreview = ({ 175 | title, 176 | colorVar, 177 | }: { 178 | title: string 179 | colorVar: string 180 | }) => { 181 | return ( 182 | 183 |
184 | {title} 185 | 186 | ) 187 | } 188 | 189 | interface props { 190 | design: Theme 191 | setDesign: (design: Theme) => void 192 | themes: Theme[] 193 | setThemes: (Themes: Theme[]) => void 194 | } 195 | 196 | const themeEquals = (theme1: Theme, theme2: Theme) => { 197 | let isEqual = true 198 | if (theme1.name !== theme2.name) isEqual = false 199 | if (theme1.image !== theme2.image) isEqual = false 200 | Object.keys(theme1.colors).forEach(key => { 201 | if (theme1.colors[key] !== theme2.colors[key]) isEqual = false 202 | }) 203 | return isEqual 204 | } 205 | 206 | export const DesignSettings = ({ 207 | design, 208 | setDesign, 209 | themes, 210 | setThemes, 211 | }: props) => { 212 | const [isNewDesign, setIsNewDesign] = useState(false) 213 | 214 | const setName = (name: string) => setDesign({ ...design, name: name }) 215 | const setColors = (colors: colorsType) => 216 | setDesign({ ...design, colors: colors }) 217 | const setImage = (image: string) => setDesign({ ...design, image: image }) 218 | 219 | // check if design does exist already 220 | useEffect(() => { 221 | const currTheme = themes.filter(theme => themeEquals(theme, design)) 222 | if (currTheme.length > 0) { 223 | setIsNewDesign(false) 224 | } else if (!isNewDesign) { 225 | setIsNewDesign(true) 226 | } 227 | }, [design, themes]) 228 | 229 | const themeChange = (themeName: string) => { 230 | const newTheme = themes.filter(theme => theme.name === themeName) 231 | if (newTheme.length > 0) { 232 | setDesign(newTheme[0]) 233 | } 234 | } 235 | 236 | const addTheme = (newTheme: Theme) => { 237 | setThemes([ 238 | ...themes.filter(theme => theme.name !== newTheme.name), 239 | newTheme, 240 | ]) 241 | } 242 | 243 | const removeTheme = (themeName: string) => { 244 | setThemes(themes.filter(theme => theme.name !== themeName)) 245 | if (themes.length > 0) themeChange(themes[0].name) 246 | } 247 | 248 | const themeExists = (themeName: string) => { 249 | return themes.filter(theme => theme.name === design.name).length > 0 250 | } 251 | 252 | return ( 253 | <> 254 |
255 | 256 | Theme 257 | 258 | 259 | {themes && ( 260 | ({ 263 | label: theme.name, 264 | value: theme.name, 265 | }))} 266 | onChange={themeChange} 267 | /> 268 | )} 269 | 270 | 271 | 276 | 277 | 278 | 279 | 280 | 281 | 286 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | addTheme(design)} 303 | text={!themeExists(design.name) ? "Add Theme" : "Save Theme"} 304 | icon={!themeExists(design.name) ? faPlus : faSave} 305 | disabled={!isNewDesign ? true : undefined} 306 | /> 307 | removeTheme(design.name)} 309 | text={"Remove Theme"} 310 | icon={faMinus} 311 | disabled={!themeExists(design.name)} 312 | /> 313 | 314 | 315 | 316 |
317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | ) 327 | } 328 | -------------------------------------------------------------------------------- /src/Startpage/Settings/LinkSettings/LinkSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import styled from "@emotion/styled" 4 | 5 | import { OptionTextArea } from "./OptionTextArea" 6 | import { linkGroup } from "../../../data/data" 7 | import { SettingsLabel } from "../SettingsWindow" 8 | 9 | interface props { 10 | linkGroups: linkGroup[] 11 | setLinkGroups: (value: linkGroup[]) => void 12 | } 13 | export const GeneralSettingsContent = styled.div` 14 | width: 100%; 15 | ` 16 | 17 | export const LinkSettings = ({ linkGroups, setLinkGroups }: props) => { 18 | return ( 19 | 20 | Links 21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/Startpage/Settings/LinkSettings/OptionTextArea.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | 3 | import styled from "@emotion/styled" 4 | 5 | import { linkGroup } from "../../../data/data" 6 | import * as Settings from "../settingsHandler" 7 | 8 | const StyledOptionTextArea = styled.div<{ error?: string }>` 9 | position: relative; 10 | border: 2px solid var(--default-color); 11 | display: flex; 12 | padding: 10px 0 10px 20px; 13 | height: calc(100% - 40px); 14 | ${({ error }) => 15 | error && 16 | ` 17 | ::after{ 18 | content: "${error}"; 19 | color: var(--accent-color); 20 | position: absolute; 21 | top: 10px; 22 | right: 15px; 23 | font-size: 0.8rem; 24 | } 25 | `} 26 | ` 27 | 28 | const StyledTextArea = styled.textarea` 29 | background-color: var(--bg-color); 30 | color: var(--default-color); 31 | border: none; 32 | height: 100%; 33 | width: 100%; 34 | outline: none; 35 | resize: none; 36 | ` 37 | 38 | const placeholder = JSON.stringify( 39 | [ 40 | { 41 | title: "Title", 42 | links: [ 43 | { 44 | label: "label", 45 | value: "url", 46 | }, 47 | { 48 | label: "label", 49 | value: "url", 50 | }, 51 | { 52 | label: "label", 53 | value: "url", 54 | }, 55 | ], 56 | }, 57 | ], 58 | null, 59 | 2 60 | ) 61 | 62 | interface props { 63 | initialValue: linkGroup[] 64 | onChange: (value: linkGroup[]) => void 65 | } 66 | 67 | const getLinksAsString = (): string => { 68 | // try to do usual parse 69 | try { 70 | const parseLinks = localStorage.getItem("link-groups") 71 | if (parseLinks) 72 | return JSON.stringify(Settings.Links.parse(parseLinks), null, 2) 73 | // eslint-disable-next-line no-empty 74 | } catch {} 75 | 76 | // try to parse broken json 77 | const links = Settings.Links.getRaw() 78 | if (links) { 79 | return links 80 | .replaceAll(":[{", ":[\n {\n") 81 | .replaceAll('[{"', '[\n {\n"') 82 | .replaceAll("}]}]", "}]\n }\n]") 83 | .replaceAll("]},{", "\n },\n {\n") 84 | .replaceAll("},{", "\n },\n {\n") 85 | .replaceAll('"}]', '"\n }\n ]') 86 | .replaceAll('"title":', ' "title":') 87 | .replaceAll('"links":', '\n "links":') 88 | .replaceAll('"label":', ' "label":') 89 | .replaceAll('"value":', '\n "value":') 90 | } 91 | 92 | //Last possible option 93 | return JSON.stringify(Settings.Links.getWithFallback(), null, 2) 94 | } 95 | 96 | export const OptionTextArea = ({ onChange }: props) => { 97 | const [error, setError] = useState(undefined) 98 | const [value, setValue] = useState(getLinksAsString()) 99 | 100 | const tryOnChangeEvent = (linkGroups: string) => { 101 | setValue(linkGroups) 102 | try { 103 | const parsedData = Settings.Links.parse(linkGroups) 104 | setError(undefined) 105 | onChange(parsedData) 106 | } catch { 107 | setError( 108 | "Your links are not parseable. Probably you have a Syntax Error?" 109 | ) 110 | } 111 | } 112 | 113 | return ( 114 | 115 | tryOnChangeEvent(e.currentTarget.value)} 117 | placeholder={placeholder} 118 | value={value} 119 | /> 120 | 121 | ) 122 | } 123 | -------------------------------------------------------------------------------- /src/Startpage/Settings/SearchSettings/FastForwardSearch.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | 3 | import styled from "@emotion/styled" 4 | import { faTrash, faPlus } from "@fortawesome/free-solid-svg-icons" 5 | 6 | import { IconButton } from "../../../components/IconButton" 7 | import { OptionTextInput } from "../../../components/OptionTextInput" 8 | import { FastForwards } from "../../../data/data" 9 | 10 | const FastForwardWrapper = styled.div` 11 | margin-bottom: 20px; 12 | display: flex; 13 | @media screen and (max-width: 1300px) { 14 | flex-direction: column; 15 | } 16 | ` 17 | const FastForwardTable = styled.table` 18 | width: 50%; 19 | padding: 0 20px; 20 | @media screen and (max-width: 1300px) { 21 | width: 100%; 22 | } 23 | ` 24 | const StyledFastForwardItem = styled.tr` 25 | > td { 26 | padding: 10px 0; 27 | overflow: hidden; 28 | white-space: nowrap; 29 | } 30 | > :first-of-type { 31 | max-width: 100px; 32 | } 33 | > :nth-of-type(3) { 34 | max-width: 300px; 35 | } 36 | > :last-of-type { 37 | width: 50px; 38 | } 39 | ` 40 | const AddItemButton = styled(IconButton)` 41 | font-size: 1rem; 42 | display: inline; 43 | ` 44 | const AddItemTextField = styled(OptionTextInput)` 45 | width: calc(100% - 50px); 46 | ` 47 | 48 | interface FastForwardItemProps { 49 | value: string 50 | url: string 51 | deleteThis: () => void 52 | } 53 | 54 | export const FastForwardItem = ({ 55 | value, 56 | url, 57 | deleteThis, 58 | }: FastForwardItemProps) => { 59 | return ( 60 | 61 | {`"${value}"`} 62 |  :  63 | {`"${url}"`} 64 | 65 | {" "} 66 | deleteThis()} /> 67 | 68 | 69 | ) 70 | } 71 | 72 | interface FastForwardAddItemProps { 73 | add: (value: string, url: string) => void 74 | } 75 | 76 | export const FastForwardAddItem = ({ add }: FastForwardAddItemProps) => { 77 | const [value, setValue] = useState("") 78 | const [url, setUrl] = useState("") 79 | 80 | return ( 81 | 82 | 83 | setValue(newValue)} 86 | placeholder={"search string"} 87 | /> 88 | 89 |  :  90 | 91 | setUrl(newUrl)} 94 | placeholder={"destination"} 95 | /> 96 | 97 | 98 | value && url && add(value, url)} 101 | icon={faPlus} 102 | /> 103 | 104 | 105 | ) 106 | } 107 | 108 | interface FastForwardSearchProps { 109 | links: FastForwards 110 | onChange: (links: FastForwards) => void 111 | } 112 | 113 | export const FastForwardSearch = ({ 114 | links, 115 | onChange, 116 | }: FastForwardSearchProps) => { 117 | const deleteValue = (value: string) => { 118 | const copy = { ...links } 119 | delete copy[value] 120 | onChange({ ...copy }) 121 | } 122 | const addValue = (value: string, url: string) => { 123 | const copy = { ...links } 124 | copy[value] = url 125 | onChange({ ...copy }) 126 | } 127 | 128 | const table = Object.keys(links) 129 | .sort() 130 | .map(value => ( 131 | deleteValue(value)} 136 | /> 137 | )) 138 | const tableLeft = [...table].splice(0, table.length / 2 + (table.length % 2)) 139 | const tableRight = [...table].splice(table.length / 2 + (table.length % 2)) 140 | 141 | return ( 142 |
143 | 144 | 145 | {tableLeft} 146 | 147 | 148 | {tableRight} 149 | 150 | 151 | 152 | {} 153 | 154 |
155 | ) 156 | } 157 | -------------------------------------------------------------------------------- /src/Startpage/Settings/SearchSettings/SearchSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import styled from "@emotion/styled" 4 | 5 | import { FastForwardSearch } from "./FastForwardSearch" 6 | import { OptionSlider } from "../../../components/OptionSlider" 7 | import { OptionTextInput } from "../../../components/OptionTextInput" 8 | import { searchEngines, Search } from "../../../data/data" 9 | import { queryToken } from "../../Searchbar/Searchbar" 10 | import { SettingElement, SettingsLabel } from "../SettingsWindow" 11 | 12 | interface props { 13 | searchSettings: Search 14 | setSearchSettings: (value: Search) => void 15 | } 16 | export const SearchSettingsContent = styled.div` 17 | width: 100%; 18 | overflow-y: auto; 19 | ` 20 | 21 | const Flex = styled.div` 22 | display: flex; 23 | align-items: center; 24 | padding-right: 40px; 25 | gap: 12px; 26 | ` 27 | 28 | const TextInput = styled(OptionTextInput)` 29 | width: 100%; 30 | height: 40px; 31 | padding-top: 0; 32 | padding-bottom: 0; 33 | ` 34 | 35 | export const SearchSettings = ({ 36 | searchSettings, 37 | setSearchSettings, 38 | }: props) => { 39 | const setEngine = (engine: string) => { 40 | setSearchSettings({ ...searchSettings, engine: engine }) 41 | } 42 | 43 | return ( 44 | 45 | Searchbar 46 | 47 | 48 | 53 | 58 | 59 | 60 |
61 | Fast Forward Search 62 | 65 | setSearchSettings({ ...searchSettings, fastForward: fastForward }) 66 | } 67 | /> 68 |
69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /src/Startpage/Settings/Settings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | 3 | import styled from "@emotion/styled" 4 | import { faSlidersH } from "@fortawesome/free-solid-svg-icons" 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 6 | 7 | import { SettingsWindow } from "./SettingsWindow" 8 | 9 | const SettingsPopupToggle = styled.button` 10 | position: fixed; 11 | top: 20px; 12 | right: 20px; 13 | font-size: 20px; 14 | 15 | color: var(--default-color); 16 | background-color: transparent; 17 | border: none; 18 | opacity: 0.3; 19 | 20 | cursor: pointer; 21 | transition: 0.3s; 22 | 23 | :hover { 24 | opacity: 0.5; 25 | color: var(--accent-color2); 26 | animation: box-flicker 0.01s ease 0s infinite alternate; 27 | } 28 | :focus { 29 | outline: none; 30 | } 31 | ` 32 | 33 | const PopupCover = styled.div` 34 | position: fixed; 35 | top: 0; 36 | right: 0; 37 | bottom: 0; 38 | left: 0; 39 | background-color: var(--bg-color); 40 | opacity: 0.7; 41 | ` 42 | 43 | export const Settings = () => { 44 | const [showSettings, setShowSettings] = useState(false) 45 | 46 | const hidePopup = () => setShowSettings(false) 47 | 48 | return ( 49 | <> 50 | setShowSettings(true)}> 51 | 52 | 53 | {showSettings && ( 54 | <> 55 | 56 | 57 | 58 | )} 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/Startpage/Settings/SettingsWindow.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | 3 | import styled from "@emotion/styled" 4 | import { 5 | faTimes, 6 | faTrash, 7 | faSave, 8 | faFire, 9 | } from "@fortawesome/free-solid-svg-icons" 10 | 11 | import { Changelog } from "./Changelog/Changelog" 12 | import { DesignSettings } from "./DesignSettings/DesignSettings" 13 | import { LinkSettings } from "./LinkSettings/LinkSettings" 14 | import { SearchSettings } from "./SearchSettings/SearchSettings" 15 | import * as Settings from "./settingsHandler" 16 | import { IconButton } from "../../components/IconButton" 17 | 18 | const StyledSettingsWindow = styled.div` 19 | background-color: var(--bg-color); 20 | position: absolute; 21 | 22 | top: var(--settings-window-gap); 23 | right: var(--settings-window-gap); 24 | bottom: var(--settings-window-gap); 25 | left: var(--settings-window-gap); 26 | 27 | border: 2px solid var(--default-color); 28 | padding: 60px 30px 30px 30px; 29 | box-shadow: 10px 10px 0px var(--accent-color); 30 | ` 31 | const WindowContent = styled.div` 32 | width: 100%; 33 | height: calc(100% - 80px); 34 | display: flex; 35 | ` 36 | 37 | const WindowHeader = styled.div` 38 | ::before { 39 | content: "Settings"; 40 | margin: 5px 20px 0 10px; 41 | } 42 | color: var(--bg-color); 43 | background-color: var(--default-color); 44 | width: 100%; 45 | height: 32px; 46 | position: absolute; 47 | left: 0; 48 | top: 0; 49 | display: flex; 50 | justify-content: space-between; 51 | ` 52 | 53 | const WindowFooter = styled.div` 54 | display: flex; 55 | justify-content: space-between; 56 | position: absolute; 57 | left: 30px; 58 | right: 30px; 59 | bottom: 30px; 60 | ` 61 | 62 | export const StyledSettingsContent = styled.div` 63 | background-color: var(--bg-color); 64 | width: 400px; 65 | height: 100%; 66 | margin-right: 30px; 67 | padding-right: 20px; 68 | overflow-y: auto; 69 | ` 70 | export const SettingsLabel = styled.p` 71 | font-size: 1rem; 72 | padding: 10px 0; 73 | ` 74 | 75 | export const SettingElement = styled.div` 76 | background-color: var(--bg-color); 77 | position: relative; 78 | padding: 10px 0px; 79 | + { 80 | margin-top: 15px; 81 | } 82 | ` 83 | 84 | const CloseButton = styled(IconButton)` 85 | z-index: 15; 86 | height: 30px; 87 | opacity: 1; 88 | padding: 0; 89 | ` 90 | 91 | export const SettingsButton = styled(IconButton)` 92 | background-color: var(--default-color); 93 | color: var(--bg-color); 94 | font-size: 1rem; 95 | padding: 10px 20px; 96 | :enabled:hover { 97 | animation: circling-shadow-small 2s ease 0s infinite normal; 98 | } 99 | ` 100 | 101 | const Tabbar = styled.div` 102 | width: 100%; 103 | display: flex; 104 | justify-content: center; 105 | ` 106 | 107 | const TabOption = styled.button<{ active: boolean }>` 108 | font-size: 1rem; 109 | font-weight: 500; 110 | transition: 0.3s; 111 | height: 100%; 112 | min-width: 150px; 113 | display: flex; 114 | justify-content: center; 115 | align-items: center; 116 | background-color: transparent; 117 | outline: none; 118 | border: none; 119 | cursor: ${({ active }) => (active ? "default" : "pointer")}; 120 | ${({ active }) => active && "text-shadow: var(--text-shadow-downwards)"}; 121 | :hover { 122 | text-shadow: var(--text-shadow-downwards); 123 | } 124 | ` 125 | 126 | const TabOptions = ["Links", "Appearance", "Searchbar", "Changelog"] 127 | 128 | interface props { 129 | hidePopup: () => void 130 | } 131 | 132 | export const SettingsWindow = ({ hidePopup }: props) => { 133 | const [currentTab, setCurrentTab] = useState(TabOptions[0]) 134 | const [design, setDesign] = useState(Settings.Design.getWithFallback()) 135 | const [themes, setThemes] = useState(Settings.Themes.getWithFallback()) 136 | const [linkGroups, setLinkGroups] = useState(Settings.Links.getWithFallback()) 137 | const [searchSettings, setSearchSettings] = useState( 138 | Settings.Search.getWithFallback() 139 | ) 140 | 141 | const applyValues = () => { 142 | Settings.Design.set(design) 143 | Settings.Themes.set(themes) 144 | Settings.Search.set(searchSettings) 145 | Settings.Links.set(linkGroups) 146 | window.location.reload() 147 | } 148 | 149 | return ( 150 | 151 | 152 | 153 | {TabOptions.map(option => ( 154 | setCurrentTab(option)} 158 | > 159 | {option} 160 | 161 | ))} 162 | 163 | hidePopup()} icon={faTimes} /> 164 | 165 | 166 | 167 | {currentTab === "Links" && ( 168 | 169 | )} 170 | 171 | {currentTab === "Appearance" && ( 172 | 178 | )} 179 | 180 | {currentTab === "Searchbar" && ( 181 | 185 | )} 186 | 187 | {currentTab === "Changelog" && } 188 | 189 | 190 | 191 | applyValues()} 193 | text={"Apply Changes"} 194 | icon={faSave} 195 | /> 196 | { 198 | window.location.reload() 199 | }} 200 | text={"Discard Changes"} 201 | icon={faFire} 202 | /> 203 | { 205 | localStorage.clear() 206 | window.location.reload() 207 | }} 208 | text={"Delete All Settings"} 209 | icon={faTrash} 210 | /> 211 | 212 | 213 | ) 214 | } 215 | -------------------------------------------------------------------------------- /src/Startpage/Settings/settingsHandler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | linkGroup, 3 | Theme, 4 | Search as SearchType, 5 | links, 6 | searchSettings, 7 | themes, 8 | } from "../../data/data" 9 | 10 | export const Search = { 11 | get: () => { 12 | const lsSearch = localStorage.getItem("search-settings") 13 | if (lsSearch) return Search.parse(lsSearch) 14 | return undefined 15 | }, 16 | getWithFallback: () => { 17 | try { 18 | return Search.get() ?? searchSettings 19 | } catch { 20 | console.error( 21 | "Your currently applied search settings appear to be corrupted." 22 | ) 23 | return searchSettings 24 | } 25 | }, 26 | 27 | set: (searchSettings: SearchType) => 28 | localStorage.setItem("search-settings", JSON.stringify(searchSettings)), 29 | 30 | parse: (searchSettings: string) => JSON.parse(searchSettings) as SearchType, 31 | } 32 | 33 | export const Themes = { 34 | get: () => { 35 | const lsThemes = localStorage.getItem("themes") 36 | if (lsThemes) return JSON.parse(lsThemes) as Theme[] 37 | return undefined 38 | }, 39 | getWithFallback: () => { 40 | try { 41 | return Themes.get() ?? themes 42 | } catch { 43 | console.error("Your currently applied themes appear to be corrupted.") 44 | return themes 45 | } 46 | }, 47 | 48 | set: (themes: Theme[]) => 49 | localStorage.setItem("themes", JSON.stringify(themes)), 50 | 51 | add: (theme: Theme) => { 52 | const lsThemes = Themes.get() 53 | if (lsThemes) Themes.set([...lsThemes, theme]) 54 | else Themes.set([theme]) 55 | }, 56 | 57 | remove: (name: string) => { 58 | const lsThemes = Themes.get() 59 | if (lsThemes) Themes.set(lsThemes.filter(theme => theme.name !== name)) 60 | }, 61 | 62 | parse: (theme: string) => JSON.parse(theme) as Theme, 63 | } 64 | 65 | const linkGroupsKey = "link-groups" 66 | export const Links = { 67 | getRaw: () => localStorage.getItem(linkGroupsKey), 68 | get: () => { 69 | const lsLinks = localStorage.getItem(linkGroupsKey) 70 | if (lsLinks) return Links.parse(lsLinks) 71 | return undefined 72 | }, 73 | getWithFallback: () => { 74 | try { 75 | return Links.get() ?? links 76 | } catch { 77 | console.error("Your currently applied links appear to be corrupted.") 78 | return links 79 | } 80 | }, 81 | 82 | set: (themes: linkGroup[]) => 83 | localStorage.setItem(linkGroupsKey, JSON.stringify(themes)), 84 | 85 | parse: (linkGroups: string) => JSON.parse(linkGroups) as linkGroup[], 86 | } 87 | 88 | export const Design = { 89 | get: () => { 90 | const lsDesign = localStorage.getItem("design") 91 | if (lsDesign) return Themes.parse(lsDesign) 92 | return undefined 93 | }, 94 | getWithFallback: () => { 95 | try { 96 | return Design.get() ?? themes[0] 97 | } catch { 98 | console.error("Your currently applied design appears to be corrupted.") 99 | return themes[0] 100 | } 101 | }, 102 | 103 | set: (design: Theme) => 104 | localStorage.setItem("design", JSON.stringify(design)), 105 | } 106 | -------------------------------------------------------------------------------- /src/Startpage/Startpage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | 3 | import styled from "@emotion/styled" 4 | 5 | import { LinkContainer } from "./LinkContainer/LinkContainer" 6 | import { Searchbar } from "./Searchbar/Searchbar" 7 | import { Settings } from "./Settings/Settings" 8 | import { Design as DesignSettings } from "./Settings/settingsHandler" 9 | import { images } from "../data/data" 10 | 11 | const Wrapper = styled.div` 12 | max-width: 1920px; 13 | height: 100%; 14 | margin: auto; 15 | position: relative; 16 | ` 17 | 18 | const StyledStartpage = styled.div` 19 | padding: 0px 100px; 20 | display: flex; 21 | flex-direction: row; 22 | justify-content: flex-start; 23 | align-items: center; 24 | height: calc(100% - 100px); 25 | ` 26 | 27 | const Image = styled.img` 28 | height: 400px; 29 | width: 400px; 30 | border: 2px solid var(--default-color); 31 | padding: 10px; 32 | object-fit: cover; 33 | 34 | animation: circling-shadow 4s ease 0s infinite normal; 35 | ` 36 | 37 | export const Startpage = () => { 38 | const [img, setImg] = useState(DesignSettings.getWithFallback().image) 39 | 40 | return ( 41 | 42 | 43 |
44 | setImg(images[0].value)} /> 45 |
46 | 47 |
48 | 49 | 50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/base/FiraCode-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/src/base/FiraCode-Light.woff -------------------------------------------------------------------------------- /src/base/FiraCode-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/src/base/FiraCode-Medium.woff -------------------------------------------------------------------------------- /src/base/FiraCode-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/src/base/FiraCode-Regular.woff -------------------------------------------------------------------------------- /src/base/FuturaLTLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/src/base/FuturaLTLight.woff -------------------------------------------------------------------------------- /src/base/animations.css: -------------------------------------------------------------------------------- 1 | @keyframes circling-shadow { 2 | 0%{ filter: 3 | drop-shadow(-10px -10px 0 var(--accent-color2)) 4 | drop-shadow(10px 10px 0 var(--accent-color)); 5 | } 6 | 25%{ filter: 7 | drop-shadow(-10px 10px 0 var(--accent-color2)) 8 | drop-shadow(10px -10px 0 var(--accent-color)); 9 | } 10 | 50%{ filter: 11 | drop-shadow(10px 10px 0 var(--accent-color)) 12 | drop-shadow(-10px -10px 0 var(--accent-color2)); 13 | } 14 | 75%{ filter: 15 | drop-shadow(10px -10px 0 var(--accent-color)) 16 | drop-shadow(-10px 10px 0 var(--accent-color2)); 17 | } 18 | 100%{ filter: 19 | drop-shadow(-10px -10px 0 var(--accent-color2)) 20 | drop-shadow(10px 10px 0 var(--accent-color)); 21 | } 22 | } 23 | @keyframes circling-shadow-small { 24 | 0%{ filter: 25 | drop-shadow(-5px -5px 0 var(--accent-color2)) 26 | drop-shadow(5px 5px 0 var(--accent-color)); 27 | } 28 | 25%{ filter: 29 | drop-shadow(-5px 5px 0 var(--accent-color2)) 30 | drop-shadow(5px -5px 0 var(--accent-color)); 31 | } 32 | 50%{ filter: 33 | drop-shadow(5px 5px 0 var(--accent-color)) 34 | drop-shadow(-5px -5px 0 var(--accent-color2)); 35 | } 36 | 75%{ filter: 37 | drop-shadow(5px -5px 0 var(--accent-color)) 38 | drop-shadow(-5px 5px 0 var(--accent-color2)); 39 | } 40 | 100%{ filter: 41 | drop-shadow(-5px -5px 0 var(--accent-color2)) 42 | drop-shadow(5px 5px 0 var(--accent-color)); 43 | } 44 | } 45 | 46 | @keyframes text-flicker { 47 | from { 48 | text-shadow: 1px 0 0 var(--accent-color), -2px 0 0 var(--accent-color2); 49 | } 50 | to { 51 | text-shadow: 2px 0.5px 2px var(--accent-color), -1px -0.5px 2px var(--accent-color2); 52 | } 53 | } 54 | 55 | @keyframes box-flicker { 56 | from { 57 | filter: drop-shadow(1px 0 0 var(--accent-color)) drop-shadow(-2px 0 0 var(--accent-color2)); 58 | } 59 | to { 60 | filter: drop-shadow(2px 0.5px 2px var(--accent-color)) drop-shadow( -1px -0.5px 2px var(--accent-color2)); 61 | } 62 | } 63 | 64 | @keyframes accordion-hover { 65 | 0% { 66 | clip: rect(9px, 9999px, 99px, 0); 67 | } 68 | 100% { 69 | clip: rect(9px, 9999px, 99px, 0); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/base/index.css: -------------------------------------------------------------------------------- 1 | @import "./variables.css"; 2 | @import "./animations.css"; 3 | 4 | /*********************** 5 | * Idk what this does. * 6 | * No touchies. * 7 | **********************/ 8 | body { 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | } 12 | 13 | /******************** 14 | * Fonts * 15 | ********************/ 16 | @font-face { 17 | font-family: 'Fira Code'; 18 | font-style: normal; 19 | font-weight: 300; 20 | src: local('Fira Code'), url('./FiraCode-Light.woff') format('woff'); 21 | } 22 | @font-face { 23 | font-family: 'Fira Code'; 24 | font-style: normal; 25 | font-weight: 500; 26 | src: local('Fira Code'), url('./FiraCode-Regular.woff') format('woff'); 27 | } 28 | body * { 29 | font-family: "Fira Code"; 30 | font-weight: 300; 31 | } 32 | 33 | /******************** 34 | * General * 35 | ********************/ 36 | 37 | html, body, #root { 38 | height:100%; 39 | width:100%; 40 | } 41 | body { 42 | margin: 0; 43 | background-color: var(--bg-color); 44 | color: var(--default-color); 45 | overflow: hidden; 46 | } 47 | p { 48 | margin:0px 49 | } 50 | a, a:visited, a:active { 51 | color: var(--default-color); 52 | text-decoration: none; 53 | } 54 | 55 | /******************** 56 | * Scrollbar * 57 | ********************/ 58 | ::-webkit-scrollbar { 59 | /* width */ 60 | width: 10px; 61 | } 62 | 63 | ::-webkit-scrollbar-thumb { 64 | /* Handle */ 65 | background: var(--default-color); 66 | } 67 | 68 | ::-webkit-scrollbar-thumb:hover { 69 | /* Handle on hover */ 70 | background: var(--default-color); 71 | } 72 | 73 | ::-webkit-scrollbar-track-piece { 74 | /* not handle on */ 75 | background: transparent; 76 | } 77 | -------------------------------------------------------------------------------- /src/base/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --bg-color: rgba(46,46,46); 3 | --default-color: rgba(230,230,230); 4 | --accent-color: rgb(255, 180, 230); 5 | --accent-color2: rgb(180, 255, 230); 6 | --border-radius: 5px; 7 | --settings-window-gap: 50px; 8 | --text-shadow-downwards: 1px 0px 0 var(--accent-color), 9 | -1px 0px 0 var(--accent-color), 10 | 0px -1px 0 var(--accent-color), 11 | 0px 1px 0 var(--accent-color), 12 | 0px 2px 0 var(--accent-color), 13 | 0px 3px 0 var(--accent-color), 14 | 0px 4px 0 var(--accent-color); 15 | } -------------------------------------------------------------------------------- /src/components/ColorPicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | 3 | import styled from "@emotion/styled" 4 | import { MaterialPicker, ColorResult } from "react-color" 5 | 6 | import { themes as defaultThemes, colorsType } from "../data/data" 7 | 8 | const ColorPickerContainer = styled.div` 9 | display: flex; 10 | > div { 11 | padding: 0 10px; 12 | width: 180px; 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | } 18 | ` 19 | 20 | const ColorOption = styled.div<{ active: boolean }>` 21 | width: 100%; 22 | padding: 5px 0; 23 | cursor: pointer; 24 | opacity: ${({ active }) => !active && "0.7"}; 25 | color: ${({ active }) => active && "var(--accent-color)"}; 26 | :hover { 27 | color: var(--accent-color2); 28 | animation: text-flicker 0.01s ease 0s infinite alternate; 29 | } 30 | ` 31 | 32 | const StyledMaterialPicker = styled.div` 33 | > div * { 34 | background-color: var(--bg-color) !important; 35 | color: var(--default-color) !important; 36 | box-shadow: none; 37 | } 38 | > div { 39 | border: 2px solid var(--default-color); 40 | } 41 | ` 42 | interface props { 43 | colors: colorsType 44 | setColors: (value: colorsType) => void 45 | } 46 | 47 | export const ColorPicker = ({ colors, setColors }: props) => { 48 | const [currentColor, setCurrentColor] = useState( 49 | Object.keys(defaultThemes[0].colors)[0] 50 | ) 51 | 52 | const handleChange = (result: ColorResult) => { 53 | const tmp = { ...colors } 54 | tmp[currentColor] = result.hex 55 | setColors(tmp) 56 | } 57 | 58 | return ( 59 | 60 |
61 | {Object.keys(colors).map(key => ( 62 | setCurrentColor(key)} 66 | > 67 | {key} 68 | 69 | ))} 70 |
71 | 72 | color && handleChange(color)} 75 | /> 76 | 77 |
78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /src/components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | 3 | import styled from "@emotion/styled" 4 | import { faAngleDown } from "@fortawesome/free-solid-svg-icons" 5 | 6 | import { IconButton } from "./IconButton" 7 | 8 | const DropdownWrapper = styled.div` 9 | position: relative; 10 | height: 40px; 11 | ` 12 | 13 | const DropdownButton = styled(IconButton)` 14 | position: absolute; 15 | left: 0; 16 | right: 0; 17 | top: 0; 18 | height: 40px; 19 | width: calc(100% + 4px); 20 | display: flex; 21 | flex-direction: row; 22 | align-items: space-between; 23 | justify-content: space-between; 24 | padding: 10px 20px; 25 | border: 2px solid var(--default-color); 26 | background-color: var(--bg-color); 27 | 28 | :enabled:hover, 29 | :focus, 30 | :hover { 31 | animation: none; 32 | opacity: 1; 33 | } 34 | font-size: initial; 35 | z-index: 10; 36 | ` 37 | 38 | const DropdownPopup = styled.div<{ height: number; items: number }>` 39 | height: ${({ height }) => `${height}px`}; 40 | position: absolute; 41 | left: 4px; 42 | top: 40px; 43 | width: calc(100% - 3px); 44 | background-color: var(--bg-color); 45 | overflow: hidden; 46 | z-index: 9; 47 | animation: box-flicker 0.01s ease 0s infinite alternate; 48 | transition: ${({ items }) => `${items * 0.1}s`}; 49 | > div { 50 | padding-top: 5px; 51 | display: flex; 52 | flex-direction: column; 53 | } 54 | ` 55 | 56 | const DropdownItem = styled(IconButton)` 57 | margin: 0; 58 | padding: 10px 20px; 59 | justify-content: flex-start; 60 | font-size: initial; 61 | :enabled:hover { 62 | animation: none; 63 | opacity: 1; 64 | background-color: var(--default-color); 65 | color: var(--bg-color); 66 | } 67 | ` 68 | 69 | interface props { 70 | items: { label: string; value: string }[] 71 | onChange: (value: string) => void 72 | value: string 73 | } 74 | 75 | export const Dropdown = ({ items, onChange, value }: props) => { 76 | const [popupHeight, setPopupHeight] = useState(0) 77 | const [hasBlur, setHasBlur] = useState(false) 78 | const getCurrentLabel = () => { 79 | const current = items.filter(item => item.value === value) 80 | if (current.length > 0) return current[0].label 81 | else return value 82 | } 83 | 84 | const handleChange = (value: string) => { 85 | onChange(value) 86 | setHasBlur(false) 87 | } 88 | 89 | return ( 90 | 91 | setHasBlur(!hasBlur)} 95 | > 96 | 97 |
setHasBlur(false)} 99 | ref={elem => setPopupHeight(elem?.clientHeight || 0)} 100 | > 101 | {items.map(item => ( 102 | handleChange(item.value)} 104 | key={item.value} 105 | text={item.label} 106 | /> 107 | ))} 108 |
109 |
110 |
111 | ) 112 | } 113 | -------------------------------------------------------------------------------- /src/components/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import styled from "@emotion/styled" 4 | import { 5 | FontAwesomeIcon, 6 | FontAwesomeIconProps, 7 | } from "@fortawesome/react-fontawesome" 8 | 9 | const StyledIconButton = styled.button<{ inverted?: boolean }>` 10 | color: ${({ inverted }) => 11 | inverted ? "var(--bg-color)" : "var(--default-color)"}; 12 | background-color: transparent; 13 | min-width: 50px; 14 | font-size: 20px; 15 | border: none; 16 | opacity: 0.7; 17 | cursor: pointer; 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | 22 | :enabled:hover { 23 | ${({ inverted }) => 24 | inverted 25 | ? `filter: 26 | drop-shadow(2px 2px 0 var(--accent-color)) 27 | drop-shadow(-2px -2px 0 var(--accent-color)) 28 | drop-shadow(-2px 2px 0 var(--accent-color)) 29 | drop-shadow(2px -2px 0 var(--accent-color))` 30 | : "animation: box-flicker 0.01s ease 0s infinite alternate"}; 31 | } 32 | :focus { 33 | outline: none; 34 | } 35 | :disabled { 36 | opacity: 0.2; 37 | cursor: default; 38 | } 39 | 40 | > span { 41 | padding-right: 10px; 42 | } 43 | ` 44 | type props = Partial> & 45 | React.ButtonHTMLAttributes & { 46 | text?: string 47 | inverted?: boolean 48 | } 49 | 50 | export const IconButton = ({ icon, text, children, ...props }: props) => { 51 | return ( 52 | 53 | {children} 54 | {text && {text}} 55 | {icon && } 56 | 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/components/OptionSlider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react" 2 | 3 | import styled from "@emotion/styled" 4 | import { faAngleLeft, faAngleRight } from "@fortawesome/free-solid-svg-icons" 5 | 6 | import { IconButton } from "./IconButton" 7 | 8 | const SliderWrapper = styled.div` 9 | height: 20px; 10 | display: flex; 11 | flex-direction: row; 12 | padding: 5px 0; 13 | > span { 14 | min-width: 100px; 15 | display: flex; 16 | justify-content: center; 17 | } 18 | ` 19 | 20 | interface props { 21 | values: { label: string; value: string }[] 22 | onChange: (value: string) => void 23 | currentValue: string 24 | } 25 | 26 | export const OptionSlider = ({ values, onChange, currentValue }: props) => { 27 | const [index, setIndex] = useState(0) 28 | useEffect(() => { 29 | values.forEach((val, i) => { 30 | currentValue === val.value && i !== index && setIndex(i) 31 | }) 32 | }, [currentValue, values, index]) 33 | 34 | const handleChange = (newIndex: number) => { 35 | setIndex(newIndex) 36 | onChange(values[newIndex]?.value) 37 | } 38 | 39 | return ( 40 | 41 | { 44 | handleChange(index - 1) 45 | }} 46 | icon={faAngleLeft} 47 | /> 48 | {values[index]?.label} 49 | 50 | = values.length - 1} 52 | onClick={() => handleChange(index + 1)} 53 | icon={faAngleRight} 54 | /> 55 | 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/components/OptionTextInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import styled from "@emotion/styled" 4 | 5 | const StyledInput = styled.input` 6 | border: 2px solid var(--default-color); 7 | width: calc(100% - 40px); 8 | height: 36px; 9 | padding: 0 20px; 10 | background-color: var(--bg-color); 11 | color: var(--default-color); 12 | outline: none; 13 | opacity: 0.5; 14 | :enabled:hover, 15 | :focus { 16 | opacity: 1; 17 | } 18 | ` 19 | 20 | type props = Omit, "onChange"> & { 21 | value: string 22 | onChange: (value: string) => void 23 | className?: string 24 | } 25 | 26 | export const OptionTextInput = ({ onChange, ...props }: props) => { 27 | return ( 28 | onChange(e.currentTarget.value)} 31 | {...props} 32 | /> 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/data/changelog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import styled from "@emotion/styled" 4 | 5 | const Link = styled.a` 6 | &, 7 | :visited { 8 | color: var(--accent-color); 9 | } 10 | :hover { 11 | text-decoration: underline; 12 | } 13 | ` 14 | 15 | const RedditUser = ({ user }: { user: string }) => ( 16 | u/{user} 17 | ) 18 | const GithubUser = ({ user }: { user: string }) => ( 19 | {user} 20 | ) 21 | 22 | export interface ChangelogVersion { 23 | version: string 24 | description?: string 25 | changes?: (string | JSX.Element)[] 26 | } 27 | 28 | export const changelog: ChangelogVersion[] = [ 29 | { 30 | version: "0.6.0", 31 | changes: [ 32 | <> 33 | Added catppuccin theme. Thanks to for 34 | contributing! 35 | , 36 | ], 37 | }, 38 | { 39 | version: "0.5.0", 40 | changes: [ 41 | "Added custom search engines", 42 | <> 43 | Added some new themes. Thanks to{" "} 44 | for contributing! 45 | , 46 | ], 47 | }, 48 | { 49 | version: "0.4.4", 50 | changes: [ 51 | <> 52 | Added new theme "Tartarus". Thanks to{" "} 53 | for contributing!
( 54 | 55 | fitting linux rice 56 | 57 | ) 58 | , 59 | ], 60 | }, 61 | { 62 | version: "0.4.3", 63 | changes: [ 64 | "Added middle mouse click to Link Group to open all links in new tabs", 65 | "Added Dockerfile for easier local setup", 66 | ], 67 | }, 68 | { 69 | version: "0.4.2", 70 | changes: ["Enhanced responsiveness for large screens", "Internal stuff"], 71 | }, 72 | { 73 | version: "0.4.1", 74 | changes: [ 75 | "Enhanced stability of the settings (I am pretty sure about it this time!!!)", 76 | "Fixed a bug with the link editor I introduced before", 77 | ], 78 | }, 79 | { 80 | version: "0.4.0", 81 | changes: [ 82 | "Added fast forward search", 83 | "Fixed a bug which prevented the link editor to load your data", 84 | "Enhanced responsiveness", 85 | "Added some more default data", 86 | ], 87 | }, 88 | { 89 | version: "0.3.0", 90 | description: 91 | "This update was hell for me, fucking themes took me way too long and I needed to restructure all the internal design data.Also oof, had so many bugs caused by the not existing peresistence of my data. Hope you enjoy it!", 92 | changes: ["Added theme management"], 93 | }, 94 | { 95 | version: "0.2.1", 96 | changes: ["Optimized keyboard control", "Restructured settings"], 97 | }, 98 | { 99 | version: "0.2.0", 100 | changes: [ 101 | "Added this changelog", 102 | "Added tabs in settings", 103 | "Added design preview", 104 | 'Added "Discard Changes" button in settings', 105 | "Added project logo", 106 | "Changed structure of settings", 107 | "I think I enhanced stability overall a bit", 108 | ], 109 | }, 110 | { 111 | version: "0.1.0", 112 | description: "The initial state of this project.", 113 | }, 114 | ] 115 | -------------------------------------------------------------------------------- /src/data/data.ts: -------------------------------------------------------------------------------- 1 | import pic_1 from "./pictures/pic_1.jpg" 2 | import pic_2 from "./pictures/pic_2.jpg" 3 | import pic_3 from "./pictures/pic_3.jpg" 4 | import pic_4 from "./pictures/pic_4.jpg" 5 | import pic_5 from "./pictures/pic_5.jpg" 6 | import pic_6 from "./pictures/pic_6.jpg" 7 | import pic_7 from "./pictures/pic_7.jpg" 8 | import pic_8 from "./pictures/pic_8.png" 9 | import { queryToken } from "../Startpage/Searchbar/Searchbar" 10 | 11 | export interface dataElem { 12 | label: string 13 | value: string 14 | } 15 | 16 | export interface linkGroup { 17 | title: string 18 | links: dataElem[] 19 | } 20 | 21 | /* eslint-disable */ 22 | /* 23 | ──────▄▌▐▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀​▀▀▀▀▀▀▌ 24 | ───▄▄██▌█ BEEP BEEP 25 | ▄▄▄▌▐██▌█ GAY PORN DELIVERY 26 | ███████▌█▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄​▄▄▄▄▄▄▌ 27 | ▀(@)▀▀▀▀▀▀▀(@)(@)▀▀▀▀▀▀▀▀▀▀▀▀▀​▀▀▀▀(@)▀ 28 | */ 29 | /* eslint-enable */ 30 | 31 | export const links: linkGroup[] = [ 32 | { 33 | title: "Reddit", 34 | links: [ 35 | { 36 | label: "r/startpages", 37 | value: "https://www.reddit.com/r/startpages/", 38 | }, 39 | { 40 | label: "r/unixporn", 41 | value: "https://www.reddit.com/r/unixporn/", 42 | }, 43 | { 44 | label: "r/rainmeter", 45 | value: "https://www.reddit.com/r/rainmeter/", 46 | }, 47 | { 48 | label: "r/AnimalsBeingDerps", 49 | value: "https://www.reddit.com/r/AnimalsBeingDerps/", 50 | }, 51 | ], 52 | }, 53 | { 54 | title: "3D Modelling", 55 | links: [ 56 | { 57 | label: "Blender", 58 | value: "https://www.blender.org/", 59 | }, 60 | { 61 | label: "BlenderGuru", 62 | value: "https://www.blenderguru.com/", 63 | }, 64 | { 65 | label: "Poliigon", 66 | value: "https://www.poliigon.com/", 67 | }, 68 | { 69 | label: "Blender tutorial", 70 | value: 71 | "https://www.youtube.com/watch?v=NyJWoyVx_XI&list=PLjEaoINr3zgEq0u2MzVgAaHEBt--xLB6U", 72 | }, 73 | { 74 | label: "The other Blender tutorial", 75 | value: 76 | "https://www.youtube.com/watch?v=bpvh-9H8S1g&list=PL8eKBkZzqDiU-qcoaghCz04sMitC1yx6k&index=1", 77 | }, 78 | ], 79 | }, 80 | { 81 | title: "Design", 82 | links: [ 83 | { 84 | label: "PixlrX", 85 | value: "https://pixlr.com/x/", 86 | }, 87 | { 88 | label: "AI Image Enlarger", 89 | value: "https://bigjpg.com/en", 90 | }, 91 | { 92 | label: "Img to Svg Converter", 93 | value: "https://picsvg.com/", 94 | }, 95 | { 96 | label: "Affinity", 97 | value: "https://affinity.serif.com/en-us/tutorials/designer/desktop/", 98 | }, 99 | { 100 | label: "Affinity - YT", 101 | value: "https://www.youtube.com/c/AffinityRevolution/playlists", 102 | }, 103 | ], 104 | }, 105 | { 106 | title: "Music", 107 | links: [ 108 | { 109 | label: "i wanna be a cowboy", 110 | value: "https://www.youtube.com/watch?v=8zWz92f_HGs", 111 | }, 112 | { 113 | label: "let the bodies hit the floor", 114 | value: "https://www.youtube.com/watch?v=b--VKaCB9u0", 115 | }, 116 | { 117 | label: "Nobody Kanna Cross It", 118 | value: "https://www.youtube.com/watch?v=2wqTnwJGvtc", 119 | }, 120 | { 121 | label: "Smug Dancin", 122 | value: "https://www.youtube.com/watch?v=eNZ9Od1jQ4Q", 123 | }, 124 | { 125 | label: "Utamaru - The Sanctified Mind Cover", 126 | value: "https://www.youtube.com/watch?v=MHlJKLlS07U", 127 | }, 128 | ], 129 | }, 130 | { 131 | title: "Sauce", 132 | links: [ 133 | { 134 | label: "Pictures - DeathAndMilk", 135 | value: "https://www.instagram.com/deathandmilk_/", 136 | }, 137 | { 138 | label: "Icons - FontAwesome", 139 | value: "https://fontawesome.com/icons", 140 | }, 141 | { 142 | label: "Text Flicker - CodeMyUI", 143 | value: 144 | "https://codemyui.com/crt-screen-text-flicker-animation-in-pure-css/", 145 | }, 146 | { 147 | label: "Wave Animation - mburakerman", 148 | value: "https://codepen.io/mburakerman/pen/eRZZEv", 149 | }, 150 | { 151 | label: "Da real sauce ԅ(♡﹃♡ԅ)", 152 | value: "https://www.youtube.com/watch?v=qr89xoZyE1g", 153 | }, 154 | { 155 | label: "Even more real sauce ( ͡° ͜ʖ ͡°)", 156 | value: "https://www.youtube.com/watch?v=VLhJOd_TFiI", 157 | }, 158 | ], 159 | }, 160 | ] 161 | 162 | export const images: dataElem[] = [ 163 | { label: "pic_1", value: pic_1 }, 164 | { label: "pic_2", value: pic_2 }, 165 | { label: "pic_3", value: pic_3 }, 166 | { label: "pic_4", value: pic_4 }, 167 | { label: "pic_5", value: pic_5 }, 168 | { label: "pic_6", value: pic_6 }, 169 | { label: "pic_7", value: pic_7 }, 170 | { label: "pic_8", value: pic_8 }, 171 | ] 172 | 173 | export const searchEngines: dataElem[] = [ 174 | { 175 | label: "DuckDuckGo", 176 | value: `https://duckduckgo.com/?q=${queryToken}`, 177 | }, 178 | { 179 | label: "Google", 180 | value: `https://www.google.com/search?q=${queryToken}`, 181 | }, 182 | { 183 | label: "Qwant", 184 | value: `https://qwant.com/?q=${queryToken}`, 185 | }, 186 | { 187 | label: "Ecosia", 188 | value: `https://ecosia.org/search/?q=${queryToken}`, 189 | }, 190 | ] 191 | 192 | export type FastForwards = Record 193 | 194 | export interface Search { 195 | engine: string 196 | fastForward: FastForwards 197 | } 198 | 199 | export const searchSettings: Search = { 200 | engine: searchEngines[0].value, 201 | fastForward: { 202 | deepl: "https://deepl.com/", 203 | maps: "https://google.de/maps/", 204 | reddit: "https://reddit.com/", 205 | github: "https://github.com/", 206 | gitlab: "https://gitlab.com/", 207 | youtube: "https://youtube.com/", 208 | }, 209 | } 210 | 211 | export interface colorsType { 212 | [key: string]: string 213 | "--bg-color": string 214 | "--default-color": string 215 | "--accent-color": string 216 | "--accent-color2": string 217 | } 218 | 219 | export interface Theme { 220 | name: string 221 | colors: colorsType 222 | image: string 223 | } 224 | 225 | export const themes: Theme[] = [ 226 | { 227 | name: "Catppuccin", 228 | image: 229 | "https://raw.githubusercontent.com/catppuccin/catppuccin/main/assets/logos/exports/1544x1544_circle.png", 230 | colors: { 231 | "--bg-color": "#24273A", 232 | "--default-color": "#CAD3F5", 233 | "--accent-color": "#C6A0F6", 234 | "--accent-color2": "#8AADF4", 235 | }, 236 | }, 237 | { 238 | name: "DeathAndMilk", 239 | image: pic_1, 240 | colors: { 241 | "--bg-color": "#2E2E2E", 242 | "--default-color": "#E6E6E6", 243 | "--accent-color": "#FFB4E6", 244 | "--accent-color2": "#B4FFE6", 245 | }, 246 | }, 247 | { 248 | name: "Pop!OS", 249 | image: 250 | "https://oswallpapers.com/wp-content/uploads/2019/04/kate-hazen-unleash-your-robot.jpg", 251 | colors: { 252 | "--bg-color": "#333136", 253 | "--default-color": "#2BC5EB", 254 | "--accent-color": "#FCD307", 255 | "--accent-color2": "#2BC5EB", 256 | }, 257 | }, 258 | { 259 | name: "Dark Souls", 260 | image: 261 | "https://i.pinimg.com/originals/16/74/db/1674dbae45cd38f3d3b4c00dc8616bd7.gif", 262 | colors: { 263 | "--bg-color": "#32323C", 264 | "--default-color": "#A0A08C", 265 | "--accent-color": "#9A6650", 266 | "--accent-color2": "#461E28", 267 | }, 268 | }, 269 | { 270 | name: "S.E.Lain", 271 | image: 272 | "https://64.media.tumblr.com/54a945edd2641e20859d6f6537cd7423/tumblr_pwa4bogz4N1qze3hdo2_r1_500.gifv", 273 | colors: { 274 | "--bg-color": "#0a1a25", 275 | "--default-color": "#a6b7ab", 276 | "--accent-color": "#94656b", 277 | "--accent-color2": "#57182e", 278 | }, 279 | }, 280 | { 281 | name: "Kitties", 282 | image: 283 | "https://64.media.tumblr.com/5a232d5c0999d02388d78e5c1025f94f/0572516693bf4014-3d/s500x750/0306dc89b657093529aa3ce96e64b9c43572e901.gifv", 284 | colors: { 285 | "--bg-color": "#495662", 286 | "--default-color": "#d1f1fa", 287 | "--accent-color": "#80aad4", 288 | "--accent-color2": "#e8a9b7", 289 | }, 290 | }, 291 | { 292 | name: "pretty chill", 293 | image: 294 | "https://e4p7c9i3.stackpathcdn.com/wp-content/uploads/2019/05/tumblr_oymsnbT0111vjxiz1o1_1280.gif?iv=165", 295 | colors: { 296 | "--bg-color": "#397d76", 297 | "--default-color": "#f1daba", 298 | "--accent-color": "#c5bdb5", 299 | "--accent-color2": "#93a662", 300 | }, 301 | }, 302 | { 303 | name: "Tartarus", 304 | image: 305 | "https://64.media.tumblr.com/8de9e4d31a132f7617ecc05e6a0f8807/tumblr_nd048m6QFH1tqptlzo1_500.gifv", 306 | colors: { 307 | "--bg-color": "#282828", 308 | "--default-color": "#D4BE98", 309 | "--accent-color": "#7DAEA3", 310 | "--accent-color2": "#A9B665", 311 | }, 312 | }, 313 | { 314 | name: "Pastel Aesthetic", 315 | image: "https://i.imgur.com/bZHurZn.jpeg", 316 | colors: { 317 | "--bg-color": "#2E2E2E", 318 | "--default-color": "#F3C9CB", 319 | "--accent-color": "#6D79BF", 320 | "--accent-color2": "#FBECEF", 321 | }, 322 | }, 323 | { 324 | name: "Bohemian", 325 | image: "https://i.imgur.com/gcZ6fmk.jpeg", 326 | colors: { 327 | "--bg-color": "#2E2E2E", 328 | "--default-color": "#D6B29A", 329 | "--accent-color": "#B35000", 330 | "--accent-color2": "#FBECEF", 331 | }, 332 | }, 333 | { 334 | name: "Modern Boho", 335 | image: "https://i.imgur.com/HkEcwGl.jpeg", 336 | colors: { 337 | "--bg-color": "#2E2E2E", 338 | "--default-color": "#C66B3C", 339 | "--accent-color": "#F6BC7C", 340 | "--accent-color2": "#54573C", 341 | }, 342 | }, 343 | { 344 | name: "Gruvbox Inspired Green", 345 | image: "https://i.imgur.com/ISjs7cg.jpeg", 346 | colors: { 347 | "--bg-color": "#2E2E2E", 348 | "--default-color": "#CC9A52", 349 | "--accent-color": "#647D44", 350 | "--accent-color2": "#FCE4B4", 351 | }, 352 | }, 353 | { 354 | name: "Beach", 355 | image: "https://i.imgur.com/gcW1jul.jpeg", 356 | colors: { 357 | "--bg-color": "#2E2E2E", 358 | "--default-color": "#E3C9BC", 359 | "--accent-color": "#91C6CC", 360 | "--accent-color2": "#F0F8FA", 361 | }, 362 | }, 363 | ] 364 | -------------------------------------------------------------------------------- /src/data/pictures/duckduckgo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.10, written by Peter Selinger 2001-2011 9 | 10 | 12 | 58 | 59 | 61 | 64 | 67 | 71 | 75 | 79 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/data/pictures/ecosia.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/data/pictures/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.10, written by Peter Selinger 2001-2011 9 | 10 | 12 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/data/pictures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/src/data/pictures/logo.png -------------------------------------------------------------------------------- /src/data/pictures/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/data/pictures/pic_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/src/data/pictures/pic_1.jpg -------------------------------------------------------------------------------- /src/data/pictures/pic_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/src/data/pictures/pic_2.jpg -------------------------------------------------------------------------------- /src/data/pictures/pic_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/src/data/pictures/pic_3.jpg -------------------------------------------------------------------------------- /src/data/pictures/pic_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/src/data/pictures/pic_4.jpg -------------------------------------------------------------------------------- /src/data/pictures/pic_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/src/data/pictures/pic_5.jpg -------------------------------------------------------------------------------- /src/data/pictures/pic_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/src/data/pictures/pic_6.jpg -------------------------------------------------------------------------------- /src/data/pictures/pic_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/src/data/pictures/pic_7.jpg -------------------------------------------------------------------------------- /src/data/pictures/pic_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrettyCoffee/fluidity/72e8eaf5e969b913dcc8ae27307785e66c0ce250/src/data/pictures/pic_8.png -------------------------------------------------------------------------------- /src/data/pictures/qwant.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.10, written by Peter Selinger 2001-2011 9 | 10 | 12 | 67 | 69 | 71 | 73 | 75 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/data/pictures/sauce.txt: -------------------------------------------------------------------------------- 1 | https://www.instagram.com/deathandmilk_/ -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from "react" 2 | 3 | import { createRoot } from "react-dom/client" 4 | 5 | import App from "./App" 6 | import "./base/index.css" 7 | 8 | const root = document.getElementById("root") 9 | 10 | if (!root) throw new Error("Missing root node") 11 | 12 | createRoot(root).render( 13 | 14 | 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ "src", "vite.config.ts" ] 23 | } 24 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import react from "@vitejs/plugin-react" 3 | import { defineConfig } from "vite" 4 | import { checker } from "vite-plugin-checker" 5 | 6 | export default defineConfig(() => { 7 | return { 8 | base: "./", 9 | build: { 10 | outDir: "build", 11 | }, 12 | plugins: [ 13 | react(), 14 | checker({ 15 | typescript: true, 16 | eslint: { 17 | lintCommand: "eslint ./src", 18 | dev: { 19 | logLevel: ["error"], 20 | }, 21 | }, 22 | }), 23 | ], 24 | } 25 | }) 26 | --------------------------------------------------------------------------------