├── .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 | 
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 | 
16 | 
17 | 
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 | You need to enable JavaScript to run this app.
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 |
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 |
--------------------------------------------------------------------------------