diff --git a/src-react/App.scss b/src-react/App.scss
new file mode 100644
index 000000000..c0b93891e
--- /dev/null
+++ b/src-react/App.scss
@@ -0,0 +1,24 @@
+@import "./shared/scss/media";
+@import "./shared/scss/theme_variables";
+
+.body-cover {
+ width: 100%;
+ z-index: 0;
+ position: fixed;
+ height: 100%;
+}
+
+.wrapper {
+ position: relative;
+ width: 85%;
+ min-height: 80px;
+ margin: 0 auto;
+ font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
+ font-size: 15px;
+ height: 100%;
+ line-height: 1.3;
+
+ @media #{$mobile-only} {
+ width: 100%;
+ }
+}
diff --git a/src-react/App.tsx b/src-react/App.tsx
index 63d246b24..11adefa16 100644
--- a/src-react/App.tsx
+++ b/src-react/App.tsx
@@ -1,5 +1,21 @@
+import { useSettings } from './shared/settings/SettingsContext';
+import Footer from './core/footer/Footer';
+import Header from './core/header/Header';
+import './App.scss';
+
function App() {
- return
Angular 2 HN — React migration in progress
;
+ const { settings } = useSettings();
+
+ return (
+
+ );
}
export default App;
diff --git a/src-react/core/footer/Footer.scss b/src-react/core/footer/Footer.scss
new file mode 100644
index 000000000..8adac775d
--- /dev/null
+++ b/src-react/core/footer/Footer.scss
@@ -0,0 +1,23 @@
+@import "../../shared/scss/media";
+@import "../../shared/scss/theme_variables";
+
+#footer {
+ position: relative;
+ padding: 10px;
+ height: 60px;
+ letter-spacing: 0.7px;
+ text-align: center;
+
+ a {
+ font-weight: bold;
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ @media #{$mobile-only} {
+ display: none;
+ }
+}
diff --git a/src-react/core/footer/Footer.tsx b/src-react/core/footer/Footer.tsx
new file mode 100644
index 000000000..188cdb643
--- /dev/null
+++ b/src-react/core/footer/Footer.tsx
@@ -0,0 +1,16 @@
+import './Footer.scss';
+
+function Footer() {
+ return (
+
+ );
+}
+
+export default Footer;
diff --git a/src-react/core/header/Header.scss b/src-react/core/header/Header.scss
new file mode 100644
index 000000000..75a262e76
--- /dev/null
+++ b/src-react/core/header/Header.scss
@@ -0,0 +1,149 @@
+@import "../../shared/scss/media";
+@import "../../shared/scss/theme_variables";
+
+#header {
+ color: #fff;
+ padding: 6px 0;
+ line-height: 18px;
+ vertical-align: middle;
+ position: relative;
+ z-index: 1;
+ width: 100%;
+
+ @media #{$mobile-only} {
+ height: 50px;
+ position: fixed;
+ top: 0;
+ }
+
+ a {
+ display: inline;
+ }
+}
+
+.home-link {
+ width: 50px;
+ height: 66px;
+}
+
+.logo-inner {
+ width: 32px;
+ position: absolute;
+ left: 17px;
+ top: 18px;
+ z-index: -1;
+ height: 32px;
+ border-radius: 50%;
+
+ @media #{$mobile-only} {
+ left: 16px;
+ top: 12px;
+ }
+}
+
+.logo {
+ width: 50px;
+ padding: 3px 8px 0;
+
+ @media #{$mobile-only} {
+ width: 45px;
+ padding: 0 0 0 10px;
+ }
+}
+
+h1 {
+ font-weight: normal;
+ display: inline-block;
+ vertical-align:middle;
+ margin: 0;
+ font-size: 16px;
+
+ a {
+ color: #fff;
+ text-decoration: none;
+ }
+}
+
+.name {
+ margin-right: 30px;
+ margin-bottom: 2px;
+
+ @media #{$mobile-only} {
+ display: none;
+ }
+}
+
+.header-text {
+ position: absolute;
+ width: inherit;
+ height: 20px;
+ left: 10px;
+ top: 27px;
+ z-index: -1;
+
+ @media #{$mobile-only} {
+ top: 22px;
+ }
+}
+
+.left {
+ position: absolute;
+ left: 60px;
+ font-size: 16px;
+
+ @media #{$mobile-only} {
+ width: 100%;
+ left: 0;
+ }
+}
+
+.header-nav {
+ display: inline-block;
+ margin-left: 20px;
+
+ @media #{$mobile-only} {
+ margin-left: 60px;
+ }
+
+ a {
+ color: hsla(0,0%,100%,.9);
+ text-decoration: none;
+ margin: 0 5px;
+ letter-spacing: 1.8px;
+
+ &:hover {
+ color: #fff;
+ }
+ }
+
+ .active {
+ color: #fff;
+ }
+}
+
+.info {
+ position: absolute;
+ top: 0;
+ right: 20px;
+ height: 100%;
+
+ @media #{$mobile-only} {
+ right: 10px;
+ }
+
+ img {
+ opacity: 0.8;
+ width: 25px;
+ margin-top: 21.5px;
+ display: block;
+
+ &:hover {
+ opacity: 1;
+ cursor: pointer;
+ }
+
+ @media #{$mobile-only} {
+ margin-top: 15px;
+ }
+ }
+}
diff --git a/src-react/core/header/Header.tsx b/src-react/core/header/Header.tsx
new file mode 100644
index 000000000..3216fce19
--- /dev/null
+++ b/src-react/core/header/Header.tsx
@@ -0,0 +1,53 @@
+import { NavLink } from 'react-router-dom';
+
+import { useSettings } from '../../shared/settings/SettingsContext';
+import Settings from '../settings/Settings';
+import './Header.scss';
+
+function Header() {
+ const { settings, toggleSettings } = useSettings();
+ const scrollTop = () => window.scrollTo(0, 0);
+
+ return (
+
+
+ {settings.showSettings && }
+
+ );
+}
+
+export default Header;
diff --git a/src-react/core/settings/Settings.scss b/src-react/core/settings/Settings.scss
new file mode 100644
index 000000000..689da6584
--- /dev/null
+++ b/src-react/core/settings/Settings.scss
@@ -0,0 +1,74 @@
+@import "../../shared/scss/media";
+@import "../../shared/scss/theme_variables";
+
+.overlay {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: rgba(0, 0, 0, 0.7);
+ opacity: 1;
+ z-index: 1;
+}
+
+.popup {
+ margin: 70px auto;
+ padding: 30px;
+ border-radius: 5px;
+ width: 30%;
+ position: relative;
+ h1 {
+ margin-top: 0;
+ margin-bottom: 0px;
+ color: #fff;
+ text-align: center;
+ letter-spacing: 1px;
+ }
+ h2 {
+ padding-top: 10px;
+ }
+ hr {
+ width: 40%;
+ margin-bottom: 20px;
+ }
+ .close {
+ position: absolute;
+ top: 12px;
+ right: 20px;
+ font-size: 30px;
+ font-weight: bold;
+ text-decoration: none;
+ color: rgba(255,255,255,0.8);
+ &:hover {
+ color: #fff;
+ cursor: pointer;
+ }
+ }
+ .content {
+ max-height: 30%;
+ color: #fff;
+ letter-spacing: 1px;
+ overflow: auto;
+ }
+ input[type=number] {
+ display: block;
+ width: 80%;
+ height: 20px;
+ margin-bottom: 15px;
+ border-radius: 5px;
+ padding: 2px;
+ }
+}
+
+.control-section {
+ margin-bottom: 15px;
+ padding-bottom: 15px;
+ border-bottom: 1px solid white;
+}
+
+@media screen and (max-width: 700px) {
+ .box, .popup {
+ width: 70%;
+ }
+}
diff --git a/src-react/core/settings/Settings.tsx b/src-react/core/settings/Settings.tsx
new file mode 100644
index 000000000..48bad2982
--- /dev/null
+++ b/src-react/core/settings/Settings.tsx
@@ -0,0 +1,117 @@
+import type { ChangeEvent, KeyboardEvent } from 'react';
+
+import { useSettings } from '../../shared/settings/SettingsContext';
+import './Settings.scss';
+
+function Settings() {
+ const {
+ settings,
+ toggleSettings,
+ toggleOpenLinksInNewTab,
+ setTheme,
+ setFont,
+ setSpacing,
+ } = useSettings();
+
+ const changeTitleFont = (event: KeyboardEvent | ChangeEvent) => {
+ setFont(event.currentTarget.value);
+ };
+ const changeSpacing = (event: KeyboardEvent | ChangeEvent) => {
+ setSpacing(event.currentTarget.value);
+ };
+
+ return (
+
+ );
+}
+
+export default Settings;
diff --git a/src-react/main.tsx b/src-react/main.tsx
index f3092f2e6..f0e26eb5a 100644
--- a/src-react/main.tsx
+++ b/src-react/main.tsx
@@ -1,10 +1,17 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
import App from './App';
+import { SettingsProvider } from './shared/settings/SettingsContext';
+import './styles.scss';
createRoot(document.getElementById('root')!).render(
-
+
+
+
+
+
,
);
diff --git a/src-react/shared/components/error-message/ErrorMessage.scss b/src-react/shared/components/error-message/ErrorMessage.scss
new file mode 100644
index 000000000..eb5cc01e8
--- /dev/null
+++ b/src-react/shared/components/error-message/ErrorMessage.scss
@@ -0,0 +1,115 @@
+@import "../../scss/media";
+@import "../../scss/theme_variables";
+
+.error-section {
+ height: 300px;
+ margin: 200px;
+
+ @media #{$mobile-only} {
+ height: 0;
+ display: block;
+ position: relative;
+ margin: 30vh 0;
+ }
+
+ p {
+ text-align: center;
+ padding: 0 25px;
+
+ &.strong {
+ margin-top: 25px;
+ font-weight: bold;
+ }
+ }
+
+ .skull {
+ width: $skull-size;
+ height: $skull-size;
+ position: relative;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ margin: auto;
+
+ .head {
+ width: 100%;
+ height: 75%;
+ border-radius: 15% / 20%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ &:before, &:after {
+ content: "";
+ position: absolute;
+ border-radius: 50%;
+ width: 20%;
+ height: 30%;
+ bottom: 10%;
+ }
+ &:before {
+ left: 10%;
+ }
+ &:after {
+ right: 10%;
+ }
+ .crack {
+ width: 10%;
+ height: 10%;
+ position: absolute;
+ top: 0;
+ right: 25%;
+ transform: skew(-15deg);
+
+ &:before {
+ content: "";
+ position: absolute;
+ top: 100%;
+ left: $skull-size / 15;
+ border-right: $skull-size / 20 solid transparent;
+ border-left: $skull-size / 40 solid transparent;
+ }
+ }
+ }
+ .mouth {
+ width: 40%;
+ height: 25%;
+ position: absolute;
+ top: 75%;
+ left: 30%;
+ border-radius: 0 0 $skull-size / 10 $skull-size / 10;
+ &:before {
+ content: "";
+ position: absolute;
+ width: 15%;
+ height: 50%;
+ border-radius: 50% / 30%;
+ left: 42.5%;
+ top: -25%;
+ }
+ .teeth {
+ position: absolute;
+ bottom: 0;
+ left: 45%;
+ width: 10%;
+ height: 50%;
+ margin-bottom: -5%;
+ border-radius: 50% / 20%;
+
+ &:before, &:after {
+ content: "";
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ border-radius: 50% / 20%;
+ }
+ &:before {
+ left: -250%;
+ }
+ &:after {
+ right: -250%;
+ }
+ }
+ }
+ }
+}
diff --git a/src-react/shared/components/error-message/ErrorMessage.tsx b/src-react/shared/components/error-message/ErrorMessage.tsx
new file mode 100644
index 000000000..ac889ba5b
--- /dev/null
+++ b/src-react/shared/components/error-message/ErrorMessage.tsx
@@ -0,0 +1,24 @@
+import './ErrorMessage.scss';
+
+interface ErrorMessageProps {
+ message: string;
+}
+
+function ErrorMessage({ message }: ErrorMessageProps) {
+ return (
+
+
+
{message}
+
If you are offline viewing, you'll need to visit this page with a network connection first before it can work offline.
+
+ );
+}
+
+export default ErrorMessage;
diff --git a/src-react/shared/components/loader/Loader.scss b/src-react/shared/components/loader/Loader.scss
new file mode 100644
index 000000000..625d0fe8b
--- /dev/null
+++ b/src-react/shared/components/loader/Loader.scss
@@ -0,0 +1,109 @@
+@import "../../scss/media";
+@import "../../scss/theme_variables";
+
+.loader {
+ -webkit-animation: load1 1s infinite ease-in-out;
+ animation: load1 1s infinite ease-in-out;
+ width: 1em;
+ height: 4em;
+ &:before, &:after {
+ -webkit-animation: load1 1s infinite ease-in-out;
+ animation: load1 1s infinite ease-in-out;
+ width: 1em;
+ height: 4em;
+ }
+ &:before, &:after {
+ position: absolute;
+ top: 0;
+ content: '';
+ }
+ &:before {
+ left: -1.5em;
+ -webkit-animation-delay: -0.32s;
+ animation-delay: -0.32s;
+ }
+}
+
+.loading-section {
+ height: 70px;
+ margin: 40px 0 40px 40px;
+
+ @media #{$mobile-only} {
+ display: block;
+ position: relative;
+ margin: 45vh 0;
+ }
+}
+
+.loader {
+ text-indent: -9999em;
+ margin: 20px 20px;
+ position: relative;
+ font-size: 11px;
+ -webkit-transform: translateZ(0);
+ -ms-transform: translateZ(0);
+ transform: translateZ(0);
+ -webkit-animation-delay: -0.16s;
+ animation-delay: -0.16s;
+ &:after {
+ left: 1.5em;
+ }
+
+ @media #{$mobile-only} {
+ margin: 20px auto;
+ }
+}
+
+@-webkit-keyframes load1 {
+ 0%,
+ 80%,
+ 100% {
+ box-shadow: 0 0;
+ height: 2em;
+ }
+ 40% {
+ box-shadow: 0 -2em;
+ height: 3em;
+ }
+}
+
+@keyframes load1 {
+ 0%,
+ 80%,
+ 100% {
+ box-shadow: 0 0;
+ height: 2em;
+ }
+ 40% {
+ box-shadow: 0 -2em;
+ height: 3em;
+ }
+}
+
+@media #{$mobile-only} {
+ @-webkit-keyframes load1 {
+ 0%,
+ 80%,
+ 100% {
+ box-shadow: 0 0;
+ height: 4em;
+ }
+ 40% {
+ box-shadow: 0 -2em;
+ height: 5em;
+ }
+ }
+
+ @keyframes load1 {
+ 0%,
+ 80%,
+ 100% {
+ box-shadow: 0 0;
+ height: 3em;
+ }
+ 40% {
+ box-shadow: 0 -2em;
+ height: 4em;
+ }
+ }
+}
diff --git a/src-react/shared/components/loader/Loader.tsx b/src-react/shared/components/loader/Loader.tsx
new file mode 100644
index 000000000..c1f549ab8
--- /dev/null
+++ b/src-react/shared/components/loader/Loader.tsx
@@ -0,0 +1,11 @@
+import './Loader.scss';
+
+function Loader() {
+ return (
+
+ );
+}
+
+export default Loader;
diff --git a/src-react/shared/scss/_media.scss b/src-react/shared/scss/_media.scss
new file mode 100644
index 000000000..b04b71a92
--- /dev/null
+++ b/src-react/shared/scss/_media.scss
@@ -0,0 +1,3 @@
+$mobile-only: "only screen and (max-width : 768px)";
+$laptop-only: "only screen and (min-width : 769px)";
+$tablet-only: "only screen and (max-width : 1024px)";
diff --git a/src-react/shared/scss/_theme_variables.scss b/src-react/shared/scss/_theme_variables.scss
new file mode 100644
index 000000000..41dae5eea
--- /dev/null
+++ b/src-react/shared/scss/_theme_variables.scss
@@ -0,0 +1,37 @@
+$skull-size: 200px;
+
+// -------------------------------------------------------------------------------------------
+// Theme Engine
+// -------------------------------------------------------------------------------------------
+
+// Day theme colors
+$theme-day-body-background-color: #fff;
+$theme-day-wrapper-background-color: #f5f5f5;
+$theme-day-wrapper-mobile-background-color: #fff;
+$theme-day-text-color: #000;
+$theme-day-subtext-color: #696969;
+$theme-day-secondary-color: #b92b27;
+$theme-day-header-background-color: $theme-day-secondary-color;
+$theme-day-logo-inner: #fff;
+
+// Day theme extras
+$theme-day-border: 2px solid #b92b27;
+
+// Night theme colors
+$theme-night-body-background-color: #37474F;
+$theme-night-wrapper-background-color: #263238;
+$theme-night-wrapper-mobile-background-color: $theme-night-wrapper-background-color;
+$theme-night-text-color: rgba(255, 255, 255, 0.7);
+$theme-night-subtext-color: #999;
+$theme-night-secondary-color: #00c0ff;
+$theme-night-header-background-color: #263238;
+$theme-night-logo-inner: $theme-night-header-background-color;
+
+// Night theme extras
+$theme-night-border: 2px solid #00c0ff;
+
+// Black theme colors
+$theme-amoledblack-body-background-color: #000;
+$theme-amoledblack-text-color: rgba(255, 255, 255, 0.75);
+$theme-amoledblack-subtext-color: rgba(255, 255, 255, 0.5);
+$theme-amoledblack-secondary-color: rgba(255, 255, 255, 0.6);
diff --git a/src-react/shared/scss/_themes.scss b/src-react/shared/scss/_themes.scss
new file mode 100644
index 000000000..231ed7cf9
--- /dev/null
+++ b/src-react/shared/scss/_themes.scss
@@ -0,0 +1,245 @@
+@import "./media";
+@import "./theme_variables";
+
+/* ----------------------------------
+
+ Want a new theme? Add your new theme variables here!
+
+---------------------------------- */
+
+@mixin theme(
+ $name,
+ $body-background-color,
+ $wrapper-background-color,
+ $wrapper-mobile-background-color,
+ $wrapper-color,
+ $item-a-color,
+ $item-a-visited-color,
+ $header-background-color,
+ $subtext-color,
+ $secondary-link-color,
+ $logo-inner-color,
+ $border
+) {
+ .#{$name} {
+ .body-cover {
+ background: $body-background-color;
+
+ @media #{$mobile-only} {
+ background: $wrapper-mobile-background-color;
+ }
+ }
+
+ .wrapper {
+ background: $wrapper-background-color;
+ color: $wrapper-color;
+
+ @media #{$mobile-only} {
+ background: $wrapper-mobile-background-color;
+ }
+
+ a {
+ color: $item-a-color;
+
+ &:visited {
+ color: $item-a-visited-color;
+ }
+ }
+
+ #header {
+ background: $header-background-color;
+ border-bottom: $border;
+ }
+
+ .logo-inner {
+ background: $logo-inner-color;
+ }
+
+ .nav {
+ a {
+ color: $secondary-link-color;
+
+ @media #{$mobile-only} {
+ color: $secondary-link-color;
+ }
+ }
+ }
+
+ #footer {
+ border-top: $border;
+
+ a {
+ color: $secondary-link-color;
+ }
+ }
+
+ .subtext, .subtext-palm, .subtext-laptop, .domain, .meta, .deleted-meta{
+ color: $subtext-color;
+
+ a {
+ color: $secondary-link-color;
+ }
+ }
+
+ .popup {
+ background: $header-background-color;
+ }
+
+ .item-header {
+ border-bottom: $border;
+
+ @media #{$mobile-only} {
+ background: $wrapper-mobile-background-color;
+ }
+ }
+
+ .pollContent {
+ .pollBar {
+ background: $secondary-link-color;
+ }
+ }
+
+ .loader {
+ color: $secondary-link-color;
+ background: $secondary-link-color;
+
+ &:before, &:after {
+ background: $secondary-link-color;
+ }
+ }
+
+ .job-header {
+ @media #{$mobile-only} {
+ border-bottom: $border;
+ }
+ }
+
+ .back-button {
+ @media #{$mobile-only} {
+ border-top: .3rem solid $secondary-link-color;
+ border-right: .3rem solid $secondary-link-color;
+ }
+ }
+
+ .error-section {
+ .skull {
+ .head {
+ background-color: $secondary-link-color;
+
+ &:before, &:after {
+ background-color: $wrapper-background-color;
+
+ @media #{$mobile-only} {
+ background-color: $wrapper-mobile-background-color;
+ }
+ }
+
+ .crack {
+ background-color: $wrapper-background-color;
+
+ @media #{$mobile-only} {
+ background-color: $wrapper-mobile-background-color;
+ }
+
+ &:before {
+ border-top: $skull-size / 8 solid $wrapper-background-color;
+
+ @media #{$mobile-only} {
+ border-top: $skull-size / 8 solid $wrapper-mobile-background-color;
+ }
+ }
+ }
+ }
+
+ .mouth {
+ background-color: $secondary-link-color;
+
+ &:before {
+ background-color: $wrapper-background-color;
+
+ @media #{$mobile-only} {
+ background-color: $wrapper-mobile-background-color;
+ }
+ }
+ }
+
+ .teeth {
+ background-color: $wrapper-background-color;
+
+ @media #{$mobile-only} {
+ background-color: $wrapper-mobile-background-color;
+ }
+
+ &:before, &:after {
+ background-color: $wrapper-background-color;
+
+ @media #{$mobile-only} {
+ background-color: $wrapper-mobile-background-color;
+ }
+ }
+ }
+ }
+ }
+
+ .main-details {
+ .name {
+ color: $secondary-link-color;
+ }
+ .right {
+ color: $secondary-link-color;
+ }
+ }
+ }
+ }
+}
+
+@include theme(
+ default,
+ $theme-day-body-background-color,
+ $theme-day-wrapper-background-color,
+ $theme-day-wrapper-mobile-background-color,
+ $theme-day-text-color,
+ $theme-day-text-color,
+ $theme-day-subtext-color,
+ $theme-day-header-background-color,
+ $theme-day-subtext-color,
+ $theme-day-secondary-color,
+ $theme-day-logo-inner,
+ $theme-day-border
+);
+
+@include theme(
+ night,
+ $theme-night-body-background-color,
+ $theme-night-wrapper-background-color,
+ $theme-night-wrapper-mobile-background-color,
+ $theme-night-text-color,
+ $theme-night-text-color,
+ $theme-night-subtext-color,
+ $theme-night-header-background-color,
+ $theme-night-subtext-color,
+ $theme-night-secondary-color,
+ $theme-night-logo-inner,
+ $theme-night-border
+);
+
+@include theme(
+ amoledblack,
+ $theme-amoledblack-body-background-color,
+ $theme-amoledblack-body-background-color,
+ $theme-amoledblack-body-background-color,
+ $theme-amoledblack-text-color,
+ $theme-amoledblack-text-color,
+ darken($theme-amoledblack-text-color, 33%),
+ $theme-amoledblack-body-background-color,
+ $theme-amoledblack-subtext-color,
+ $theme-amoledblack-secondary-color,
+ $theme-amoledblack-body-background-color,
+ $theme-amoledblack-secondary-color
+);
+
+/* ----------------------------------
+
+ Include your new theme here as well as the settings component
+
+---------------------------------- */
diff --git a/src-react/styles.scss b/src-react/styles.scss
new file mode 100644
index 000000000..d5650d1d4
--- /dev/null
+++ b/src-react/styles.scss
@@ -0,0 +1,16 @@
+@import "./shared/scss/themes";
+
+html {
+ height: 100%;
+ width: 100%;
+}
+
+body {
+ margin: 0;
+ height: 100%;
+ width: 100%;
+}
+
+pre {
+ white-space: pre-wrap;
+}