diff --git a/index.html b/index.html
index add429b..4aa4a15 100644
--- a/index.html
+++ b/index.html
@@ -2,7 +2,7 @@
-
+
@@ -14,6 +14,10 @@
href="https://fonts.googleapis.com/css2?family=Fraunces:ital,wght@0,100;0,300;0,400;0,500;1,300&display=swap"
rel="stylesheet"
/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/logo.svg b/public/logo.svg
new file mode 100644
index 0000000..affb2df
--- /dev/null
+++ b/public/logo.svg
@@ -0,0 +1,24 @@
+
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index d957de3..e17ce9a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,24 +1,27 @@
import './App.css';
+
import { useState } from 'react';
+
+import { ThemeSelector } from 'components/ThemeSelector';
import styled, { ThemeProvider } from 'styled-components';
-import GlobalStyles from './global-styles';
-import { theme as primaryTheme } from './Theme';
-import { Header } from './components/Header';
+
import { Footer } from './components/Footer';
+import { Header } from './components/Header';
import StoicQuote from './components/QuoteContainer';
+import GlobalStyles from './global-styles';
+import { theme as primaryTheme } from './Theme';
export const Container = styled.main`
- text-align: center;
- height: 100vh;
- font-size: calc(10px + 2vmin);
- display: flex;
+ height: 100%;
+ /* text-align: center; */
+ /* display: flex;
flex-direction: column;
- justify-content: space-between;
+ justify-content: space-between; */
padding: 1rem;
`;
const TITLE = 'Stoic Quote';
-const COPYRIGHT = 'Natalie Pina \u00A9 2023';
+const COPYRIGHT = 'Natalie Pina \u00A9 2024';
const App = (): React.ReactElement => {
const [theme, setTheme] = useState(primaryTheme);
@@ -27,7 +30,8 @@ const App = (): React.ReactElement => {
-
+
+
diff --git a/src/Theme.tsx b/src/Theme.tsx
index 9aa1b16..3181374 100644
--- a/src/Theme.tsx
+++ b/src/Theme.tsx
@@ -2,15 +2,18 @@ import baseStyled, { ThemedStyledInterface } from 'styled-components';
const baseTheme = {
fonts: {
- primary: 'Fraunces',
- secondary: 'Cinzel',
+ primary: 'Fraunces, Arial, sans-serif',
+ secondary: 'Cinzel, Georgia, serif',
+ body: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif',
},
fontSizes: {
- sm: '.8em',
- base: '1em',
- md: '1.3em',
- lg: '2em',
- xl: '2.8em',
+ xs: '0.8rem', // Extra small
+ sm: '1rem', // Small
+ base: '1.25rem', // Base (equivalent to 20px)
+ md: '1.563rem', // Medium
+ lg: '1.953rem', // Large
+ xl: '2.441rem', // Extra large
+ xxl: '3.052rem', // Double extra large
},
sizes: {
xs: '4px',
@@ -25,36 +28,30 @@ const baseTheme = {
export const theme = {
...baseTheme,
colors: {
- light: '#ffffff',
- med: '#000000',
- dark: '#0a0c0e',
- primary: '#963d72',
- body: '#0a0c0e',
text: '#f9f9f9',
+ background: '#0a0c0e',
+ border: '#101212',
+ accent: '#3c7a89',
},
};
export const secondaryTheme = {
...baseTheme,
colors: {
- light: '#0a0c0e',
- med: '#ffffff',
- dark: '#d2d7db',
- primary: '#850f55',
- body: '#ffffff',
text: '#0a0c0e',
+ background: '#ffffff',
+ border: '#f5f5f5',
+ accent: '#3c7a89',
},
};
export const stoneTheme = {
...baseTheme,
colors: {
- light: '#333333',
- med: '#B0B0B0',
- dark: '#333333',
- primary: '#555555',
- body: '#D9D9D9',
- text: '#111111',
+ text: '#2f2f2f',
+ background: '#e2e2e2',
+ border: '#d6d6d6',
+ accent: '#3c7a89',
},
};
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 61b4733..03c307b 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -1,20 +1,28 @@
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
interface ButtonProps {
onClick?: () => void;
children?: React.ReactNode;
+ active?: boolean;
}
-export const Btn = styled.button`
+const activeStyles = css`
+ border: 1px solid ${({ theme: { colors } }) => colors.accent};
+ color: ${({ theme: { colors } }) => colors.accent};
+`;
+
+export const Btn = styled.button`
cursor: pointer;
- padding: 0.5rem 0.75rem;
+ padding-block: 0.5rem;
+ padding-inline: 0.75rem;
background-color: transparent;
- color: ${({ theme: { colors } }) => colors.light};
+ color: ${({ theme: { colors } }) => colors.text};
font-size: ${({ theme: { fontSizes } }) => fontSizes.xs};
- font-weight: bold;
+ font-family: ${({ theme: { fonts } }) => fonts.body};
+ font-weight: 600;
+ text-transform: uppercase;
border: 1px solid currentColor;
- font-family: ${({ theme: { fonts } }) => fonts.secondary};
- min-width: 60px;
+ border-radius: 2px;
&:hover {
transition: all 0.8s;
@@ -39,11 +47,29 @@ export const Btn = styled.button`
svg {
font-size: 20px;
}
+
+ ${({ active }) => active && activeStyles}
+
+ ${({ active }) =>
+ active &&
+ css`
+ outline: 1px solid ${({ theme: { colors } }) => colors.accent};
+ `}
+
+
+ @media (min-width: 767px) {
+ font-size: ${({ theme: { fontSizes } }) => fontSizes.sm};
+ }
`;
export const Button = ({
onClick,
children,
+ active,
}: ButtonProps): React.ReactElement => {
- return {children};
+ return (
+
+ {children}
+
+ );
};
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
index 9adda56..7f70f2b 100644
--- a/src/components/Footer.tsx
+++ b/src/components/Footer.tsx
@@ -3,21 +3,21 @@ import styled from 'styled-components';
interface FooterProps {
copyright?: string;
}
+
const FooterContainer = styled.footer`
- width: 100%;
- color: ${({ theme: { colors } }) => colors.light};
- font-size: 0.8rem;
- padding: 5px 0;
- text-align: left;
+ font-size: ${({ theme: { fontSizes } }) => fontSizes.xs};
+ text-align: right;
+ position: fixed;
+ bottom: 0;
`;
const Copyright = styled.p`
a {
text-decoration: none;
- color: ${({ theme: { colors } }) => colors.light};
+ color: ${({ theme: { colors } }) => colors.text};
&:hover {
- color: ${({ theme: { colors } }) => colors.primary};
+ color: ${({ theme: { colors } }) => colors.accent};
}
}
`;
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 50e7238..d700c62 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -1,22 +1,14 @@
import styled from 'styled-components';
-import { Button } from './Button';
-import {
- Theme,
- stoneTheme,
- theme as primaryTheme,
- secondaryTheme,
-} from '../Theme';
interface HeaderProps {
- setTheme: React.Dispatch>;
title: string;
}
const HeaderTitle = styled.h1`
- margin: 2rem auto 0;
- color: ${({ theme: { colors } }) => colors.light};
- font-size: ${({ theme: { fontSizes } }) => fontSizes.md};
- font-weight: 300;
+ margin: 1rem auto 0;
+ color: ${({ theme: { colors } }) => colors.text};
+ font-size: ${({ theme: { fontSizes } }) => fontSizes.lg};
+ font-weight: 700;
font-family: ${({ theme: { fonts } }) => fonts.secondary};
`;
@@ -25,43 +17,9 @@ const HeaderContainer = styled.div`
flex-direction: column;
`;
-const ButtonWrapper = styled.div`
- display: flex;
- gap: ${({ theme: { sizes } }) => sizes.md};
- flex-direction: column;
-
- @media (min-width: 768px) {
- position: absolute;
- }
-`;
-
-export const Header = ({ title, setTheme }: HeaderProps) => {
- const switchTheme = (newTheme: Theme) => {
- if (setTheme) setTheme(newTheme);
- };
-
+export const Header = ({ title }: HeaderProps) => {
return (
-
-
-
-
-
{title}
);
diff --git a/src/components/Quote.tsx b/src/components/Quote.tsx
index d88fe61..1a63d62 100644
--- a/src/components/Quote.tsx
+++ b/src/components/Quote.tsx
@@ -10,32 +10,37 @@ export interface QuoteData {
}
const QuoteWrapper = styled.div`
- margin: 0 auto;
- padding-bottom: ${({ theme: { sizes } }) => sizes.md};
- width: 90%;
- color: ${({ theme: { colors } }) => colors.light};
+ padding-block-end: ${({ theme: { sizes } }) => sizes.md};
+ color: ${({ theme: { colors } }) => colors.text};
+ font-size: ${({ theme: { fontSizes } }) => fontSizes.md};
+ width: 100%;
+
+ @media (min-width: 767px) {
+ font-size: ${({ theme: { fontSizes } }) => fontSizes.xl};
+ }
`;
const BlockQuote = styled.blockquote`
- text-align: center;
- font-size: ${({ theme: { fontSizes } }) => fontSizes.md};
+ max-width: 50ch;
`;
const Citation = styled.cite`
- font-size: ${({ theme: { fontSizes } }) => fontSizes.md};
+ font-size: ${({ theme: { fontSizes } }) => fontSizes.sm};
&:before {
content: '— ';
}
+
+ @media (min-width: 767px) {
+ font-size: ${({ theme: { fontSizes } }) => fontSizes.lg};
+ }
`;
export const Quote = ({ quote }: QuoteProps): JSX.Element => {
return (
- <>
-
- {quote?.quote}
- {quote?.author}
-
- >
+
+ {quote?.quote}
+ {quote?.author}
+
);
};
diff --git a/src/components/QuoteContainer.tsx b/src/components/QuoteContainer.tsx
index 289e8f6..42ab723 100644
--- a/src/components/QuoteContainer.tsx
+++ b/src/components/QuoteContainer.tsx
@@ -1,87 +1,51 @@
-import React, { useEffect, useRef, useState } from 'react';
-import styled from 'styled-components';
+import React from 'react';
+
+import useQuote from 'hooks/useQuote';
import { FaQuoteLeft, FaQuoteRight } from 'react-icons/fa';
import { ThreeDots } from 'react-loader-spinner';
-import { copyText } from '../helpers/helpers';
+import styled from 'styled-components';
+
import { Button } from './Button';
-import { QuoteData, Quote } from './Quote';
-// @ts-ignore
-import stoicQuote from 'stoic-quotes';
+import { Quote } from './Quote';
export const MainContainer = styled.div`
- width: 75vw;
- margin: 1rem auto;
+ justify-content: center;
+ align-items: center;
+ padding-inline: 0.5rem;
+ margin-right: auto;
+ margin-left: auto;
display: flex;
flex-direction: column;
place-items: center center;
font-family: ${({ theme: { fonts } }) => fonts.primary};
- padding: 0.5rem 0;
- color: ${({ theme: { colors } }) => colors.light};
+ text-align: center;
+ color: ${({ theme: { colors } }) => colors.text};
- @media (min-width: 768px) {
- padding: 2.5rem 0;
+ @media (min-width: 767px) {
+ font-size: ${({ theme: { fontSizes } }) => fontSizes.xl};
}
`;
-export const ActionButtonsWrapper = styled.div`
- margin: ${({ theme: { sizes } }) => sizes.xl} auto;
- width: 100%;
- display: flex;
- justify-content: center;
- gap: ${({ theme: { sizes } }) => sizes.md};
+const SectionWrapper = styled.section`
+ border-radius: 20px;
+ background-color: ${({ theme: { colors } }) => colors.border};
+ padding: 2rem;
+ margin-block: 2rem;
`;
-export const ButtonWrapper = styled.div`
- margin: ${({ theme: { sizes } }) => sizes.xl} auto;
- width: 100%;
+export const ButtonsWrapper = styled.div`
display: flex;
justify-content: center;
+ align-items: center;
gap: ${({ theme: { sizes } }) => sizes.md};
- flex-direction: column;
-
- @media (min-width: 768px) {
- flex-direction: row;
- }
`;
const QuoteContainer = (): React.ReactElement => {
- const [quote, setQuote] = useState(null);
- const [loading, setLoading] = useState(false);
- const [toolTip, setToolTip] = useState('Copy');
- const text = useRef('');
-
- useEffect(() => {
- const getQuote = async () => {
- try {
- setLoading(true);
- const res = await stoicQuote();
- const { quote, author } = res;
-
- setQuote({ quote, author });
- text.current = `"${quote}" -${author}`;
- } catch (e) {
- setQuote(null);
- } finally {
- setTimeout(() => setLoading(false), 300);
- }
- };
- if (!quote) getQuote();
- }, [quote]);
-
- const getNewQuote = () => {
- setQuote(null);
- setLoading(true);
- };
-
- const handleCopy = () => {
- copyText(text.current);
- setToolTip('Copied');
- setTimeout(() => setToolTip('Copy'), 1500);
- };
+ const { quote, loading, toolTip, text, handleCopy, getNewQuote } = useQuote();
return (
-
-
+
+
-
+
);
};
diff --git a/src/components/ThemeSelector.tsx b/src/components/ThemeSelector.tsx
new file mode 100644
index 0000000..507b8e1
--- /dev/null
+++ b/src/components/ThemeSelector.tsx
@@ -0,0 +1,67 @@
+import React, { useState } from 'react';
+
+import styled from 'styled-components';
+
+import {
+ secondaryTheme,
+ stoneTheme,
+ theme as primaryTheme,
+ Theme,
+} from '../Theme';
+import { Button } from './Button';
+
+interface ThemeSelectorProps {
+ setTheme: React.Dispatch>;
+}
+
+const ThemeSelectorContainer = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ justify-content: end;
+ padding: 0 20px;
+`;
+
+const ThemeSelectorWrapper = styled.div`
+ display: flex;
+ gap: ${({ theme: { sizes } }) => sizes.sm};
+`;
+
+export const ThemeSelector = ({ setTheme }: ThemeSelectorProps) => {
+ const [selectedTheme, setSelectedTheme] = useState(null);
+
+ const switchTheme = (newTheme: Theme) => {
+ if (setTheme) {
+ setTheme(newTheme);
+ setSelectedTheme(newTheme);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/global-styles.js b/src/global-styles.js
index f9411d8..37310e5 100644
--- a/src/global-styles.js
+++ b/src/global-styles.js
@@ -1,21 +1,33 @@
import { createGlobalStyle } from 'styled-components';
const GlobalStyles = createGlobalStyle`
- *,
- *::after,
- *::before {
- box-sizing: border-box;
- }
+ *,
+ *::after,
+ *::before {
+ box-sizing: border-box;
+ }
- body {
- font-family: 'Fraunces', sans-serif;
- background: ${({ theme: { colors } }) => colors.body};
- color: ${({ theme: { colors } }) => colors.text};
- }
+ * {
+ margin: 0;
+ padding: 0;
+ }
- html {
- scroll-behavior: smooth;
- }
+ html {
+ scroll-behavior: smooth;
+ }
+
+ body {
+ font-family: 'Fraunces', sans-serif;
+ background: ${({ theme: { colors } }) => colors.background};
+ color: ${({ theme: { colors } }) => colors.text};
+ line-height: 1.5;
+ }
+
+ h1,
+ h2,
+ h3 {
+ line-height: 1.2;
+ }
`;
export default GlobalStyles;
diff --git a/src/helpers/helpers.ts b/src/helpers/helpers.ts
index 5d86d60..572851f 100644
--- a/src/helpers/helpers.ts
+++ b/src/helpers/helpers.ts
@@ -7,3 +7,8 @@ export const copyText = (value: string): void => {
document.execCommand('copy');
document.body.removeChild(dummy);
};
+
+export const getSelectedTheme = () => {
+ const storedTheme = localStorage.getItem('selectedTheme');
+ return storedTheme ? JSON.parse(storedTheme) : 'primaryTheme';
+};
diff --git a/src/hooks/useQuote.tsx b/src/hooks/useQuote.tsx
new file mode 100644
index 0000000..29b6ccd
--- /dev/null
+++ b/src/hooks/useQuote.tsx
@@ -0,0 +1,47 @@
+import { useEffect, useRef, useState } from 'react';
+
+import { QuoteData } from 'components/Quote';
+// @ts-ignore
+import stoicQuote from 'stoic-quotes';
+
+import { copyText } from '../helpers/helpers';
+
+function useQuote() {
+ const [quote, setQuote] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [toolTip, setToolTip] = useState('Copy');
+ const text = useRef('');
+
+ useEffect(() => {
+ const getQuote = async () => {
+ try {
+ setLoading(true);
+ const res = await stoicQuote();
+ const { quote, author } = res;
+
+ setQuote({ quote, author });
+ text.current = `"${quote}" -${author}`;
+ } catch (e) {
+ setQuote(null);
+ } finally {
+ setTimeout(() => setLoading(false), 300);
+ }
+ };
+ if (!quote) getQuote();
+ }, [quote]);
+
+ const getNewQuote = () => {
+ setQuote(null);
+ setLoading(true);
+ };
+
+ const handleCopy = () => {
+ copyText(text.current);
+ setToolTip('Copied');
+ setTimeout(() => setToolTip('Copy'), 1500);
+ };
+
+ return { quote, loading, toolTip, text, handleCopy, getNewQuote };
+}
+
+export default useQuote;
diff --git a/src/index.tsx b/src/index.tsx
index d23f06b..33e280e 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,5 +1,7 @@
import React from 'react';
+
import ReactDOM from 'react-dom/client';
+
import App from './App';
const root = ReactDOM.createRoot(