diff --git a/src/main.tsx b/src/main.tsx index 6e48f57c..db16d364 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,7 +1,7 @@ import { createRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; -import App from './react/App'; import { SettingsProvider } from './react/context/SettingsContext'; +import AppRoutes from './react/routes'; import './react/styles.scss'; const rootElement = document.getElementById('root'); @@ -13,7 +13,7 @@ if (!rootElement) { createRoot(rootElement).render( - + ); diff --git a/src/react/App.scss b/src/react/App.scss new file mode 100644 index 00000000..2b421f0d --- /dev/null +++ b/src/react/App.scss @@ -0,0 +1,24 @@ +@import "./scss/media"; +@import "./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 7537f7c2..7303e10a 100644 --- a/src/react/App.tsx +++ b/src/react/App.tsx @@ -1,11 +1,23 @@ +import { Outlet } from 'react-router-dom'; import { useSettings } from './context/SettingsContext'; +import { usePageViews } from './hooks/usePageViews'; +import Footer from './components/Footer'; +import Header from './components/Header'; +import './App.scss'; export default function App() { const { settings } = useSettings(); + usePageViews(); + return ( -
-

Hacker News

-
+
+
+
+
+ +
+
); } diff --git a/src/react/components/Feed.tsx b/src/react/components/Feed.tsx new file mode 100644 index 00000000..a17d97f4 --- /dev/null +++ b/src/react/components/Feed.tsx @@ -0,0 +1,17 @@ +import { useParams } from 'react-router-dom'; +import { FeedName } from '../models/feed-name.type'; + +interface FeedProps { + feedName: FeedName; +} + +export default function Feed({ feedName }: FeedProps) { + const { page } = useParams<'page'>(); + + return ( +
+

{feedName}

+

Page {page}

+
+ ); +} diff --git a/src/react/components/Footer.scss b/src/react/components/Footer.scss new file mode 100644 index 00000000..ec664bb1 --- /dev/null +++ b/src/react/components/Footer.scss @@ -0,0 +1,23 @@ +@import "../scss/media"; +@import "../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/components/Footer.tsx b/src/react/components/Footer.tsx new file mode 100644 index 00000000..dcd1c0f6 --- /dev/null +++ b/src/react/components/Footer.tsx @@ -0,0 +1,14 @@ +import './Footer.scss'; + +export default function Footer() { + return ( + + ); +} diff --git a/src/react/components/Header.scss b/src/react/components/Header.scss new file mode 100644 index 00000000..b97bc434 --- /dev/null +++ b/src/react/components/Header.scss @@ -0,0 +1,149 @@ +@import "../scss/media"; +@import "../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/components/Header.tsx b/src/react/components/Header.tsx new file mode 100644 index 00000000..86663e92 --- /dev/null +++ b/src/react/components/Header.tsx @@ -0,0 +1,72 @@ +import { Fragment } from 'react'; +import { NavLink, useLocation } from 'react-router-dom'; +import { useSettings } from '../context/SettingsContext'; +import { FeedName } from '../models/feed-name.type'; +import Settings from './Settings'; +import './Header.scss'; + +const feedLinks: Array<{ feedName: FeedName; path: string; label: string }> = [ + { feedName: 'newest', path: '/newest/1', label: 'new' }, + { feedName: 'show', path: '/show/1', label: 'show' }, + { feedName: 'ask', path: '/ask/1', label: 'ask' }, + { feedName: 'jobs', path: '/jobs/1', label: 'jobs' }, +]; + +function isFeedActive(pathname: string, feedName: FeedName): boolean { + return pathname.split('/')[1] === feedName; +} + +function scrollTop(): void { + window.scrollTo(0, 0); +} + +export default function Header() { + const { pathname } = useLocation(); + const { settings, toggleSettings } = useSettings(); + + return ( +
+ + {settings.showSettings && } +
+ ); +} diff --git a/src/react/components/ItemDetails.tsx b/src/react/components/ItemDetails.tsx new file mode 100644 index 00000000..e6e5382d --- /dev/null +++ b/src/react/components/ItemDetails.tsx @@ -0,0 +1,12 @@ +import { useParams } from 'react-router-dom'; + +export default function ItemDetails() { + const { id } = useParams<'id'>(); + + return ( +
+

Item details

+

Item {id}

+
+ ); +} diff --git a/src/react/components/Settings.scss b/src/react/components/Settings.scss new file mode 100644 index 00000000..354c248b --- /dev/null +++ b/src/react/components/Settings.scss @@ -0,0 +1,74 @@ +@import "../scss/media"; +@import "../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/components/Settings.tsx b/src/react/components/Settings.tsx new file mode 100644 index 00000000..f1cbe0b8 --- /dev/null +++ b/src/react/components/Settings.tsx @@ -0,0 +1,104 @@ +import { useSettings } from '../context/SettingsContext'; +import './Settings.scss'; + +export default function Settings() { + const { + settings, + toggleSettings, + toggleOpenLinksInNewTab, + setTheme, + setFont, + setSpacing, + } = useSettings(); + + return ( +
+
+

Settings

+
+ + × + +
+
+

Links

+ {' '} + Open links in a new tab +
+
+
+

Select a theme

+
+ +
+
+ +
+
+ +
+
+
+

Change Font

+
+ +
+
+ +
+
+
+
+
+
+ ); +} diff --git a/src/react/components/User.tsx b/src/react/components/User.tsx new file mode 100644 index 00000000..6f21b762 --- /dev/null +++ b/src/react/components/User.tsx @@ -0,0 +1,12 @@ +import { useParams } from 'react-router-dom'; + +export default function User() { + const { id } = useParams<'id'>(); + + return ( +
+

User

+

{id}

+
+ ); +} diff --git a/src/react/hooks/usePageViews.ts b/src/react/hooks/usePageViews.ts new file mode 100644 index 00000000..a58c3615 --- /dev/null +++ b/src/react/hooks/usePageViews.ts @@ -0,0 +1,15 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +export function usePageViews(): void { + const { pathname } = useLocation(); + + useEffect(() => { + if (typeof window === 'undefined' || typeof (window as any).ga !== 'function') { + return; + } + + (window as any).ga('set', 'page', pathname); + (window as any).ga('send', 'pageview'); + }, [pathname]); +} diff --git a/src/react/routes.tsx b/src/react/routes.tsx new file mode 100644 index 00000000..115c537a --- /dev/null +++ b/src/react/routes.tsx @@ -0,0 +1,41 @@ +import { lazy, Suspense } from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import { FeedName } from './models/feed-name.type'; +import App from './App'; +import Feed from './components/Feed'; + +const ItemDetails = lazy(() => import('./components/ItemDetails')); +const User = lazy(() => import('./components/User')); + +const lazyFallback =
Loading...
; + +export default function AppRoutes() { + return ( + + }> + } /> + } /> + } /> + } /> + } /> + } /> + + + + } + /> + + + + } + /> + + + ); +}