Initial Commit
This commit is contained in:
commit
221decc8bc
33
.eslintrc.json
Normal file
33
.eslintrc.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-hooks/recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 12,
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": ["react", "react-hooks"],
|
||||||
|
"rules": {
|
||||||
|
"no-console": "off",
|
||||||
|
"semi": ["error", "always"],
|
||||||
|
"quotes": ["error", "double"],
|
||||||
|
"no-unused-vars": "warn",
|
||||||
|
"react/prop-types": "off",
|
||||||
|
"react/react-in-jsx-scope": "off"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
BIN
logo.afdesign
Normal file
BIN
logo.afdesign
Normal file
Binary file not shown.
21330
package-lock.json
generated
Normal file
21330
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
60
package.json
Normal file
60
package.json
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"name": "personal-site",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"homepage": "https://tombutcher.work",
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.6.1",
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/react": "^16.2.0",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
"@tsparticles/react": "^3.0.0",
|
||||||
|
"@tsparticles/slim": "^3.8.1",
|
||||||
|
"antd": "^5.24.2",
|
||||||
|
"framer-motion": "^12.4.7",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
|
"react-particles": "^2.12.2",
|
||||||
|
"react-responsive": "^10.0.0",
|
||||||
|
"react-router-dom": "^7.2.0",
|
||||||
|
"react-scripts": "^5.0.1",
|
||||||
|
"react-tsparticles": "^2.12.2",
|
||||||
|
"react-turnstile": "^1.1.4",
|
||||||
|
"sass": "^1.85.1",
|
||||||
|
"web-vitals": "^4.2.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject",
|
||||||
|
"predeploy": "npm run build",
|
||||||
|
"deploy": "gh-pages -d build"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-config-react-app": "^7.0.1",
|
||||||
|
"eslint-plugin-react": "^7.37.4",
|
||||||
|
"gh-pages": "^6.3.0",
|
||||||
|
"globals": "^16.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
131
public/fonts.css
Normal file
131
public/fonts.css
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "SF-Pro-Rounded";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 900;
|
||||||
|
src:
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Black-Subset.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Black-Subset.woff")
|
||||||
|
format("woff"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Black-Subset.otf")
|
||||||
|
format("opentype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "SF-Pro-Rounded";
|
||||||
|
font-style: normal;
|
||||||
|
|
||||||
|
font-weight: 700;
|
||||||
|
src:
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Bold-Subset.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Bold-Subset.woff")
|
||||||
|
format("woff"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Bold-Subset.otf")
|
||||||
|
format("opentype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "SF-Pro-Rounded";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 800;
|
||||||
|
src:
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Heavy-Subset.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Heavy-Subset.woff")
|
||||||
|
format("woff"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Heavy-Subset.otf")
|
||||||
|
format("opentype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "SF-Pro-Rounded";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
font-weight: 200;
|
||||||
|
src:
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Light-Subset.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Light-Subset.woff")
|
||||||
|
format("woff"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Light-Subset.otf")
|
||||||
|
format("opentype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "SF-Pro-Rounded";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src:
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Medium-Subset.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Medium-Subset.woff")
|
||||||
|
format("woff"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Medium-Subset.otf")
|
||||||
|
format("opentype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "SF-Pro-Rounded";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src:
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Regular-Subset.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Regular-Subset.woff")
|
||||||
|
format("woff"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Regular-Subset.otf")
|
||||||
|
format("opentype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "SF-Pro-Rounded";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
src:
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Semibold-Subset.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Semibold-Subset.woff")
|
||||||
|
format("woff"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Semibold-Subset.otf")
|
||||||
|
format("opentype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "SF-Pro-Rounded";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src:
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Thin-Subset.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Thin-Subset.woff")
|
||||||
|
format("woff"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Thin-Subset.otf")
|
||||||
|
format("opentype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "SF-Pro-Rounded";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100;
|
||||||
|
src:
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Ultralight-Subset.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Ultralight-Subset.woff")
|
||||||
|
format("woff"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Ultralight-Subset.otf")
|
||||||
|
format("opentype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Grold Rounded Slim";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
src:
|
||||||
|
url("https://cdn.tombutcher.work/fonts/Grold-Rounded-Slim-Bold-Subset.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/Grold-Rounded-Slim-Bold-Subset.woff")
|
||||||
|
format("woff"),
|
||||||
|
url("https://cdn.tombutcher.work/fonts/Grold-Rounded-Slim-Bold-Subset.otf")
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
511
public/global.css
Normal file
511
public/global.css
Normal file
@ -0,0 +1,511 @@
|
|||||||
|
* {
|
||||||
|
font-family: "SF-Pro-Rounded";
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
touch-action: none; /* Disable touch actions (like scrolling) */
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
span {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--unit-100vh: 100vh;
|
||||||
|
}
|
||||||
|
@supports (height: 100dvh) {
|
||||||
|
:root {
|
||||||
|
--unit-100vh: 100dvh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-typography,
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
font-family: "Grold Rounded Slim";
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-family: "SF-Pro-Rounded" !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
letter-spacing: 1.3px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tblogo {
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbview {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 25px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbbutton {
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbbutton:hover {
|
||||||
|
background: none !important;
|
||||||
|
color: white !important;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbnav span {
|
||||||
|
font-size: 30px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbnav {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
color: white;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
line-height: 1.2;
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 6px 15px;
|
||||||
|
padding-top: 9px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbmobilenav span {
|
||||||
|
font-family: "Grold Rounded Slim";
|
||||||
|
font-size: 45px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbmobilenav {
|
||||||
|
font-family: "Grold Rounded Slim";
|
||||||
|
font-size: 45px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
color: white;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
line-height: 1.2;
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 0px 0px;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbmobilenav:hover {
|
||||||
|
color: #ffffff !important;
|
||||||
|
background: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbnav Span {
|
||||||
|
font-family: "Grold Rounded Slim";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbmobilenav Span {
|
||||||
|
text-align: end;
|
||||||
|
line-height: 0.95;
|
||||||
|
font-family: "Grold Rounded Slim";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbnav:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
color: white !important;
|
||||||
|
box-shadow:
|
||||||
|
6px 6px 12px #c5c5c52b,
|
||||||
|
-6px -6px 12px #ffffff30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard {
|
||||||
|
width: 70%;
|
||||||
|
height: 70%;
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard .ant-card-head {
|
||||||
|
padding: 0 !important;
|
||||||
|
color: white;
|
||||||
|
min-height: 38px;
|
||||||
|
border-bottom: 1px solid #ffffff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard .ant-card-head .ant-card-head-title {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard .ant-card-body {
|
||||||
|
padding: 24px 0px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard input {
|
||||||
|
background: none;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #ffffff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard input:focus {
|
||||||
|
border-color: #ffffff !important;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard input:focus-within {
|
||||||
|
border-color: #ffffff !important;
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard input:active {
|
||||||
|
border-color: #ffffff;
|
||||||
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard input:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
color: white !important;
|
||||||
|
box-shadow:
|
||||||
|
6px 6px 12px #c5c5c52b,
|
||||||
|
-6px -6px 12px #ffffff30;
|
||||||
|
border-color: #ffffff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard input.ant-input-status-error {
|
||||||
|
background: none !important;
|
||||||
|
border-color: #ffffff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard input::placeholder {
|
||||||
|
color: #ffffffb0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard .ant-card-body button {
|
||||||
|
background: none;
|
||||||
|
border-color: #ffffff00;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard .ant-card-body button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
color: white !important;
|
||||||
|
box-shadow:
|
||||||
|
6px 6px 12px #c5c5c52b,
|
||||||
|
-6px -6px 12px #ffffff30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-actions Span {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard .ant-card-body button:active {
|
||||||
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
color: white !important;
|
||||||
|
box-shadow:
|
||||||
|
6px 6px 12px #c5c5c52b,
|
||||||
|
-6px -6px 12px #ffffff30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard .ant-card-actions {
|
||||||
|
background: none;
|
||||||
|
min-height: 40px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard .ant-card-actions li {
|
||||||
|
width: unset !important;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard .ant-card-actions .anticon {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbcard .ant-card-actions Button:hover {
|
||||||
|
box-shadow: none;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbsocial {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbsocial:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-mask {
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbturnstile {
|
||||||
|
width: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbturnstile .ant-modal-content {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogfilters {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbtag {
|
||||||
|
font-family: "SF-Pro-Rounded";
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbdisabledtag {
|
||||||
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
border-color: transparent;
|
||||||
|
color: #ffffff;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbtag:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
box-shadow: 6px 6px 12px #c5c5c52b;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbtag.ant-tag-checkable-checked {
|
||||||
|
color: #ffffff !important;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogbox {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
transition: box-shadow 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogtitle {
|
||||||
|
line-height: 0.9;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent h1 {
|
||||||
|
line-height: 0.6;
|
||||||
|
font-size: 2.3em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent p,
|
||||||
|
.tbblogcontent ul,
|
||||||
|
.tbblogcontent ol {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent ul p,
|
||||||
|
.tbblogcontent ul ul,
|
||||||
|
.tbblogcontent ul ol {
|
||||||
|
margin-bottom: 0em;
|
||||||
|
margin-top: 0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent ol p,
|
||||||
|
.tbblogcontent ol ul,
|
||||||
|
.tbblogcontent ol ol {
|
||||||
|
margin-bottom: 0em;
|
||||||
|
margin-top: 0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent ul,
|
||||||
|
.tbblogcontent ol {
|
||||||
|
padding-inline-start: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent h2 {
|
||||||
|
line-height: 0.6;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
margin-top: 1em;
|
||||||
|
font-size: 1.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent li {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent hr {
|
||||||
|
height: 1px;
|
||||||
|
border: none;
|
||||||
|
background-color: rgba(255, 255, 255, 0.25);
|
||||||
|
margin: 10px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbcallout {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
display: flex;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: normal;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbcallout .tbcalloutcontent {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbcallout div:first-child {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbcallout div p:last-child,
|
||||||
|
.tbblogcontent .tbcallout div div:last-child,
|
||||||
|
.tbblogcontent .tbcallout div ul:last-child,
|
||||||
|
.tbblogcontent .tbcallout div ol:last-child {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbcallout div p:first-child,
|
||||||
|
.tbblogcontent .tbcallout div h1:first-child,
|
||||||
|
.tbblogcontent .tbcallout div h2:first-child,
|
||||||
|
.tbblogcontent .tbcallout div ul:first-child,
|
||||||
|
.tbblogcontent .tbcallout div ol:first-child {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbcallout .tbemoji {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbcallout .tbemoji,
|
||||||
|
.tbblogcontent .tbcallout .tbicon {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbquote {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-left: 1px solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent img {
|
||||||
|
max-height: 260px;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbimage {
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbimagecaption {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent i.tbemoji {
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent img.tbicon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent div.tbicon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbtable {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbtable.tbcolumnheader tr:first-child {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbtable.tbrowheader td:first-child {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent td {
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbcolumnlist {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbcolumnlist .tbcolumn:first-child {
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbblogcontent .tbcolumnlist .tbcolumn {
|
||||||
|
flex: 1 1 0px;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
28
public/index.html
Normal file
28
public/index.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
href="https://cdn.tombutcher.work/favicon/favicon.ico"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<meta name="description" content="Tom Butcher's Personal Website" />
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
href="https://cdn.tombutcher.work/favicon/favicon192.png"
|
||||||
|
/>
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<link rel="stylesheet" href="%PUBLIC_URL%/fonts.css?e=e.css" />
|
||||||
|
<link rel="stylesheet" href="%PUBLIC_URL%/global.css?t=test" />
|
||||||
|
<title>TOM BUTCHER</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
public/manifest.json
Normal file
20
public/manifest.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"short_name": "TomButcher",
|
||||||
|
"name": "TOM BUTCHER",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "favicon512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#E800B6",
|
||||||
|
"background_color": "#2B0BFF"
|
||||||
|
}
|
||||||
0
public/notion-icons.css
Normal file
0
public/notion-icons.css
Normal file
278
src/App.jsx
Normal file
278
src/App.jsx
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Layout, Typography, Space } from "antd";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import ParticlesBackground from "./components/ParticlesBackground";
|
||||||
|
import { useMediaQuery } from "react-responsive";
|
||||||
|
import MainView from "./views/MainView";
|
||||||
|
import NotFoundView from "./views/NotFoundView";
|
||||||
|
import CVView from "./views/CVView";
|
||||||
|
import ContactView from "./views/ContactView";
|
||||||
|
import SocialsView from "./views/SocialsView";
|
||||||
|
import ExperienceView from "./views/ExperienceView";
|
||||||
|
import BlogsView from "./views/BlogsView";
|
||||||
|
import BlogView from "./views/BlogView";
|
||||||
|
import CacheReloadView from "./utils/CacheReloadView";
|
||||||
|
import { LinkOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
const { Content } = Layout;
|
||||||
|
const { Footer } = Layout;
|
||||||
|
const { Title } = Typography;
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery({ maxWidth: 600 });
|
||||||
|
|
||||||
|
const getQueryParam = (param) => {
|
||||||
|
const searchParams = new URLSearchParams(location.search);
|
||||||
|
return searchParams.get(param) || "index";
|
||||||
|
};
|
||||||
|
|
||||||
|
const [currentView, setCurrentView] = useState(getQueryParam("to"));
|
||||||
|
const [referrer, setReferrer] = useState(getQueryParam("r"));
|
||||||
|
const [isKeyboardOpen, setIsKeyboardOpen] = useState(false);
|
||||||
|
|
||||||
|
const initHandlers = () => {
|
||||||
|
// Add event listener to detect when input is focused
|
||||||
|
const handleFocus = () => {
|
||||||
|
setIsKeyboardOpen(true);
|
||||||
|
// Allow scrolling when input is focused
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
setIsKeyboardOpen(false);
|
||||||
|
// Smooth scroll to top when input is blurred
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach event listeners
|
||||||
|
document.querySelectorAll("input, textarea").forEach((input) => {
|
||||||
|
input.addEventListener("focus", handleFocus);
|
||||||
|
input.addEventListener("blur", handleBlur);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Clean up event listeners
|
||||||
|
document.querySelectorAll("input, textarea").forEach((input) => {
|
||||||
|
input.removeEventListener("focus", handleFocus);
|
||||||
|
input.removeEventListener("blur", handleBlur);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newReferrer = getQueryParam("r");
|
||||||
|
if (newReferrer != "*") {
|
||||||
|
setReferrer(newReferrer); // Only update if 'r' exists
|
||||||
|
}
|
||||||
|
setCurrentView(getQueryParam("to"));
|
||||||
|
}, [location.search]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
|
||||||
|
// Add the "to" parameter to the query
|
||||||
|
queryParams.set("to", currentView);
|
||||||
|
|
||||||
|
// If the referrer exists, add it to the query as well
|
||||||
|
if (referrer) {
|
||||||
|
queryParams.set("r", referrer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate with the updated query string
|
||||||
|
navigate(`?${queryParams.toString()}`);
|
||||||
|
|
||||||
|
// Initialize event handlers
|
||||||
|
const cleanup = initHandlers();
|
||||||
|
|
||||||
|
// Initial body setup to prevent scrollbars
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
document.body.style.margin = "0";
|
||||||
|
document.body.style.padding = "0";
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cleanup();
|
||||||
|
document.body.style.overflow = "auto"; // Reset on unmount
|
||||||
|
};
|
||||||
|
}, [currentView, referrer, navigate]);
|
||||||
|
|
||||||
|
const fadeVariants = {
|
||||||
|
initial: {
|
||||||
|
clipPath: "polygon(0 0, 0 0, 0 100%, 0% 100%)",
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
animate: {
|
||||||
|
clipPath: "polygon(0 0, 100% 0, 100% 100%, 0 100%)",
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.7,
|
||||||
|
ease: "easeInOut",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
clipPath: "polygon(100% 0, 100% 0, 100% 100%, 100% 100%)",
|
||||||
|
opacity: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.7,
|
||||||
|
ease: "easeInOut",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render different views based on currentView state
|
||||||
|
const renderView = () => {
|
||||||
|
if (currentView.startsWith("blog-")) {
|
||||||
|
const blogSlug = currentView.replace("blog-", "");
|
||||||
|
return <BlogView setCurrentView={setCurrentView} slug={blogSlug} />;
|
||||||
|
}
|
||||||
|
switch (currentView) {
|
||||||
|
case "index":
|
||||||
|
return <MainView setCurrentView={setCurrentView} />;
|
||||||
|
case "cv":
|
||||||
|
return <CVView setCurrentView={setCurrentView} />;
|
||||||
|
case "experience":
|
||||||
|
return <ExperienceView setCurrentView={setCurrentView} />;
|
||||||
|
case "blogs":
|
||||||
|
return <BlogsView setCurrentView={setCurrentView} />;
|
||||||
|
case "contact":
|
||||||
|
return <ContactView setCurrentView={setCurrentView} />;
|
||||||
|
case "socials":
|
||||||
|
return (
|
||||||
|
<SocialsView setCurrentView={setCurrentView} referrer={referrer} />
|
||||||
|
);
|
||||||
|
case "cacheReload":
|
||||||
|
return <CacheReloadView setCurrentView={setCurrentView} />;
|
||||||
|
default:
|
||||||
|
return <NotFoundView setCurrentView={setCurrentView} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ParticlesBackground />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "rgba(255, 255, 255, 0.0)",
|
||||||
|
backdropFilter: "blur(50px)",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
position: "absolute",
|
||||||
|
display: "inline",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<Layout
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
background: "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Content */}
|
||||||
|
<Content
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
flex: 1,
|
||||||
|
overflow: "hidden",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={currentView}
|
||||||
|
variants={fadeVariants}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
overflow: "auto",
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderView()}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</Content>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ paddingLeft: "20px" }}
|
||||||
|
animate={{
|
||||||
|
paddingLeft: currentView === "index" && isMobile ? "50px" : "20px",
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "0",
|
||||||
|
left: isMobile ? "0" : "50%",
|
||||||
|
transform: isMobile ? "unset" : "translateX(-50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Footer
|
||||||
|
style={{
|
||||||
|
zIndex: 1,
|
||||||
|
textAlign: isMobile ? "left" : "center",
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
backgroundSize: "cover",
|
||||||
|
background: "transparent",
|
||||||
|
color: "#FFFFFF",
|
||||||
|
padding: "20px 25px",
|
||||||
|
paddingLeft: "0px",
|
||||||
|
paddingTop: "0px",
|
||||||
|
fontSize: "16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setCurrentView("index");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
© {new Date().getFullYear()}{" "}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setCurrentView("socials");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LinkOutlined />
|
||||||
|
</a>
|
||||||
|
</Space>
|
||||||
|
</Footer>
|
||||||
|
</motion.div>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
117
src/components/AuthParticles.jsx
Normal file
117
src/components/AuthParticles.jsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||||
|
import Particles, { initParticlesEngine } from "@tsparticles/react";
|
||||||
|
import { loadSlim } from "@tsparticles/slim";
|
||||||
|
|
||||||
|
import "./Auth.css";
|
||||||
|
|
||||||
|
const ParticlesComponent = React.memo(({ options, particlesLoaded }) => {
|
||||||
|
return (
|
||||||
|
<Particles
|
||||||
|
id="tsparticles"
|
||||||
|
options={options}
|
||||||
|
particlesLoaded={particlesLoaded}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ParticlesComponent.displayName = "ParticlesComponent";
|
||||||
|
|
||||||
|
const AuthParticles = () => {
|
||||||
|
const [init, setInit] = useState(false);
|
||||||
|
|
||||||
|
// this should be run only once per application lifetime
|
||||||
|
useEffect(() => {
|
||||||
|
initParticlesEngine(async (engine) => {
|
||||||
|
await loadSlim(engine);
|
||||||
|
}).then(() => {
|
||||||
|
setInit(true);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const particlesLoaded = useCallback((container) => {
|
||||||
|
console.log(container);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() => ({
|
||||||
|
background: {
|
||||||
|
color: {
|
||||||
|
value: "#141414",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fpsLimit: 120,
|
||||||
|
interactivity: {
|
||||||
|
events: {
|
||||||
|
onClick: {
|
||||||
|
enable: true,
|
||||||
|
mode: "push",
|
||||||
|
},
|
||||||
|
onHover: {
|
||||||
|
enable: true,
|
||||||
|
mode: "repulse",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: {
|
||||||
|
push: {
|
||||||
|
quantity: 4,
|
||||||
|
},
|
||||||
|
repulse: {
|
||||||
|
distance: 200,
|
||||||
|
duration: 0.4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
particles: {
|
||||||
|
color: {
|
||||||
|
value: "#ffffff",
|
||||||
|
},
|
||||||
|
links: {
|
||||||
|
color: "#ffffff",
|
||||||
|
distance: 150,
|
||||||
|
enable: true,
|
||||||
|
opacity: 0.5,
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
move: {
|
||||||
|
direction: "none",
|
||||||
|
enable: true,
|
||||||
|
outModes: {
|
||||||
|
default: "bounce",
|
||||||
|
},
|
||||||
|
random: false,
|
||||||
|
speed: 1,
|
||||||
|
straight: false,
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
density: {
|
||||||
|
enable: true,
|
||||||
|
},
|
||||||
|
value: 160,
|
||||||
|
},
|
||||||
|
opacity: {
|
||||||
|
value: 0.5,
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
type: "circle",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
value: { min: 1, max: 5 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
detectRetina: true,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{init && (
|
||||||
|
<ParticlesComponent
|
||||||
|
options={options}
|
||||||
|
particlesLoaded={particlesLoaded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthParticles;
|
||||||
122
src/components/ParticlesBackground.jsx
Normal file
122
src/components/ParticlesBackground.jsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||||
|
import Particles, { initParticlesEngine } from "@tsparticles/react";
|
||||||
|
import { loadSlim } from "@tsparticles/slim";
|
||||||
|
|
||||||
|
const ParticlesComponent = React.memo(({ options, particlesLoaded }) => {
|
||||||
|
return (
|
||||||
|
<Particles
|
||||||
|
id="tsparticles"
|
||||||
|
options={options}
|
||||||
|
particlesLoaded={particlesLoaded}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ParticlesComponent.displayName = "ParticlesComponent";
|
||||||
|
|
||||||
|
const ParticlesBackground = () => {
|
||||||
|
const [init, setInit] = useState(false);
|
||||||
|
|
||||||
|
// this should be run only once per application lifetime
|
||||||
|
useEffect(() => {
|
||||||
|
initParticlesEngine(async (engine) => {
|
||||||
|
await loadSlim(engine);
|
||||||
|
}).then(() => {
|
||||||
|
setInit(true);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const particlesLoaded = useCallback((container) => {
|
||||||
|
console.log(container);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() => ({
|
||||||
|
background: {
|
||||||
|
color: {
|
||||||
|
value: "#FF00A1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fpsLimit: 120,
|
||||||
|
interactivity: {
|
||||||
|
events: {
|
||||||
|
onClick: {
|
||||||
|
enable: false,
|
||||||
|
mode: "push",
|
||||||
|
},
|
||||||
|
onHover: {
|
||||||
|
enable: true,
|
||||||
|
mode: "repulse",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: {
|
||||||
|
push: {
|
||||||
|
quantity: 4,
|
||||||
|
},
|
||||||
|
repulse: {
|
||||||
|
distance: 100,
|
||||||
|
duration: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
particles: {
|
||||||
|
color: {
|
||||||
|
value: [
|
||||||
|
"#FF00A1",
|
||||||
|
"#0310FF",
|
||||||
|
"#2DE2FF",
|
||||||
|
"#6E00FF",
|
||||||
|
"#0310FF",
|
||||||
|
"#FF00A1",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
links: {
|
||||||
|
color: "#ffffff",
|
||||||
|
distance: 150,
|
||||||
|
enable: true,
|
||||||
|
opacity: 0.0,
|
||||||
|
width: 0,
|
||||||
|
},
|
||||||
|
move: {
|
||||||
|
direction: "none",
|
||||||
|
enable: true,
|
||||||
|
outModes: {
|
||||||
|
default: "out",
|
||||||
|
},
|
||||||
|
random: true,
|
||||||
|
speed: 1,
|
||||||
|
straight: false,
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
density: {
|
||||||
|
enable: true,
|
||||||
|
},
|
||||||
|
value: 400,
|
||||||
|
},
|
||||||
|
opacity: {
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
type: "circle",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
value: { min: 100, max: 300 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
detectRetina: true,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{init && (
|
||||||
|
<ParticlesComponent
|
||||||
|
options={options}
|
||||||
|
particlesLoaded={particlesLoaded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ParticlesBackground;
|
||||||
28
src/components/TurnstileModal.jsx
Normal file
28
src/components/TurnstileModal.jsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Modal } from "antd";
|
||||||
|
import Turnstile from "react-turnstile";
|
||||||
|
|
||||||
|
const TurnstileModal = ({ open, onClose, onSuccess }) => {
|
||||||
|
const [turnstileToken, setTurnstileToken] = useState("");
|
||||||
|
|
||||||
|
const handleVerify = (token) => {
|
||||||
|
setTurnstileToken(token);
|
||||||
|
onClose(); // Close modal after verification
|
||||||
|
onSuccess(token); // Notify parent component
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
footer={null}
|
||||||
|
onCancel={onClose}
|
||||||
|
closeIcon={null}
|
||||||
|
centered
|
||||||
|
className="tbturnstile"
|
||||||
|
>
|
||||||
|
<Turnstile sitekey="0x4AAAAAAA_bc3QTrE68whtg" onVerify={handleVerify} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TurnstileModal;
|
||||||
15
src/index.js
Normal file
15
src/index.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import App from "./App";
|
||||||
|
import "antd/dist/reset.css"; // Import Ant Design styles
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
,
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
72
src/utils/CacheReloadView.jsx
Normal file
72
src/utils/CacheReloadView.jsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useMediaQuery } from "react-responsive";
|
||||||
|
import { Card, Flex, message } from "antd";
|
||||||
|
import { LoadingOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
const CacheReloadView = ({ setCurrentView }) => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery({ maxWidth: 600 });
|
||||||
|
|
||||||
|
// Fetch blogs from API
|
||||||
|
useEffect(() => {
|
||||||
|
const reloadCache = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
"http://192.168.68.53:8787/api/utils/cache",
|
||||||
|
{ method: "POST" },
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
window.close();
|
||||||
|
setLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error reloading cache:", err);
|
||||||
|
setError(err.message);
|
||||||
|
setLoading(false);
|
||||||
|
message.error("Failed to reload cache!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reloadCache();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tbview">
|
||||||
|
<Card
|
||||||
|
className={"tbcard"}
|
||||||
|
title="Reload Cache"
|
||||||
|
style={{
|
||||||
|
width: isMobile ? "100%" : "70%",
|
||||||
|
height: isMobile ? "100%" : "70%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
bodyStyle={{
|
||||||
|
flex: 1,
|
||||||
|
overflowY: "hidden", // Changed from auto to hidden
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Flex justify="center" align="center" style={{ padding: "20px" }}>
|
||||||
|
<LoadingOutlined style={{ fontSize: 48, color: "white" }} spin />
|
||||||
|
</Flex>
|
||||||
|
) : error ? (
|
||||||
|
<Flex justify="center" align="center">
|
||||||
|
<p>Error to reload cache: {error}</p>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Flex justify="center" align="center">
|
||||||
|
<p>Cache Reloaded!</p>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CacheReloadView;
|
||||||
137
src/views/BlogView.jsx
Normal file
137
src/views/BlogView.jsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useMediaQuery } from "react-responsive";
|
||||||
|
import { Tag, Button, Card, Flex, message } from "antd";
|
||||||
|
import { ArrowLeftOutlined, LoadingOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
const BlogView = ({ setCurrentView, slug }) => {
|
||||||
|
const [blog, setBlog] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery({ maxWidth: 600 });
|
||||||
|
|
||||||
|
// Fetch blog content from API
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchBlog = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://web.tombutcher.work/api/view/blog?b=${slug}`,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
setBlog(data);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error fetching blog with slug ${slug}:`, err);
|
||||||
|
setError(err.message);
|
||||||
|
setLoading(false);
|
||||||
|
message.error("Failed to load blog content");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (slug) {
|
||||||
|
fetchBlog();
|
||||||
|
} else {
|
||||||
|
setError("No blog slug provided");
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [slug]);
|
||||||
|
|
||||||
|
// Format date
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tbview">
|
||||||
|
<Card
|
||||||
|
className={"tbcard"}
|
||||||
|
title={"Blog"}
|
||||||
|
style={{
|
||||||
|
width: isMobile ? "100%" : "70%",
|
||||||
|
height: isMobile ? "100%" : "70%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
bodyStyle={{
|
||||||
|
flex: 1,
|
||||||
|
overflowY: "auto",
|
||||||
|
padding: "0",
|
||||||
|
margin: "20px 0",
|
||||||
|
}}
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
className={"tbbutton"}
|
||||||
|
key="back"
|
||||||
|
onClick={() => setCurrentView("blogs")}
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Flex justify="center" align="center" style={{ padding: "20px" }}>
|
||||||
|
<LoadingOutlined style={{ fontSize: 48, color: "white" }} spin />
|
||||||
|
</Flex>
|
||||||
|
) : error ? (
|
||||||
|
<Flex justify="center" align="center">
|
||||||
|
<p>Error loading blog: {error}</p>
|
||||||
|
</Flex>
|
||||||
|
) : blog ? (
|
||||||
|
<div style={{ padding: isMobile ? "0 0" : "0 20px" }}>
|
||||||
|
<Flex vertical gap={0} style={{ padding: "0px 0" }}>
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<h1 className="tbblogtitle">{blog.title}</h1>
|
||||||
|
{!isMobile ? (
|
||||||
|
<span style={{ fontSize: "1.17em", color: "#ffffff80" }}>
|
||||||
|
{formatDate(blog.last_edited)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<p style={{ fontSize: "16px", fontStyle: "italic" }}>
|
||||||
|
{blog.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
{!isMobile ? (
|
||||||
|
<span style={{ fontSize: "14px", color: "#ffffff80" }}>
|
||||||
|
By {blog.author}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<Flex gap={4}>
|
||||||
|
{blog.tags &&
|
||||||
|
blog.tags.map((tag) => (
|
||||||
|
<Tag className="tbdisabledtag" key={`blog-view-${tag}`}>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="tbblogcontent"
|
||||||
|
dangerouslySetInnerHTML={{ __html: blog.content }}
|
||||||
|
style={{ marginTop: "20px" }}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Flex justify="center" align="center">
|
||||||
|
<p>Blog not found</p>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlogView;
|
||||||
213
src/views/BlogsView.jsx
Normal file
213
src/views/BlogsView.jsx
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useMediaQuery } from "react-responsive";
|
||||||
|
import { Tag, Button, Card, Flex, message } from "antd";
|
||||||
|
import { LoadingOutlined, ArrowLeftOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
const BlogsView = ({ setCurrentView }) => {
|
||||||
|
const [blogs, setBlogs] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [selectedTags, setSelectedTags] = useState([]);
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery({ maxWidth: 600 });
|
||||||
|
|
||||||
|
// Fetch blogs from API
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchBlogs = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
"https://web.tombutcher.work/api/list/blogs",
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
setBlogs(data);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching blogs:", err);
|
||||||
|
setError(err.message);
|
||||||
|
setLoading(false);
|
||||||
|
message.error("Failed to load blogs");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchBlogs();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Get all unique tags from blogs
|
||||||
|
const getAllTags = () => {
|
||||||
|
const tagsSet = new Set();
|
||||||
|
blogs.forEach((blog) => {
|
||||||
|
blog.tags.forEach((tag) => tagsSet.add(tag));
|
||||||
|
});
|
||||||
|
return Array.from(tagsSet);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle tag selection
|
||||||
|
const handleTagChange = (tag, checked) => {
|
||||||
|
const nextSelectedTags = checked
|
||||||
|
? [...selectedTags, tag]
|
||||||
|
: selectedTags.filter((t) => t !== tag);
|
||||||
|
setSelectedTags(nextSelectedTags);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter blogs based on selected tags
|
||||||
|
const filteredBlogs =
|
||||||
|
selectedTags.length > 0
|
||||||
|
? blogs.filter((blog) =>
|
||||||
|
blog.tags.some((tag) => selectedTags.includes(tag)),
|
||||||
|
)
|
||||||
|
: blogs;
|
||||||
|
|
||||||
|
// Format date
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tbview">
|
||||||
|
<Card
|
||||||
|
className={"tbcard"}
|
||||||
|
title="Blog"
|
||||||
|
style={{
|
||||||
|
width: isMobile ? "100%" : "70%",
|
||||||
|
height: isMobile ? "100%" : "70%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
bodyStyle={{
|
||||||
|
flex: 1,
|
||||||
|
overflowY: "hidden", // Changed from auto to hidden
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
className={"tbbutton"}
|
||||||
|
key="back"
|
||||||
|
onClick={() => setCurrentView("index")}
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Flex justify="center" align="center" style={{ padding: "20px" }}>
|
||||||
|
<LoadingOutlined style={{ fontSize: 48, color: "white" }} spin />
|
||||||
|
</Flex>
|
||||||
|
) : error ? (
|
||||||
|
<Flex justify="center" align="center">
|
||||||
|
<p>Error loading blogs: {error}</p>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Tags section with center alignment and no background */}
|
||||||
|
<Flex
|
||||||
|
justify="center"
|
||||||
|
wrap
|
||||||
|
className="tbblogfilters"
|
||||||
|
style={{
|
||||||
|
flexShrink: 0, // Prevents tags from shrinking
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getAllTags().map((tag) => (
|
||||||
|
<Tag.CheckableTag
|
||||||
|
key={tag}
|
||||||
|
checked={selectedTags.includes(tag)}
|
||||||
|
onChange={(checked) => handleTagChange(tag, checked)}
|
||||||
|
className="tbtag"
|
||||||
|
style={{
|
||||||
|
margin: "4px",
|
||||||
|
background: "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</Tag.CheckableTag>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* Blog list with scrolling */}
|
||||||
|
<Flex
|
||||||
|
style={{
|
||||||
|
margin: isMobile ? "0" : "0 10%",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "20px",
|
||||||
|
padding: isMobile ? "12px 0px" : "12px 24px",
|
||||||
|
overflowY: "auto", // Add scroll to this container only
|
||||||
|
flexGrow: 1, // Allow this container to grow and take available space
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filteredBlogs.length > 0 ? (
|
||||||
|
filteredBlogs.map((blog) => (
|
||||||
|
<Flex
|
||||||
|
key={blog.slug}
|
||||||
|
vertical
|
||||||
|
onClick={() => setCurrentView(`blog-${blog.slug}`)}
|
||||||
|
className="tbblogbox"
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<h3 style={{ marginBottom: "0px" }}>{blog.title}</h3>
|
||||||
|
<span style={{ fontSize: "1.17em", color: "#ffffff80" }}>
|
||||||
|
{formatDate(blog.last_edited)}
|
||||||
|
</span>
|
||||||
|
</Flex>
|
||||||
|
<p style={{ marginBottom: "8px" }}>{blog.description}</p>
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<span style={{ fontSize: "14px", color: "#ffffff80" }}>
|
||||||
|
By {blog.author}
|
||||||
|
</span>
|
||||||
|
<Flex gap={"small"}>
|
||||||
|
{blog.tags.map((tag) => (
|
||||||
|
<Tag
|
||||||
|
key={`${blog.slug}-${tag}`}
|
||||||
|
className="tbdisabledtag"
|
||||||
|
style={{ margin: "0" }}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p>No blogs match the selected tags</p>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// CSS for hover effect
|
||||||
|
const blogCardStyles = `
|
||||||
|
.blog-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbtag {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbdisabledtag {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default BlogsView;
|
||||||
115
src/views/CVView.jsx
Normal file
115
src/views/CVView.jsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useMediaQuery } from "react-responsive";
|
||||||
|
import { Form, Input, Button, Card, Flex } from "antd";
|
||||||
|
import {
|
||||||
|
FilePdfOutlined,
|
||||||
|
CloudDownloadOutlined,
|
||||||
|
ArrowLeftOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
const CVView = ({ setCurrentView }) => {
|
||||||
|
const onFinish = (values) => {
|
||||||
|
console.log("Form values:", values);
|
||||||
|
};
|
||||||
|
const isMobile = useMediaQuery({ maxWidth: 600 });
|
||||||
|
const [isEmailFocused, setIsEmailFocused] = useState(false); // Track focus state
|
||||||
|
return (
|
||||||
|
<div className="tbview">
|
||||||
|
<Card
|
||||||
|
className={"tbcard"}
|
||||||
|
title="CV"
|
||||||
|
style={{
|
||||||
|
width: isMobile ? "100%" : "70%",
|
||||||
|
height: isMobile ? "100%" : "70%",
|
||||||
|
}}
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
className={"tbbutton"}
|
||||||
|
key="back"
|
||||||
|
onClick={() => setCurrentView("index")}
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Flex style={{ margin: isMobile ? "0px 0px" : "0px 10%" }} vertical>
|
||||||
|
<Flex
|
||||||
|
gap="middle"
|
||||||
|
style={{ borderBottom: "1px solid #ffffff", padding: "10px 0px" }}
|
||||||
|
>
|
||||||
|
<FilePdfOutlined style={{ fontSize: "64px" }} />
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<h3 style={{ marginBottom: "0px" }}>Curriculum Vitae 2025</h3>
|
||||||
|
<p style={{ marginBottom: "2px" }}>
|
||||||
|
Enter your email address to download:
|
||||||
|
</p>
|
||||||
|
<Form
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={onFinish}
|
||||||
|
style={{ paddingBottom: "5px" }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="email"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: "Please enter your email" },
|
||||||
|
]}
|
||||||
|
style={{ marginBottom: "0" }}
|
||||||
|
>
|
||||||
|
<Flex gap="small">
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
placeholder="example@example.com"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
onFocus={() => setIsEmailFocused(true)} // Set focused state to true
|
||||||
|
onBlur={() => setIsEmailFocused(false)} // Set focused state to false
|
||||||
|
/>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
<CloudDownloadOutlined />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
<Flex gap="middle" style={{ padding: "10px 0px" }}>
|
||||||
|
<FilePdfOutlined style={{ fontSize: "64px" }} />
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<h3 style={{ marginBottom: "0px" }}>Curriculum Vitae 2024</h3>
|
||||||
|
<p style={{ marginBottom: "2px" }}>
|
||||||
|
Enter your email address to download:
|
||||||
|
</p>
|
||||||
|
<Form
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={onFinish}
|
||||||
|
style={{ paddingBottom: "5px" }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="email"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: "Please enter your email" },
|
||||||
|
]}
|
||||||
|
style={{ marginBottom: "0" }}
|
||||||
|
>
|
||||||
|
<Flex gap="small">
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
placeholder="example@example.com"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
onFocus={() => setIsEmailFocused(true)} // Set focused state to true
|
||||||
|
onBlur={() => setIsEmailFocused(false)} // Set focused state to false
|
||||||
|
/>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
<CloudDownloadOutlined />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CVView;
|
||||||
103
src/views/ContactView.jsx
Normal file
103
src/views/ContactView.jsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useMediaQuery } from "react-responsive";
|
||||||
|
import { Form, Input, Button, Card, Flex, message } from "antd";
|
||||||
|
import TurnstileModal from "../components/TurnstileModal";
|
||||||
|
import { ArrowRightOutlined, ArrowLeftOutlined } from "@ant-design/icons";
|
||||||
|
const ContactView = ({ setCurrentView }) => {
|
||||||
|
const isMobile = useMediaQuery({ maxWidth: 600 });
|
||||||
|
const [isEmailFocused, setIsEmailFocused] = useState(false); // Track focus state
|
||||||
|
const [turnstileOpen, setTurnstileOpen] = useState(false);
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const handleSubmit = async (token) => {
|
||||||
|
console.log(token);
|
||||||
|
if (!token) {
|
||||||
|
setTurnstileOpen(true); // Open Turnstile modal
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://web.tombutcher.work/api/contact", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ email, token }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
message.success("Message sent successfully!");
|
||||||
|
} else {
|
||||||
|
message.error("Verification failed. Try again.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error("Error sending request.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="tbview">
|
||||||
|
<Card
|
||||||
|
className={"tbcard"}
|
||||||
|
title="Contact"
|
||||||
|
style={{
|
||||||
|
width: isMobile ? "100%" : "70%",
|
||||||
|
height: isMobile ? "100%" : "70%",
|
||||||
|
}}
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
className={"tbbutton"}
|
||||||
|
key="back"
|
||||||
|
onClick={() => setCurrentView("index")}
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div style={{ minWidth: isMobile ? "100%" : "70%" }}>
|
||||||
|
<p>Enter your email below and I will be in contact:</p>
|
||||||
|
<Form
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={() => {
|
||||||
|
handleSubmit();
|
||||||
|
}}
|
||||||
|
style={{ paddingBottom: "5px" }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="email"
|
||||||
|
rules={[{ required: true, message: "Please enter your email" }]}
|
||||||
|
style={{ marginBottom: "8px" }}
|
||||||
|
>
|
||||||
|
<Flex gap="small">
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
placeholder="example@example.com"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
onFocus={() => setIsEmailFocused(true)} // Set focused state to true
|
||||||
|
onBlur={() => setIsEmailFocused(false)} // Set focused state to false
|
||||||
|
/>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
<ArrowRightOutlined />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
<p>
|
||||||
|
I'm often busy, but I'll make sure to get back to you
|
||||||
|
within 48 hours!
|
||||||
|
</p>
|
||||||
|
<p style={{ marginBottom: "0px", color: "rgb(255 255 255 / 50%)" }}>
|
||||||
|
By clicking the submit button, you consent to your email address and
|
||||||
|
certain browser-related details (such as an approximate geographical
|
||||||
|
location based on your IP) being sent exclusively to me via email.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<TurnstileModal
|
||||||
|
open={turnstileOpen}
|
||||||
|
onClose={() => setTurnstileOpen(false)}
|
||||||
|
onSuccess={handleSubmit} // Sends token to handleSubmit
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactView;
|
||||||
63
src/views/ExperienceView.jsx
Normal file
63
src/views/ExperienceView.jsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useMediaQuery } from "react-responsive";
|
||||||
|
import { Button, Card, Flex } from "antd";
|
||||||
|
import { ArrowLeftOutlined } from "@ant-design/icons";
|
||||||
|
const ExperienceView = ({ setCurrentView }) => {
|
||||||
|
const isMobile = useMediaQuery({ maxWidth: 600 });
|
||||||
|
return (
|
||||||
|
<div className="tbview">
|
||||||
|
<Card
|
||||||
|
className={"tbcard"}
|
||||||
|
title="Experience"
|
||||||
|
style={{
|
||||||
|
width: isMobile ? "100%" : "70%",
|
||||||
|
height: isMobile ? "100%" : "70%",
|
||||||
|
}}
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
className={"tbbutton"}
|
||||||
|
key="back"
|
||||||
|
onClick={() => setCurrentView("index")}
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
vertical
|
||||||
|
gap="45px"
|
||||||
|
style={{ margin: "0 10%", paddingBottom: "15px" }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`https://cdn.tombutcher.work/logos/nucleus-logo.svg`}
|
||||||
|
alt="Nucleus Logo"
|
||||||
|
height={"80px"}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={`https://cdn.tombutcher.work/logos/greeneking-logo.svg`}
|
||||||
|
alt="Greene King Logo"
|
||||||
|
height={"60px"}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={`https://cdn.tombutcher.work/logos/thelondonoffice-logo.svg`}
|
||||||
|
alt="The London Office Logo"
|
||||||
|
height={"55px"}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={`https://cdn.tombutcher.work/logos/revolution-logo.svg`}
|
||||||
|
alt="Revolution Logo"
|
||||||
|
height={"65px"}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={`https://cdn.tombutcher.work/logos/istore-logo.svg`}
|
||||||
|
alt="Revolution Logo"
|
||||||
|
height={"40px"}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExperienceView;
|
||||||
90
src/views/MainView.jsx
Normal file
90
src/views/MainView.jsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// views/MainView.jsx
|
||||||
|
import React from "react";
|
||||||
|
import { Typography, Button, Flex, Space } from "antd";
|
||||||
|
import { ReactComponent as LogoHorizontal } from "./logo-horizontal.svg";
|
||||||
|
import { ReactComponent as LogoVertical } from "./logo-vertical.svg";
|
||||||
|
import { useMediaQuery } from "react-responsive";
|
||||||
|
|
||||||
|
const { Paragraph } = Typography;
|
||||||
|
|
||||||
|
const MainView = ({ setCurrentView }) => {
|
||||||
|
const isMobile = useMediaQuery({ maxWidth: 600 });
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
justify="center"
|
||||||
|
align={isMobile ? "flex-start" : "center"}
|
||||||
|
vertical
|
||||||
|
style={{ height: "100%", padding: "24px 50px" }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: "center", maxWidth: "800px", margin: "0 0" }}>
|
||||||
|
<div className={"tblogo"}>
|
||||||
|
{isMobile ? (
|
||||||
|
<LogoVertical height={180} />
|
||||||
|
) : (
|
||||||
|
<LogoHorizontal height={60} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isMobile ? (
|
||||||
|
<Flex
|
||||||
|
justify="left"
|
||||||
|
align="left"
|
||||||
|
vertical
|
||||||
|
style={{ marginTop: "12.5px" }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="tbmobilenav"
|
||||||
|
style={{ textAlign: "left" }}
|
||||||
|
onClick={() => setCurrentView("experience")}
|
||||||
|
>
|
||||||
|
Experience
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="tbmobilenav"
|
||||||
|
style={{ textAlign: "left" }}
|
||||||
|
onClick={() => setCurrentView("blogs")}
|
||||||
|
>
|
||||||
|
Blog
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="tbmobilenav"
|
||||||
|
style={{ textAlign: "left" }}
|
||||||
|
onClick={() => setCurrentView("cv")}
|
||||||
|
>
|
||||||
|
CV
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="tbmobilenav"
|
||||||
|
style={{ textAlign: "left" }}
|
||||||
|
onClick={() => setCurrentView("contact")}
|
||||||
|
>
|
||||||
|
Contact
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Flex justify="center" align="center">
|
||||||
|
<Button
|
||||||
|
className={"tbnav"}
|
||||||
|
onClick={() => setCurrentView("experience")}
|
||||||
|
>
|
||||||
|
Experience
|
||||||
|
</Button>
|
||||||
|
<Button className={"tbnav"} onClick={() => setCurrentView("blogs")}>
|
||||||
|
Blog
|
||||||
|
</Button>
|
||||||
|
<Button className={"tbnav"} onClick={() => setCurrentView("cv")}>
|
||||||
|
CV
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={"tbnav"}
|
||||||
|
onClick={() => setCurrentView("contact")}
|
||||||
|
>
|
||||||
|
Contact
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainView;
|
||||||
18
src/views/NotFoundView.jsx
Normal file
18
src/views/NotFoundView.jsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button, Flex } from "antd";
|
||||||
|
import { useMediaQuery } from "react-responsive";
|
||||||
|
|
||||||
|
const NotFoundView = ({ setCurrentView }) => {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: "center", maxWidth: "800px", margin: "0 auto" }}>
|
||||||
|
<h1>404 Not found</h1>
|
||||||
|
<Flex justify="center" align="center">
|
||||||
|
<Button className="tbnav" onClick={() => setCurrentView("index")}>
|
||||||
|
Home
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFoundView;
|
||||||
98
src/views/SocialsView.jsx
Normal file
98
src/views/SocialsView.jsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useMediaQuery } from "react-responsive";
|
||||||
|
import { Flex, Spin } from "antd";
|
||||||
|
import { ReactComponent as LogoHorizontal } from "./logo-horizontal.svg";
|
||||||
|
import { ReactComponent as LogoVertical } from "./logo-vertical.svg";
|
||||||
|
import { LoadingOutlined } from "@ant-design/icons";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import * as FaIcons from "react-icons/fa6"; // Import all FA6 icons dynamically
|
||||||
|
|
||||||
|
const SocialsView = ({ setCurrentView, referrer }) => {
|
||||||
|
const isMobile = useMediaQuery({ maxWidth: 600 });
|
||||||
|
const [socials, setSocials] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true); // Loading state
|
||||||
|
// Fetch social media links from the API
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSocials = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`http://192.168.68.53:8787/api/list/socials?r=${referrer}`,
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
setSocials(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching socials:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false); // Stop loading once data is fetched
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSocials();
|
||||||
|
}, [referrer]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
justify="center"
|
||||||
|
align={isMobile ? "flex-start" : "center"}
|
||||||
|
vertical
|
||||||
|
style={{ height: "100%", padding: "24px 50px" }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: "center", maxWidth: "900px", margin: "0 0" }}>
|
||||||
|
<div className={"tblogo"}>
|
||||||
|
{isMobile ? (
|
||||||
|
<LogoVertical height={180} />
|
||||||
|
) : (
|
||||||
|
<LogoHorizontal height={60} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Flex
|
||||||
|
gap="20px"
|
||||||
|
align={"center"}
|
||||||
|
justify={isMobile ? "start" : "center"}
|
||||||
|
style={{
|
||||||
|
margin: isMobile ? "12.5px 0" : "12.5px 10px",
|
||||||
|
paddingBottom: "15px",
|
||||||
|
}}
|
||||||
|
wrap
|
||||||
|
>
|
||||||
|
{/* Show loading spinner while data is being fetched */}
|
||||||
|
{loading ? (
|
||||||
|
<Spin
|
||||||
|
indicator={
|
||||||
|
<LoadingOutlined
|
||||||
|
style={{ fontSize: 48, color: "white" }}
|
||||||
|
spin
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Flex
|
||||||
|
gap="20px"
|
||||||
|
align={"center"}
|
||||||
|
justify={isMobile ? "start" : "center"}
|
||||||
|
style={{
|
||||||
|
margin: isMobile ? "12.5px 0" : "12.5px 10px",
|
||||||
|
paddingBottom: "15px",
|
||||||
|
}}
|
||||||
|
wrap
|
||||||
|
>
|
||||||
|
{/* Dynamically render social media icons */}
|
||||||
|
{socials.map(({ name, icon, url }) => {
|
||||||
|
const IconComponent = FaIcons[icon]; // Map string to actual icon component
|
||||||
|
return (
|
||||||
|
IconComponent && (
|
||||||
|
<a key={name} href={url} className="tbsocial">
|
||||||
|
<IconComponent style={{ fontSize: "64px" }} />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SocialsView;
|
||||||
21
src/views/logo-horizontal.svg
Normal file
21
src/views/logo-horizontal.svg
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 894 87" version="1.1" xmlns="http://www.w3.org/2000/svg" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(1,0,0,1,-91.7237,-316.8)">
|
||||||
|
<g transform="matrix(1,0,0,1,-186.893,-49.2326)">
|
||||||
|
<path d="M282.793,367.473L335.641,367.473C336.697,367.473 337.657,367.881 338.521,368.697C339.385,369.513 339.817,370.449 339.817,371.505L339.817,381.153C339.817,382.209 339.385,383.145 338.521,383.961C337.657,384.777 336.697,385.185 335.641,385.185L318.937,385.185L318.937,446.673C318.937,447.729 318.529,448.689 317.713,449.553C316.897,450.417 315.961,450.849 314.905,450.849L303.529,450.849C302.473,450.849 301.537,450.417 300.721,449.553C299.905,448.689 299.497,447.729 299.497,446.673L299.497,385.185L282.793,385.185C281.737,385.185 280.777,384.777 279.913,383.961C279.049,383.145 278.617,382.209 278.617,381.153L278.617,371.505C278.617,370.449 279.049,369.513 279.913,368.697C280.777,367.881 281.737,367.473 282.793,367.473Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
<path d="M354.937,378.273C361.657,370.113 371.017,366.033 383.017,366.033C395.017,366.033 404.353,370.113 411.025,378.273C417.697,386.433 421.033,396.753 421.033,409.233C421.033,421.617 417.673,431.913 410.953,440.121C404.233,448.329 394.921,452.433 383.017,452.433C371.113,452.433 361.801,448.329 355.081,440.121C348.361,431.913 345.001,421.617 345.001,409.233C345.001,396.753 348.313,386.433 354.937,378.273ZM365.017,409.233C365.017,426.225 371.017,434.721 383.017,434.721C395.017,434.721 401.017,426.225 401.017,409.233C401.017,392.241 395.017,383.745 383.017,383.745C371.017,383.745 365.017,392.241 365.017,409.233Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
<path d="M477.049,439.761L475.321,439.761C474.265,439.761 473.545,439.377 473.161,438.609L455.593,406.497L455.593,446.673C455.593,447.729 455.185,448.689 454.369,449.553C453.553,450.417 452.617,450.849 451.561,450.849L440.329,450.849C439.273,450.849 438.337,450.417 437.521,449.553C436.705,448.689 436.297,447.729 436.297,446.673L436.297,371.649C436.297,370.593 436.705,369.633 437.521,368.769C438.337,367.905 439.273,367.473 440.329,367.473L453.289,367.473C454.825,367.473 455.881,368.049 456.457,369.201L476.041,404.337L495.625,369.201C496.201,368.049 497.257,367.473 498.793,367.473L511.753,367.473C512.809,367.473 513.745,367.905 514.561,368.769C515.377,369.633 515.785,370.593 515.785,371.649L515.785,446.673C515.785,447.729 515.377,448.689 514.561,449.553C513.745,450.417 512.809,450.849 511.753,450.849L500.521,450.849C499.465,450.849 498.529,450.417 497.713,449.553C496.897,448.689 496.489,447.729 496.489,446.673L496.489,406.497L478.921,438.609C478.537,439.377 477.913,439.761 477.049,439.761Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
<path d="M574.969,433.425L583.177,433.425C589.801,433.425 593.113,430.641 593.113,425.073C593.113,419.697 589.801,417.009 583.177,417.009L574.969,417.009L574.969,433.425ZM583.033,384.897L574.969,384.897L574.969,399.297L583.033,399.297C583.609,399.297 584.305,399.249 585.121,399.153C585.937,399.057 587.041,398.409 588.433,397.209C589.825,396.009 590.521,394.305 590.521,392.097C590.521,389.889 589.897,388.185 588.649,386.985C587.401,385.785 586.153,385.137 584.905,385.041L583.033,384.897ZM559.705,367.473L583.177,367.473C583.945,367.473 584.905,367.521 586.057,367.617C587.209,367.713 589.321,368.121 592.393,368.841C595.465,369.561 598.177,370.593 600.529,371.937C602.881,373.281 605.017,375.369 606.937,378.201C608.857,381.033 609.817,384.417 609.817,388.353C609.817,391.329 609.409,393.993 608.593,396.345C607.777,398.697 606.817,400.521 605.713,401.817C604.609,403.113 603.433,404.217 602.185,405.129C600.937,406.041 599.929,406.641 599.161,406.929C598.393,407.217 597.913,407.361 597.721,407.361C598.969,407.745 600.145,408.225 601.249,408.801C602.353,409.377 604.009,410.385 606.217,411.825C608.425,413.265 610.177,415.257 611.473,417.801C612.769,420.345 613.417,423.249 613.417,426.513C613.417,442.737 602.185,450.849 579.721,450.849L559.705,450.849C558.649,450.849 557.713,450.417 556.897,449.553C556.081,448.689 555.673,447.729 555.673,446.673L555.673,371.649C555.673,370.593 556.081,369.633 556.897,368.769C557.713,367.905 558.649,367.473 559.705,367.473Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
<path d="M657.337,452.433C656.953,452.433 656.377,452.409 655.609,452.361C654.841,452.313 653.377,452.097 651.217,451.713C649.057,451.329 647.017,450.753 645.097,449.985C643.177,449.217 641.017,447.993 638.617,446.313C636.217,444.633 634.153,442.665 632.425,440.409C630.697,438.153 629.257,435.153 628.105,431.409C626.953,427.665 626.377,423.489 626.377,418.881L626.377,371.649C626.377,370.593 626.785,369.633 627.601,368.769C628.417,367.905 629.353,367.473 630.409,367.473L641.785,367.473C642.841,367.473 643.777,367.905 644.593,368.769C645.409,369.633 645.817,370.593 645.817,371.649L645.817,421.905C645.817,422.289 645.841,422.769 645.889,423.345C645.937,423.921 646.177,424.929 646.609,426.369C647.041,427.809 647.617,429.105 648.337,430.257C649.057,431.409 650.209,432.441 651.793,433.353C653.377,434.265 655.225,434.721 657.337,434.721C660.697,434.721 663.337,433.665 665.257,431.553C667.177,429.441 668.233,427.329 668.425,425.217L668.857,421.905L668.857,371.649C668.857,370.593 669.265,369.633 670.081,368.769C670.897,367.905 671.833,367.473 672.889,367.473L684.265,367.473C685.321,367.473 686.257,367.905 687.073,368.769C687.889,369.633 688.297,370.593 688.297,371.649L688.297,418.881C688.297,423.489 687.721,427.665 686.569,431.409C685.417,435.153 683.929,438.153 682.105,440.409C680.281,442.665 678.265,444.633 676.057,446.313C673.849,447.993 671.665,449.217 669.505,449.985C667.345,450.753 665.353,451.329 663.529,451.713C661.705,452.097 660.217,452.289 659.065,452.289L657.337,452.433Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
<path d="M704.713,367.473L757.561,367.473C758.617,367.473 759.577,367.881 760.441,368.697C761.305,369.513 761.737,370.449 761.737,371.505L761.737,381.153C761.737,382.209 761.305,383.145 760.441,383.961C759.577,384.777 758.617,385.185 757.561,385.185L740.857,385.185L740.857,446.673C740.857,447.729 740.449,448.689 739.633,449.553C738.817,450.417 737.881,450.849 736.825,450.849L725.449,450.849C724.393,450.849 723.457,450.417 722.641,449.553C721.825,448.689 721.417,447.729 721.417,446.673L721.417,385.185L704.713,385.185C703.657,385.185 702.697,384.777 701.833,383.961C700.969,383.145 700.537,382.209 700.537,381.153L700.537,371.505C700.537,370.449 700.969,369.513 701.833,368.697C702.697,367.881 703.657,367.473 704.713,367.473Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
<path d="M805.081,452.433C793.081,452.433 783.601,448.329 776.641,440.121C769.681,431.913 766.201,421.617 766.201,409.233C766.201,396.849 769.681,386.553 776.641,378.345C783.601,370.137 793.081,366.033 805.081,366.033C825.721,366.033 838.249,375.633 842.665,394.833C842.473,395.889 841.969,396.801 841.153,397.569C840.337,398.337 839.401,398.721 838.345,398.721L825.961,398.721C824.521,398.721 823.417,398.001 822.649,396.561C820.345,388.017 814.489,383.745 805.081,383.745C798.649,383.745 793.897,386.097 790.825,390.801C787.753,395.505 786.217,401.649 786.217,409.233C786.217,416.721 787.753,422.841 790.825,427.593C793.897,432.345 798.649,434.721 805.081,434.721C814.489,434.721 820.345,430.449 822.649,421.905C823.417,420.465 824.521,419.745 825.961,419.745L838.345,419.745C839.401,419.745 840.337,420.129 841.153,420.897C841.969,421.665 842.473,422.577 842.665,423.633C838.249,442.833 825.721,452.433 805.081,452.433Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
<path d="M896.521,446.673L896.521,421.905L874.633,421.905L874.633,446.673C874.633,447.729 874.225,448.689 873.409,449.553C872.593,450.417 871.657,450.849 870.601,450.849L859.369,450.849C858.313,450.849 857.377,450.417 856.561,449.553C855.745,448.689 855.337,447.729 855.337,446.673L855.337,371.649C855.337,370.593 855.745,369.633 856.561,368.769C857.377,367.905 858.313,367.473 859.369,367.473L870.601,367.473C871.657,367.473 872.593,367.905 873.409,368.769C874.225,369.633 874.633,370.593 874.633,371.649L874.633,404.337L896.521,404.337L896.521,371.649C896.521,370.593 896.929,369.633 897.745,368.769C898.561,367.905 899.497,367.473 900.553,367.473L911.785,367.473C912.841,367.473 913.777,367.905 914.593,368.769C915.409,369.633 915.817,370.593 915.817,371.649L915.817,446.673C915.817,447.729 915.409,448.689 914.593,449.553C913.777,450.417 912.841,450.849 911.785,450.849L900.553,450.849C899.497,450.849 898.561,450.417 897.745,449.553C896.929,448.689 896.521,447.729 896.521,446.673Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
<path d="M932.521,446.817L932.521,371.505C932.521,370.449 932.953,369.513 933.817,368.697C934.681,367.881 935.641,367.473 936.697,367.473L977.881,367.473C978.937,367.473 979.897,367.881 980.761,368.697C981.625,369.513 982.057,370.449 982.057,371.505L982.057,381.153C982.057,382.209 981.625,383.145 980.761,383.961C979.897,384.777 978.937,385.185 977.881,385.185L951.817,385.185L951.817,401.025L974.857,401.025C975.913,401.025 976.873,401.433 977.737,402.249C978.601,403.065 979.033,404.001 979.033,405.057L979.033,414.705C979.033,415.761 978.601,416.697 977.737,417.513C976.873,418.329 975.913,418.737 974.857,418.737L951.817,418.737L951.817,433.137L977.881,433.137C978.937,433.137 979.897,433.545 980.761,434.361C981.625,435.177 982.057,436.113 982.057,437.169L982.057,446.817C982.057,447.873 981.625,448.809 980.761,449.625C979.897,450.441 978.937,450.849 977.881,450.849L936.697,450.849C935.641,450.849 934.681,450.441 933.817,449.625C932.953,448.809 932.521,447.873 932.521,446.817Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
<path d="M1022.23,409.953C1025.79,409.953 1028.66,408.945 1030.87,406.929C1033.08,404.913 1034.18,401.745 1034.18,397.425C1034.18,393.105 1033.08,389.913 1030.87,387.849C1028.66,385.785 1025.79,384.753 1022.23,384.753L1015.75,384.753L1015.75,409.953L1022.23,409.953ZM1037.64,449.409L1025.98,426.945L1022.67,427.233L1015.75,427.233L1015.75,446.673C1015.75,447.729 1015.35,448.689 1014.53,449.553C1013.71,450.417 1012.78,450.849 1011.72,450.849L1000.49,450.849C999.433,450.849 998.497,450.417 997.681,449.553C996.865,448.689 996.457,447.729 996.457,446.673L996.457,371.505C996.457,370.449 996.889,369.513 997.753,368.697C998.617,367.881 999.577,367.473 1000.63,367.473L1022.67,367.473C1023.14,367.473 1023.77,367.497 1024.54,367.545C1025.31,367.593 1026.82,367.809 1029.07,368.193C1031.33,368.577 1033.46,369.105 1035.48,369.777C1037.5,370.449 1039.75,371.553 1042.25,373.089C1044.75,374.625 1046.88,376.401 1048.66,378.417C1050.43,380.433 1051.94,383.097 1053.19,386.409C1054.44,389.721 1055.07,393.393 1055.07,397.425C1055.07,407.505 1051.51,415.137 1044.41,420.321L1058.09,446.817C1058.09,447.969 1057.73,448.929 1057.01,449.697C1056.29,450.465 1055.35,450.849 1054.2,450.849L1040.81,450.849C1039.46,450.849 1038.41,450.369 1037.64,449.409Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,4,0)">
|
||||||
|
<path d="M947.978,337.608L901.718,337.608C900.662,337.608 899.702,337.2 898.838,336.384C897.974,335.568 897.542,334.632 897.542,333.576L897.542,322.344C897.542,321.288 897.974,320.352 898.838,319.536C899.702,318.72 900.662,318.312 901.718,318.312L967.817,318.312C971.199,318.312 974.203,319.676 976.827,322.403C979.554,325.027 980.918,328.031 980.918,331.413L980.918,397.512C980.918,398.568 980.51,399.528 979.694,400.392C978.878,401.256 977.942,401.688 976.886,401.688L965.654,401.688C964.598,401.688 963.662,401.256 962.846,400.392C962.03,399.528 961.622,398.568 961.622,397.512L961.622,351.252L912.36,400.515C911.613,401.262 910.646,401.652 909.458,401.686C908.27,401.72 907.302,401.363 906.556,400.617L898.614,392.674C897.867,391.928 897.51,390.96 897.544,389.772C897.578,388.585 897.969,387.617 898.715,386.87L947.978,337.608Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
BIN
src/views/logo-vertical.afdesign
Normal file
BIN
src/views/logo-vertical.afdesign
Normal file
Binary file not shown.
41
src/views/logo-vertical.svg
Normal file
41
src/views/logo-vertical.svg
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 503 301" preserveAspectRatio="xMinYMin meet" version="1.1" xmlns="http://www.w3.org/2000/svg" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(1,0,0,1,-91.7237,-144)">
|
||||||
|
<g transform="matrix(1,0,0,1,-186.893,-46.2326)">
|
||||||
|
<g transform="matrix(1,0,0,1,0,-68.8)">
|
||||||
|
<path d="M282.793,367.473L335.641,367.473C336.697,367.473 337.657,367.881 338.521,368.697C339.385,369.513 339.817,370.449 339.817,371.505L339.817,381.153C339.817,382.209 339.385,383.145 338.521,383.961C337.657,384.777 336.697,385.185 335.641,385.185L318.937,385.185L318.937,446.673C318.937,447.729 318.529,448.689 317.713,449.553C316.897,450.417 315.961,450.849 314.905,450.849L303.529,450.849C302.473,450.849 301.537,450.417 300.721,449.553C299.905,448.689 299.497,447.729 299.497,446.673L299.497,385.185L282.793,385.185C281.737,385.185 280.777,384.777 279.913,383.961C279.049,383.145 278.617,382.209 278.617,381.153L278.617,371.505C278.617,370.449 279.049,369.513 279.913,368.697C280.777,367.881 281.737,367.473 282.793,367.473Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,-68.8)">
|
||||||
|
<path d="M354.937,378.273C361.657,370.113 371.017,366.033 383.017,366.033C395.017,366.033 404.353,370.113 411.025,378.273C417.697,386.433 421.033,396.753 421.033,409.233C421.033,421.617 417.673,431.913 410.953,440.121C404.233,448.329 394.921,452.433 383.017,452.433C371.113,452.433 361.801,448.329 355.081,440.121C348.361,431.913 345.001,421.617 345.001,409.233C345.001,396.753 348.313,386.433 354.937,378.273ZM365.017,409.233C365.017,426.225 371.017,434.721 383.017,434.721C395.017,434.721 401.017,426.225 401.017,409.233C401.017,392.241 395.017,383.745 383.017,383.745C371.017,383.745 365.017,392.241 365.017,409.233Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,-68.8)">
|
||||||
|
<path d="M477.049,439.761L475.321,439.761C474.265,439.761 473.545,439.377 473.161,438.609L455.593,406.497L455.593,446.673C455.593,447.729 455.185,448.689 454.369,449.553C453.553,450.417 452.617,450.849 451.561,450.849L440.329,450.849C439.273,450.849 438.337,450.417 437.521,449.553C436.705,448.689 436.297,447.729 436.297,446.673L436.297,371.649C436.297,370.593 436.705,369.633 437.521,368.769C438.337,367.905 439.273,367.473 440.329,367.473L453.289,367.473C454.825,367.473 455.881,368.049 456.457,369.201L476.041,404.337L495.625,369.201C496.201,368.049 497.257,367.473 498.793,367.473L511.753,367.473C512.809,367.473 513.745,367.905 514.561,368.769C515.377,369.633 515.785,370.593 515.785,371.649L515.785,446.673C515.785,447.729 515.377,448.689 514.561,449.553C513.745,450.417 512.809,450.849 511.753,450.849L500.521,450.849C499.465,450.849 498.529,450.417 497.713,449.553C496.897,448.689 496.489,447.729 496.489,446.673L496.489,406.497L478.921,438.609C478.537,439.377 477.913,439.761 477.049,439.761Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,-277.056,38)">
|
||||||
|
<path d="M574.969,433.425L583.177,433.425C589.801,433.425 593.113,430.641 593.113,425.073C593.113,419.697 589.801,417.009 583.177,417.009L574.969,417.009L574.969,433.425ZM583.033,384.897L574.969,384.897L574.969,399.297L583.033,399.297C583.609,399.297 584.305,399.249 585.121,399.153C585.937,399.057 587.041,398.409 588.433,397.209C589.825,396.009 590.521,394.305 590.521,392.097C590.521,389.889 589.897,388.185 588.649,386.985C587.401,385.785 586.153,385.137 584.905,385.041L583.033,384.897ZM559.705,367.473L583.177,367.473C583.945,367.473 584.905,367.521 586.057,367.617C587.209,367.713 589.321,368.121 592.393,368.841C595.465,369.561 598.177,370.593 600.529,371.937C602.881,373.281 605.017,375.369 606.937,378.201C608.857,381.033 609.817,384.417 609.817,388.353C609.817,391.329 609.409,393.993 608.593,396.345C607.777,398.697 606.817,400.521 605.713,401.817C604.609,403.113 603.433,404.217 602.185,405.129C600.937,406.041 599.929,406.641 599.161,406.929C598.393,407.217 597.913,407.361 597.721,407.361C598.969,407.745 600.145,408.225 601.249,408.801C602.353,409.377 604.009,410.385 606.217,411.825C608.425,413.265 610.177,415.257 611.473,417.801C612.769,420.345 613.417,423.249 613.417,426.513C613.417,442.737 602.185,450.849 579.721,450.849L559.705,450.849C558.649,450.849 557.713,450.417 556.897,449.553C556.081,448.689 555.673,447.729 555.673,446.673L555.673,371.649C555.673,370.593 556.081,369.633 556.897,368.769C557.713,367.905 558.649,367.473 559.705,367.473Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,-277.056,38)">
|
||||||
|
<path d="M657.337,452.433C656.953,452.433 656.377,452.409 655.609,452.361C654.841,452.313 653.377,452.097 651.217,451.713C649.057,451.329 647.017,450.753 645.097,449.985C643.177,449.217 641.017,447.993 638.617,446.313C636.217,444.633 634.153,442.665 632.425,440.409C630.697,438.153 629.257,435.153 628.105,431.409C626.953,427.665 626.377,423.489 626.377,418.881L626.377,371.649C626.377,370.593 626.785,369.633 627.601,368.769C628.417,367.905 629.353,367.473 630.409,367.473L641.785,367.473C642.841,367.473 643.777,367.905 644.593,368.769C645.409,369.633 645.817,370.593 645.817,371.649L645.817,421.905C645.817,422.289 645.841,422.769 645.889,423.345C645.937,423.921 646.177,424.929 646.609,426.369C647.041,427.809 647.617,429.105 648.337,430.257C649.057,431.409 650.209,432.441 651.793,433.353C653.377,434.265 655.225,434.721 657.337,434.721C660.697,434.721 663.337,433.665 665.257,431.553C667.177,429.441 668.233,427.329 668.425,425.217L668.857,421.905L668.857,371.649C668.857,370.593 669.265,369.633 670.081,368.769C670.897,367.905 671.833,367.473 672.889,367.473L684.265,367.473C685.321,367.473 686.257,367.905 687.073,368.769C687.889,369.633 688.297,370.593 688.297,371.649L688.297,418.881C688.297,423.489 687.721,427.665 686.569,431.409C685.417,435.153 683.929,438.153 682.105,440.409C680.281,442.665 678.265,444.633 676.057,446.313C673.849,447.993 671.665,449.217 669.505,449.985C667.345,450.753 665.353,451.329 663.529,451.713C661.705,452.097 660.217,452.289 659.065,452.289L657.337,452.433Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,-277.056,38)">
|
||||||
|
<path d="M704.713,367.473L757.561,367.473C758.617,367.473 759.577,367.881 760.441,368.697C761.305,369.513 761.737,370.449 761.737,371.505L761.737,381.153C761.737,382.209 761.305,383.145 760.441,383.961C759.577,384.777 758.617,385.185 757.561,385.185L740.857,385.185L740.857,446.673C740.857,447.729 740.449,448.689 739.633,449.553C738.817,450.417 737.881,450.849 736.825,450.849L725.449,450.849C724.393,450.849 723.457,450.417 722.641,449.553C721.825,448.689 721.417,447.729 721.417,446.673L721.417,385.185L704.713,385.185C703.657,385.185 702.697,384.777 701.833,383.961C700.969,383.145 700.537,382.209 700.537,381.153L700.537,371.505C700.537,370.449 700.969,369.513 701.833,368.697C702.697,367.881 703.657,367.473 704.713,367.473Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,-277.056,38)">
|
||||||
|
<path d="M805.081,452.433C793.081,452.433 783.601,448.329 776.641,440.121C769.681,431.913 766.201,421.617 766.201,409.233C766.201,396.849 769.681,386.553 776.641,378.345C783.601,370.137 793.081,366.033 805.081,366.033C825.721,366.033 838.249,375.633 842.665,394.833C842.473,395.889 841.969,396.801 841.153,397.569C840.337,398.337 839.401,398.721 838.345,398.721L825.961,398.721C824.521,398.721 823.417,398.001 822.649,396.561C820.345,388.017 814.489,383.745 805.081,383.745C798.649,383.745 793.897,386.097 790.825,390.801C787.753,395.505 786.217,401.649 786.217,409.233C786.217,416.721 787.753,422.841 790.825,427.593C793.897,432.345 798.649,434.721 805.081,434.721C814.489,434.721 820.345,430.449 822.649,421.905C823.417,420.465 824.521,419.745 825.961,419.745L838.345,419.745C839.401,419.745 840.337,420.129 841.153,420.897C841.969,421.665 842.473,422.577 842.665,423.633C838.249,442.833 825.721,452.433 805.081,452.433Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,-277.056,38)">
|
||||||
|
<path d="M896.521,446.673L896.521,421.905L874.633,421.905L874.633,446.673C874.633,447.729 874.225,448.689 873.409,449.553C872.593,450.417 871.657,450.849 870.601,450.849L859.369,450.849C858.313,450.849 857.377,450.417 856.561,449.553C855.745,448.689 855.337,447.729 855.337,446.673L855.337,371.649C855.337,370.593 855.745,369.633 856.561,368.769C857.377,367.905 858.313,367.473 859.369,367.473L870.601,367.473C871.657,367.473 872.593,367.905 873.409,368.769C874.225,369.633 874.633,370.593 874.633,371.649L874.633,404.337L896.521,404.337L896.521,371.649C896.521,370.593 896.929,369.633 897.745,368.769C898.561,367.905 899.497,367.473 900.553,367.473L911.785,367.473C912.841,367.473 913.777,367.905 914.593,368.769C915.409,369.633 915.817,370.593 915.817,371.649L915.817,446.673C915.817,447.729 915.409,448.689 914.593,449.553C913.777,450.417 912.841,450.849 911.785,450.849L900.553,450.849C899.497,450.849 898.561,450.417 897.745,449.553C896.929,448.689 896.521,447.729 896.521,446.673Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,-277.056,38)">
|
||||||
|
<path d="M932.521,446.817L932.521,371.505C932.521,370.449 932.953,369.513 933.817,368.697C934.681,367.881 935.641,367.473 936.697,367.473L977.881,367.473C978.937,367.473 979.897,367.881 980.761,368.697C981.625,369.513 982.057,370.449 982.057,371.505L982.057,381.153C982.057,382.209 981.625,383.145 980.761,383.961C979.897,384.777 978.937,385.185 977.881,385.185L951.817,385.185L951.817,401.025L974.857,401.025C975.913,401.025 976.873,401.433 977.737,402.249C978.601,403.065 979.033,404.001 979.033,405.057L979.033,414.705C979.033,415.761 978.601,416.697 977.737,417.513C976.873,418.329 975.913,418.737 974.857,418.737L951.817,418.737L951.817,433.137L977.881,433.137C978.937,433.137 979.897,433.545 980.761,434.361C981.625,435.177 982.057,436.113 982.057,437.169L982.057,446.817C982.057,447.873 981.625,448.809 980.761,449.625C979.897,450.441 978.937,450.849 977.881,450.849L936.697,450.849C935.641,450.849 934.681,450.441 933.817,449.625C932.953,448.809 932.521,447.873 932.521,446.817Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,-277.056,38)">
|
||||||
|
<path d="M1022.23,409.953C1025.79,409.953 1028.66,408.945 1030.87,406.929C1033.08,404.913 1034.18,401.745 1034.18,397.425C1034.18,393.105 1033.08,389.913 1030.87,387.849C1028.66,385.785 1025.79,384.753 1022.23,384.753L1015.75,384.753L1015.75,409.953L1022.23,409.953ZM1037.64,449.409L1025.98,426.945L1022.67,427.233L1015.75,427.233L1015.75,446.673C1015.75,447.729 1015.35,448.689 1014.53,449.553C1013.71,450.417 1012.78,450.849 1011.72,450.849L1000.49,450.849C999.433,450.849 998.497,450.417 997.681,449.553C996.865,448.689 996.457,447.729 996.457,446.673L996.457,371.505C996.457,370.449 996.889,369.513 997.753,368.697C998.617,367.881 999.577,367.473 1000.63,367.473L1022.67,367.473C1023.14,367.473 1023.77,367.497 1024.54,367.545C1025.31,367.593 1026.82,367.809 1029.07,368.193C1031.33,368.577 1033.46,369.105 1035.48,369.777C1037.5,370.449 1039.75,371.553 1042.25,373.089C1044.75,374.625 1046.88,376.401 1048.66,378.417C1050.43,380.433 1051.94,383.097 1053.19,386.409C1054.44,389.721 1055.07,393.393 1055.07,397.425C1055.07,407.505 1051.51,415.137 1044.41,420.321L1058.09,446.817C1058.09,447.969 1057.73,448.929 1057.01,449.697C1056.29,450.465 1055.35,450.849 1054.2,450.849L1040.81,450.849C1039.46,450.849 1038.41,450.369 1037.64,449.409Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,-805.818,-174.312)">
|
||||||
|
<path d="M947.978,337.608L901.718,337.608C900.662,337.608 899.702,337.2 898.838,336.384C897.974,335.568 897.542,334.632 897.542,333.576L897.542,322.344C897.542,321.288 897.974,320.352 898.838,319.536C899.702,318.72 900.662,318.312 901.718,318.312L967.817,318.312C971.199,318.312 974.203,319.676 976.827,322.403C979.554,325.027 980.918,328.031 980.918,331.413L980.918,397.512C980.918,398.568 980.51,399.528 979.694,400.392C978.878,401.256 977.942,401.688 976.886,401.688L965.654,401.688C964.598,401.688 963.662,401.256 962.846,400.392C962.03,399.528 961.622,398.568 961.622,397.512L961.622,351.252L912.36,400.515C911.613,401.262 910.646,401.652 909.458,401.686C908.27,401.72 907.302,401.363 906.556,400.617L898.614,392.674C897.867,391.928 897.51,390.96 897.544,389.772C897.578,388.585 897.969,387.617 898.715,386.87L947.978,337.608Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 13 KiB |
Loading…
x
Reference in New Issue
Block a user