add: docker files + loading spinner & theme context

This commit is contained in:
Franek 2024-05-31 15:49:30 +02:00
parent f1e8f62bb2
commit 7d88da6667
No known key found for this signature in database
GPG Key ID: 0329F871B2079351
12 changed files with 191 additions and 39 deletions

11
Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM node:18-alpine
WORKDIR /app
COPY package.json ./
RUN yarn install
COPY . .
CMD ["yarn", "start"]

11
docker-compose.yml Normal file
View File

@ -0,0 +1,11 @@
services:
parafiaborzeta:
build:
context: .
dockerfile: Dockerfile
container_name: parafiaborzeta
ports:
- '3000:3000'
volumes:
- .:/app
- /app/node_modules

View File

@ -1,5 +0,0 @@
export default function NavigationBar() {
return <nav>
</nav>
}

View File

@ -1,23 +1,56 @@
'use client'
import { Montserrat } from "next/font/google"; import { Montserrat } from "next/font/google";
import type { Metadata } from "next"; import { useState, useEffect } from "react";
import NavigationBar from "@/components/navigation_bar.component";
import { ThemeProvider } from "@/providers/theme.provider";
import { GlobalLayout, MainBlock } from "@/styles";
import Loader from "@/components/loader.component";
const inter = Montserrat({ subsets: ["latin"] }); const inter = Montserrat({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Parafia pw. Niepokalanego Serca NMP w Borzęcie",
description: "Parafia pw. Niepokalanego Serca NMP w Borzęcie",
};
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( const [loading, setLoading] = useState(true);
useEffect(() => {
const handleComplete = () => setLoading(false);
// simulate loading delay
const timer = setTimeout(handleComplete, 1000);
return () => clearTimeout(timer);
}, []);
return <>
<html lang="en"> <html lang="en">
{/*
can't use next.Metadata,
because 'use client' + styled components are
preventing it.
next/head wouldn't work too, so legacy solution is used
*/}
<head>
<title>Parafia pw. Niepokalanego Serca NMP w Borzęcie</title>
<meta name="description" content="Strona parafii pod wezwaniem Niepokalanego Serca Najświętszej Maryi Panny w Borzęcie." />
</head>
<body className={inter.className}> <body className={inter.className}>
<ThemeProvider>
<GlobalLayout />
{loading
? <Loader />
: <>
<MainBlock>
<NavigationBar />
{children} {children}
</MainBlock>
</>}
</ThemeProvider>
</body> </body>
</html> </html>
); </>;
} }

View File

@ -1,26 +1,17 @@
'use client' 'use client'
import { ThemeProvider } from "styled-components"; import { useTheme } from "@/providers/theme.provider";
import { Button } from "../styles";
import { Button, GlobalLayout, MainBlock } from "./styles";
import { Theme, darkTheme, lightTheme } from "./themes";
import useLocalStorage from "./utils/local_storage";
const themes: Record<string, Theme> = {
light: lightTheme,
dark: darkTheme
}
export default function Home() { export default function Home() {
const [theme, setTheme] = useLocalStorage("theme", "light"); const { theme, toggleTheme } = useTheme();
return <ThemeProvider theme={themes[theme] ?? lightTheme}> return <>
<GlobalLayout />
<MainBlock>
<h1>Parafia w Borzęcie</h1> <h1>Parafia w Borzęcie</h1>
<p>Już wkrótce powstanie tutaj świeża strona parafii pod wezwaniem Niepokalanego Serca Maryi w Borzęcie.</p> <p>Już wkrótce powstanie tutaj świeża strona parafii pod wezwaniem Niepokalanego Serca Maryi w Borzęcie.</p>
<a href="tel:+48123456789">zadzwoń do proboszcza</a> <a href="tel:+48123456789">zadzwoń do proboszcza</a>
<Button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>Przełącz motyw</Button> <hr />
</MainBlock> <h2>Aktualny motyw: {theme}</h2>
</ThemeProvider>; <Button onClick={toggleTheme}>Przełącz motyw</Button>
</>;
} }

View File

@ -1,17 +1,18 @@
export type ThemeMode = 'light' | 'dark'
export interface Theme { export interface Theme {
background: string, background: string,
padding: number, space_px: number,
text: string text: string
} }
export const darkTheme: Theme = { export const darkTheme: Theme = {
padding: 10, space_px: 10,
background: "#2f3136", background: "#2f3136",
text: "#fff" text: "#fff"
} }
export const lightTheme: Theme = { export const lightTheme: Theme = {
padding: 10, space_px: 10,
background: "#eee", background: "#eee",
text: "#000" text: "#000"
} }

View File

@ -0,0 +1,34 @@
import styled, { keyframes } from "styled-components";
const spin = keyframes`
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
`;
const LoaderOverlay = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
`;
const Spinner = styled.div`
border: 8px solid ${({ theme }) => theme.text};
border-top: 8px solid ${({ theme }) => theme.background};
border-radius: 50%;
width: 50px;
height: 50px;
animation: ${spin} 1s linear infinite;
`;
export default function Loader() {
return <LoaderOverlay>
<Spinner />
</LoaderOverlay>
};

View File

@ -0,0 +1,33 @@
import styled from "styled-components"
import Link from "next/link"
export default function NavigationBar() {
return <Navigation>
<h1>Parafia w Borzęcie</h1>
<NavigationLinks>
<li><Link href="/">Strona główna</Link></li>
<li><Link href="/contact">Kontakt</Link></li>
</NavigationLinks>
</Navigation>
}
const Navigation = styled.nav`
display: flex;
align-items: center;
justify-content: space-between;
padding: ${({ theme }) => theme.space_px}px;
position: fixed;
width: 100%;
left: 0;
top: 0;
`
const NavigationLinks = styled.ul`
list-style-type: none;
li {
margin: ${({ theme }) => theme.space_px}px;
display: block;
float: left;
}
`

View File

@ -0,0 +1,41 @@
import { createContext, useContext, useMemo, PropsWithChildren } from "react";
import { ThemeProvider as StyledThemeProvider } from "styled-components";
import { ThemeMode, lightTheme, darkTheme } from "@/app/themes";
import useLocalStorage from "@/utils/local_storage";
const ThemeContext = createContext({
theme: "light",
toggleTheme: () => { },
} as {
theme: ThemeMode,
toggleTheme: () => void
});
export const ThemeProvider = ({ children }: PropsWithChildren) => {
const [theme, setTheme] = useLocalStorage<ThemeMode>("theme", "light");
const toggleTheme = () => {
setTheme(previous => previous === "light" ? "dark" : "light");
};
const value = useMemo(() => ({ theme, toggleTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
<StyledThemeProvider theme={theme === "light" ? lightTheme : darkTheme}>
{children}
</StyledThemeProvider>
</ThemeContext.Provider>
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};

View File

@ -39,7 +39,8 @@ export const MainBlock = styled.main`
export const Button = styled.button` export const Button = styled.button`
border: none; border: none;
outline: none; outline: none;
padding: ${({ theme }) => theme.padding}px; cursor: pointer;
padding: ${({ theme }) => theme.space_px}px;
color: ${({ theme }) => theme.background}; color: ${({ theme }) => theme.background};
background-color: ${({ theme }) => theme.text}; background-color: ${({ theme }) => theme.text};
`; `;

View File

@ -9,6 +9,7 @@ export default function useLocalStorage<T>(
useEffect(() => { useEffect(() => {
const item = window.localStorage.getItem(key) const item = window.localStorage.getItem(key)
if (item) { if (item) {
setValue(JSON.parse(item)) setValue(JSON.parse(item))
} }