├── .prettierrc
├── public
├── postap.png
├── cyberpunk.png
├── fairytale.png
├── mountains.jpg
├── images
│ └── weather
│ │ ├── 01d@2x.png
│ │ ├── 01n@2x.png
│ │ ├── 02d@2x.png
│ │ ├── 02n@2x.png
│ │ ├── 03d@2x.png
│ │ ├── 03n@2x.png
│ │ ├── 04d@2x.png
│ │ ├── 04n@2x.png
│ │ ├── 09d@2x.png
│ │ ├── 09n@2x.png
│ │ ├── 10d@2x.png
│ │ ├── 10n@2x.png
│ │ ├── 11d@2x.png
│ │ ├── 11n@2x.png
│ │ ├── 13d@2x.png
│ │ ├── 13n@2x.png
│ │ ├── 50d@2x.png
│ │ ├── 50n@2x.png
│ │ └── weather-icons-license.txt
└── vercel.svg
├── src
├── app
│ ├── favicon.ico
│ ├── snake
│ │ └── page.tsx
│ ├── notepad
│ │ └── page.tsx
│ ├── layout.tsx
│ ├── page.tsx
│ └── api
│ │ ├── facts
│ │ └── route.ts
│ │ ├── quotes
│ │ └── route.ts
│ │ └── weather
│ │ └── route.ts
├── assets
│ ├── images
│ │ ├── gh.png
│ │ ├── snake.png
│ │ ├── refresh.png
│ │ ├── spinner.gif
│ │ ├── changeCity.png
│ │ └── weather
│ │ │ ├── 01d@2x.png
│ │ │ ├── 01n@2x.png
│ │ │ ├── 02d@2x.png
│ │ │ ├── 02n@2x.png
│ │ │ ├── 03d@2x.png
│ │ │ ├── 03n@2x.png
│ │ │ ├── 04d@2x.png
│ │ │ ├── 04n@2x.png
│ │ │ ├── 09d@2x.png
│ │ │ ├── 09n@2x.png
│ │ │ ├── 10d@2x.png
│ │ │ ├── 10n@2x.png
│ │ │ ├── 11d@2x.png
│ │ │ ├── 11n@2x.png
│ │ │ ├── 13d@2x.png
│ │ │ ├── 13n@2x.png
│ │ │ ├── 50d@2x.png
│ │ │ └── 50n@2x.png
│ └── icons
│ │ ├── CheckIcon.tsx
│ │ ├── ArrowLeftIcon.tsx
│ │ ├── CloseIcon.tsx
│ │ ├── EditIcon.tsx
│ │ ├── PictureIcon.tsx
│ │ ├── HumidityIcon.tsx
│ │ ├── HomeIcon.tsx
│ │ ├── RefreshIcon.tsx
│ │ ├── PlannerMobileIcon.tsx
│ │ ├── NotepadEditIcon.tsx
│ │ ├── WeatherMobileIcon.tsx
│ │ ├── GithubIcon.tsx
│ │ ├── DeleteIcon.tsx
│ │ ├── DragHandleIcon.tsx
│ │ ├── SettingsIcon.tsx
│ │ ├── PressureIcon.tsx
│ │ ├── WindIcon.tsx
│ │ ├── HandIcon.tsx
│ │ └── SnakeIcon.tsx
├── services
│ ├── fetchFact.ts
│ ├── fetchQuote.ts
│ ├── providers.tsx
│ └── fetchWeatherData.ts
├── hooks
│ ├── useIsMounted.ts
│ ├── useClickOutside.ts
│ ├── useCurrentDate.ts
│ ├── useEditUserData.ts
│ ├── useSettings.ts
│ ├── useIntro.ts
│ ├── useNotepad.ts
│ ├── useWeatherData.ts
│ ├── useWelcome.ts
│ ├── usePlanner.ts
│ ├── useHomepage.ts
│ └── useSnake.ts
├── store
│ ├── mobileViewStore.ts
│ ├── snakeStore.ts
│ ├── weatherStore.ts
│ ├── plannerStore.ts
│ ├── notepadStore.ts
│ ├── userStore.ts
│ └── settingsStore.ts
├── components
│ ├── settings
│ │ ├── SettingsTitle.tsx
│ │ ├── SettingsSection.tsx
│ │ ├── SettingsModals.tsx
│ │ ├── SettingsSlider.tsx
│ │ ├── SettingsGithub.tsx
│ │ ├── Settings.tsx
│ │ ├── SettingsSectionRow.tsx
│ │ └── SettingsContent.tsx
│ ├── auth
│ │ └── ProtectedRoute.tsx
│ ├── views
│ │ ├── homepage
│ │ │ ├── weather
│ │ │ │ ├── WeatherParameter.tsx
│ │ │ │ └── WeatherHourBox.tsx
│ │ │ ├── Homepage.tsx
│ │ │ ├── planner
│ │ │ │ ├── PlannerHeader.tsx
│ │ │ │ ├── Planner.tsx
│ │ │ │ └── PlannerItem.tsx
│ │ │ └── welcome
│ │ │ │ └── Welcome.tsx
│ │ ├── notepad
│ │ │ ├── NotepadTextArea.tsx
│ │ │ └── Notepad.tsx
│ │ ├── intro
│ │ │ └── Intro.tsx
│ │ └── snake
│ │ │ └── Snake.tsx
│ ├── layout
│ │ ├── SnakeButton.tsx
│ │ ├── MobileView.tsx
│ │ ├── SideButtons.tsx
│ │ ├── MobileNavigation.tsx
│ │ └── Layout.tsx
│ ├── common
│ │ ├── PageWrapper.tsx
│ │ └── Loader.tsx
│ └── modals
│ │ ├── NotepadAlert.tsx
│ │ ├── EditUserDataModal.tsx
│ │ └── ClearAllDataModal.tsx
├── utils
│ ├── clearAllData.ts
│ ├── localStorageUtils.ts
│ └── countryMapping.ts
└── theme
│ ├── components
│ ├── contentBox.tsx
│ ├── button.ts
│ └── text.ts
│ ├── generateTheme.ts
│ ├── colors.ts
│ └── extendedColors.ts
├── .eslintrc.json
├── next-env.d.ts
├── next.config.js
├── .gitignore
├── CHANGELOG.md
├── tsconfig.json
├── CONTRIBUTING.md
├── package.json
└── README.md
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "bracketSameLine": true
4 | }
5 |
--------------------------------------------------------------------------------
/public/postap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/postap.png
--------------------------------------------------------------------------------
/public/cyberpunk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/cyberpunk.png
--------------------------------------------------------------------------------
/public/fairytale.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/fairytale.png
--------------------------------------------------------------------------------
/public/mountains.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/mountains.jpg
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/assets/images/gh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/gh.png
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next", "prettier"],
3 | "ignorePatterns": ["next.config.js"]
4 | }
5 |
--------------------------------------------------------------------------------
/src/assets/images/snake.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/snake.png
--------------------------------------------------------------------------------
/src/assets/images/refresh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/refresh.png
--------------------------------------------------------------------------------
/src/assets/images/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/spinner.gif
--------------------------------------------------------------------------------
/public/images/weather/01d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/01d@2x.png
--------------------------------------------------------------------------------
/public/images/weather/01n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/01n@2x.png
--------------------------------------------------------------------------------
/public/images/weather/02d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/02d@2x.png
--------------------------------------------------------------------------------
/public/images/weather/02n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/02n@2x.png
--------------------------------------------------------------------------------
/public/images/weather/03d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/03d@2x.png
--------------------------------------------------------------------------------
/public/images/weather/03n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/03n@2x.png
--------------------------------------------------------------------------------
/public/images/weather/04d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/04d@2x.png
--------------------------------------------------------------------------------
/public/images/weather/04n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/04n@2x.png
--------------------------------------------------------------------------------
/public/images/weather/09d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/09d@2x.png
--------------------------------------------------------------------------------
/public/images/weather/09n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/09n@2x.png
--------------------------------------------------------------------------------
/public/images/weather/10d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/10d@2x.png
--------------------------------------------------------------------------------
/public/images/weather/10n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/10n@2x.png
--------------------------------------------------------------------------------
/public/images/weather/11d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/11d@2x.png
--------------------------------------------------------------------------------
/public/images/weather/11n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/11n@2x.png
--------------------------------------------------------------------------------
/public/images/weather/13d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/13d@2x.png
--------------------------------------------------------------------------------
/public/images/weather/13n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/13n@2x.png
--------------------------------------------------------------------------------
/public/images/weather/50d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/50d@2x.png
--------------------------------------------------------------------------------
/public/images/weather/50n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/public/images/weather/50n@2x.png
--------------------------------------------------------------------------------
/src/assets/images/changeCity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/changeCity.png
--------------------------------------------------------------------------------
/src/assets/images/weather/01d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/01d@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/01n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/01n@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/02d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/02d@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/02n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/02n@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/03d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/03d@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/03n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/03n@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/04d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/04d@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/04n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/04n@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/09d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/09d@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/09n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/09n@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/10d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/10d@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/10n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/10n@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/11d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/11d@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/11n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/11n@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/13d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/13d@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/13n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/13n@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/50d@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/50d@2x.png
--------------------------------------------------------------------------------
/src/assets/images/weather/50n@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt765/mistyloop/HEAD/src/assets/images/weather/50n@2x.png
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/src/services/fetchFact.ts:
--------------------------------------------------------------------------------
1 | export const fetchFact = async () => {
2 | let data;
3 | do {
4 | const response = await fetch('/api/facts');
5 | data = await response.json();
6 | } while (data.text.length > 130 || data.text.length < 35);
7 | return data.text;
8 | };
9 |
--------------------------------------------------------------------------------
/src/hooks/useIsMounted.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState, useEffect } from 'react';
4 |
5 | export const useIsMounted = (): boolean => {
6 | const [isMounted, setIsMounted] = useState(false);
7 |
8 | useEffect(() => {
9 | setIsMounted(true);
10 | }, []);
11 |
12 | return isMounted;
13 | };
14 |
--------------------------------------------------------------------------------
/src/app/snake/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { SnakeGame } from '@/components/views/snake/Snake';
4 | import { ProtectedRoute } from '@/components/auth/ProtectedRoute';
5 |
6 | export default function SnakePage() {
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/assets/icons/CheckIcon.tsx:
--------------------------------------------------------------------------------
1 | export const CheckIcon = () => (
2 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/services/fetchQuote.ts:
--------------------------------------------------------------------------------
1 | export const fetchQuote = async () => {
2 | let data;
3 | const category = 'happiness';
4 | do {
5 | const response = await fetch(`/api/quotes?category=${category}`);
6 | data = await response.json();
7 | } while (data[0].quote.length > 125 || data[0].quote.length < 35);
8 | return { quote: data[0].quote, author: data[0].author };
9 | };
10 |
--------------------------------------------------------------------------------
/src/app/notepad/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Notepad } from '@/components/views/notepad/Notepad';
4 | import { useIsMounted } from '@/hooks/useIsMounted';
5 | import { ProtectedRoute } from '@/components/auth/ProtectedRoute';
6 |
7 | export default function NotepadPage() {
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/assets/icons/ArrowLeftIcon.tsx:
--------------------------------------------------------------------------------
1 | export const ArrowLeftIcon = () => (
2 |
5 | );
6 |
--------------------------------------------------------------------------------
/src/assets/icons/CloseIcon.tsx:
--------------------------------------------------------------------------------
1 | export const CloseIcon = () => (
2 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/assets/icons/EditIcon.tsx:
--------------------------------------------------------------------------------
1 | export const EditIcon = () => (
2 |
5 | );
6 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: false,
4 | env: {
5 | MY_SECRET_API_KEY: process.env.MY_SECRET_API_KEY,
6 | ANOTHER_SECRET: process.env.ANOTHER_SECRET,
7 | },
8 | images: {
9 | remotePatterns: [
10 | {
11 | protocol: 'https',
12 | hostname: 'cdn.weatherapi.com',
13 | pathname: '**',
14 | },
15 | ],
16 | },
17 | };
18 |
19 | module.exports = nextConfig;
20 |
--------------------------------------------------------------------------------
/src/store/mobileViewStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { ViewType } from '@/hooks/useHomepage';
3 |
4 | interface MobileViewState {
5 | mobileView: ViewType;
6 | setMobileView: (view: ViewType) => void;
7 | }
8 |
9 | export const useMobileViewStore = create((set) => ({
10 | mobileView: 'mobileHome',
11 | setMobileView: (view) => {
12 | localStorage.setItem('currentMobileView', view);
13 | set({ mobileView: view });
14 | },
15 | }));
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
--------------------------------------------------------------------------------
/public/images/weather/weather-icons-license.txt:
--------------------------------------------------------------------------------
1 | Weather Icons License
2 | =====================
3 |
4 | Weather icons provided by OpenWeather.
5 |
6 | Source: https://openweathermap.org/
7 | Copyright (c) OpenWeather Ltd.
8 | Licensed under Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
9 | License: https://creativecommons.org/licenses/by-sa/4.0/
10 |
11 | No modifications have been made to the original icons.
12 |
13 | This project is not affiliated with or endorsed by OpenWeather Ltd.
--------------------------------------------------------------------------------
/src/store/snakeStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | import {
4 | loadFromLocalStorage,
5 | saveToLocalStorage,
6 | } from '@/utils/localStorageUtils';
7 |
8 | type SnakeStoreState = {
9 | record: number;
10 | setRecord: (record: number) => void;
11 | };
12 |
13 | export const useSnakeStore = create((set) => ({
14 | record: loadFromLocalStorage('snakeStoreRecord', 0),
15 | setRecord: (record) => {
16 | saveToLocalStorage('snakeStoreRecord', record);
17 | set({ record });
18 | },
19 | }));
20 |
21 |
--------------------------------------------------------------------------------
/src/assets/icons/PictureIcon.tsx:
--------------------------------------------------------------------------------
1 | export const PictureIcon = () => (
2 |
9 | );
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.0.0 (13-02-2025)
4 |
5 | - add changelog
6 | - add first tag & Github release
7 | - update package.json
8 | - add contributing guidelines
9 |
10 |
11 | ## 1.0.1 (15-02-2025)
12 |
13 | - migrate to new weather api
14 | - redesign homepage buttons
15 | - fix laptop resolution
16 | - refactor local storage utils
17 | - adjust quotes api url
18 |
19 | ## 1.0.2 (20-05-2025)
20 |
21 | - migrate to app router
22 | - create separate layout folder
23 | - extract views into subpages
24 | - fix console errors
25 | - adjust mobile resolution
26 | - migrate planner to @hello-pangea/dnd
27 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Providers } from '@/services/providers';
2 | import { Layout } from '../components/layout/Layout';
3 |
4 | export const metadata = {
5 | title: 'MistyLoop',
6 | description:
7 | 'Application designed to be an alternative to default starting page in the browser',
8 | };
9 |
10 | export default function RootLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode;
14 | }) {
15 | return (
16 |
17 |
18 |
19 | {children}
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/settings/SettingsTitle.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@chakra-ui/react';
2 |
3 | interface SettingsTitleProps {
4 | title: string;
5 | }
6 |
7 | export const SettingsTitle = ({ title }: SettingsTitleProps) => {
8 | return (
9 |
20 | {title}
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/store/weatherStore.ts:
--------------------------------------------------------------------------------
1 | import { create, SetState } from 'zustand';
2 | import { WeatherData } from '@/hooks/useWeatherData';
3 |
4 | interface WeatherStore {
5 | weatherData: WeatherData | null;
6 | isLoading: boolean;
7 | isError: boolean;
8 | setWeatherData: (data: WeatherData | null, hasError?: boolean) => void;
9 | }
10 |
11 | export const useWeatherStore = create((set: SetState) => ({
12 | weatherData: null,
13 | isLoading: false,
14 | isError: false,
15 | setWeatherData: (data: WeatherData | null, hasError: boolean = false) =>
16 | set((state) => ({ ...state, weatherData: data, isError: hasError })),
17 | }));
18 |
--------------------------------------------------------------------------------
/src/assets/icons/HumidityIcon.tsx:
--------------------------------------------------------------------------------
1 | export const HumidityIcon = () =>
2 |
17 |
18 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useMediaQuery } from '@chakra-ui/react';
4 | import { Homepage } from '@/components/views/homepage/Homepage';
5 | import { Intro } from '@/components/views/intro/Intro';
6 | import { MobileView } from '@/components/layout/MobileView';
7 | import { useUserStoreWrapper } from '@/store/userStore';
8 |
9 | export default function Home() {
10 | const { name, city } = useUserStoreWrapper();
11 | const [isDesktop] = useMediaQuery('(min-width: 1280px)');
12 | const [isMobile] = useMediaQuery('(max-width: 1279px)');
13 |
14 | if (!name || !city) {
15 | return ;
16 | }
17 |
18 | return (
19 | <>
20 | {isDesktop && }
21 | {isMobile && }
22 | >
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/assets/icons/HomeIcon.tsx:
--------------------------------------------------------------------------------
1 | export const HomeIcon = () => {
2 | return (
3 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/assets/icons/RefreshIcon.tsx:
--------------------------------------------------------------------------------
1 | export const RefreshIcon = () => (
2 |
5 | );
6 |
--------------------------------------------------------------------------------
/src/assets/icons/PlannerMobileIcon.tsx:
--------------------------------------------------------------------------------
1 | export const PlannerMobileIcon = () => (
2 |
5 | );
6 |
--------------------------------------------------------------------------------
/src/app/api/facts/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 |
3 | const baseFactsApiUrl = process.env.FACTS_API_URL || '';
4 |
5 | export async function GET() {
6 | try {
7 | const factsApiUrl = `${baseFactsApiUrl}?language=en`;
8 | const response = await fetch(factsApiUrl);
9 |
10 | if (!response.ok) {
11 | const errorText = await response.text();
12 | return NextResponse.json(
13 | { error: `Facts API response error: ${errorText}` },
14 | { status: response.status }
15 | );
16 | }
17 |
18 | const data = await response.json();
19 | return NextResponse.json(data);
20 | } catch (error) {
21 | console.error('Error fetching facts:', error);
22 | return NextResponse.json(
23 | { error: 'Failed to fetch facts' },
24 | { status: 500 }
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/assets/icons/NotepadEditIcon.tsx:
--------------------------------------------------------------------------------
1 | export const NotepadEditIcon = () => (
2 |
5 | );
6 |
--------------------------------------------------------------------------------
/src/hooks/useClickOutside.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, RefObject } from 'react';
2 |
3 | export const useClickOutside = (
4 | ref: RefObject,
5 | handler: (event: MouseEvent | TouchEvent) => void
6 | ): void => {
7 | useEffect(() => {
8 | const listener = (event: MouseEvent | TouchEvent) => {
9 | // Do nothing if clicking ref's element or descendent elements
10 | if (!ref.current || ref.current.contains(event.target as Node)) {
11 | return;
12 | }
13 | handler(event);
14 | };
15 | document.addEventListener('mousedown', listener);
16 | document.addEventListener('touchstart', listener);
17 | return () => {
18 | document.removeEventListener('mousedown', listener);
19 | document.removeEventListener('touchstart', listener);
20 | };
21 | }, [ref, handler]);
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/settings/SettingsSection.tsx:
--------------------------------------------------------------------------------
1 | import { BoxProps, Flex } from '@chakra-ui/react';
2 |
3 | interface Props extends BoxProps {
4 | paddingBottom?: string;
5 | children: React.ReactNode;
6 | }
7 |
8 | export const SettingsSection = ({
9 | paddingBottom,
10 | children,
11 | ...props
12 | }: Props) => {
13 | return (
14 |
30 | {children}
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/src/assets/icons/WeatherMobileIcon.tsx:
--------------------------------------------------------------------------------
1 | export const WeatherMobileIcon = () => (
2 |
5 | );
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "bundler",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve",
20 | "incremental": true,
21 | "baseUrl": ".",
22 | "paths": {
23 | "@/*": [
24 | "./src/*"
25 | ]
26 | },
27 | "plugins": [
28 | {
29 | "name": "next"
30 | }
31 | ]
32 | },
33 | "include": [
34 | "**/*.ts",
35 | "**/*.tsx",
36 | "next-env.d.ts",
37 | ".next/types/**/*.ts"
38 | ],
39 | "exclude": [
40 | "node_modules"
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Thank you for your interest in contributing to MistyLoop project. I appreciate your feedback, which is invaluable in helping me improve and grow this application. If you encounter any issues, have suggestions for improvements, or would like to share your thoughts, please don't hesitate to open an issue on the GitHub Issues page or to contact me directly.
2 |
3 | If you would like to support the ongoing development and maintenance of this project, you can do so through the GitHub Sponsors program. Your sponsorship helps me dedicate more time and resources to this project. To become a sponsor, you can click the "Sponsor" button on my profile.
4 |
5 | For some time, I will hold off on accepting external merge requests. While I greatly appreciate your willingness to contribute code, I am currently focused on managing the project's development internally. This may change in the future.
6 |
7 | Thank you once again for your interest and support!
8 |
9 | ~matt765
--------------------------------------------------------------------------------
/src/assets/icons/GithubIcon.tsx:
--------------------------------------------------------------------------------
1 | export const GithubIcon = () => {
2 | return (
3 |
6 | );
7 | };
8 |
--------------------------------------------------------------------------------
/src/assets/icons/DeleteIcon.tsx:
--------------------------------------------------------------------------------
1 | export const DeleteIcon = () =>
2 |
19 |
20 |
--------------------------------------------------------------------------------
/src/assets/icons/DragHandleIcon.tsx:
--------------------------------------------------------------------------------
1 | export const DragHandleIcon = () =>
2 |
19 |
20 |
--------------------------------------------------------------------------------
/src/assets/icons/SettingsIcon.tsx:
--------------------------------------------------------------------------------
1 | export const SettingsIcon = () => (
2 |
15 | );
16 |
--------------------------------------------------------------------------------
/src/components/auth/ProtectedRoute.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ReactNode, useEffect } from 'react';
4 | import { useRouter } from 'next/navigation';
5 | import { useUserStoreWrapper } from '@/store/userStore';
6 | import { Flex } from '@chakra-ui/react';
7 | import { Loader } from '@/components/common/Loader';
8 |
9 | interface ProtectedRouteProps {
10 | children: ReactNode;
11 | }
12 |
13 | export const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
14 | const { name, city, isMounted } = useUserStoreWrapper();
15 | const router = useRouter();
16 |
17 | useEffect(() => {
18 | if (isMounted && (!name || !city)) {
19 | router.replace('/');
20 | }
21 | }, [name, city, isMounted, router]);
22 |
23 | if (!isMounted) {
24 | return (
25 |
26 |
27 |
28 | );
29 | }
30 |
31 | if (!name || !city) {
32 | return null;
33 | }
34 |
35 | return <>{children}>;
36 | };
37 |
--------------------------------------------------------------------------------
/src/components/views/homepage/weather/WeatherParameter.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Icon, Text } from '@chakra-ui/react';
2 |
3 | interface WeatherParameterProps {
4 | icon: React.FC>;
5 | title: string;
6 | value: string | number | undefined;
7 | }
8 |
9 | export const WeatherParameter = ({
10 | icon,
11 | title,
12 | value,
13 | }: WeatherParameterProps) => (
14 |
22 |
23 |
24 |
25 |
26 |
29 | {title}
30 |
31 |
34 | {value}
35 |
36 |
37 |
38 | );
39 |
--------------------------------------------------------------------------------
/src/utils/clearAllData.ts:
--------------------------------------------------------------------------------
1 | import { useNotepadStore } from '@/store/notepadStore';
2 | import { plannerItemsDefault, usePlannerStore } from '@/store/plannerStore';
3 | import { useSettingsStore } from '@/store/settingsStore';
4 | import { useSnakeStore } from '@/store/snakeStore';
5 | import { useUserStore } from '@/store/userStore';
6 |
7 | export const clearAllData = () => {
8 | const notepadStore = useNotepadStore.getState();
9 | const plannerStore = usePlannerStore.getState();
10 | const settingsStore = useSettingsStore.getState();
11 | const snakeStore = useSnakeStore.getState();
12 | const userStore = useUserStore.getState();
13 |
14 | notepadStore.setStoreNote('');
15 | notepadStore.setIsNotepadModalConfirmed(false);
16 | plannerStore.setPlannerItems([]);
17 | settingsStore.resetSettings();
18 | snakeStore.setRecord(0);
19 | userStore.setName('');
20 | userStore.setCity('');
21 |
22 | localStorage.clear();
23 |
24 | const { setPlannerItems } = usePlannerStore.getState();
25 | setPlannerItems(plannerItemsDefault);
26 | };
27 |
--------------------------------------------------------------------------------
/src/app/api/quotes/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 |
3 | const apiKey = process.env.QUOTES_API_KEY;
4 | const apiUrl = process.env.QUOTES_API_URL || '';
5 |
6 | export async function GET() {
7 | if (!apiKey) {
8 | return NextResponse.json({ error: 'API key is missing' }, { status: 500 });
9 | }
10 |
11 | try {
12 | const response = await fetch(apiUrl, {
13 | method: 'GET',
14 | headers: {
15 | 'X-Api-Key': apiKey,
16 | } as HeadersInit,
17 | });
18 |
19 | if (!response.ok) {
20 | const errorText = await response.text();
21 | return NextResponse.json(
22 | { error: `API response error: ${errorText}` },
23 | { status: response.status }
24 | );
25 | }
26 |
27 | const data = await response.json();
28 | return NextResponse.json(data);
29 | } catch (error) {
30 | console.error('Error fetching quotes:', error);
31 | return NextResponse.json(
32 | { error: 'Failed to fetch quotes' },
33 | { status: 500 }
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/theme/components/contentBox.tsx:
--------------------------------------------------------------------------------
1 | import useSettingsStore from '@/store/settingsStore';
2 | import { Flex, BoxProps, useColorMode } from '@chakra-ui/react';
3 | import { Ref } from 'react';
4 |
5 | interface ContentBoxProps extends BoxProps {
6 | children: React.ReactNode;
7 | ref?: Ref;
8 | }
9 |
10 | export const ContentBox = ({ children, ref, ...props }: ContentBoxProps) => {
11 | const { colorMode } = useColorMode();
12 | const theme = useSettingsStore((state) => state.theme);
13 |
14 | const boxShadowStyle =
15 | colorMode === 'light' && theme === 'basicTheme'
16 | ? { boxShadow: { base: "", lg: '0 3px 8px rgba(0,0,0,.74)' } }
17 | : {};
18 |
19 | return (
20 |
30 | {children}
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/PressureIcon.tsx:
--------------------------------------------------------------------------------
1 | export const PressureIcon = () =>
2 |
17 |
18 |
--------------------------------------------------------------------------------
/src/store/plannerStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | import {
4 | loadFromLocalStorage,
5 | saveToLocalStorage,
6 | } from '@/utils/localStorageUtils';
7 |
8 | export const plannerItemsDefault = [
9 | {
10 | text: 'Finish front-end project',
11 | isCrossed: false,
12 | },
13 | {
14 | text: 'Fix a printer',
15 | isCrossed: false,
16 | },
17 | {
18 | text: 'Buy new music album',
19 | isCrossed: false,
20 | },
21 | {
22 | text: 'Sort old photos',
23 | isCrossed: true,
24 | },
25 | {
26 | text: 'Feed the cat',
27 | isCrossed: false,
28 | },
29 | ];
30 |
31 | interface PlannerItem {
32 | text: string;
33 | isCrossed: boolean;
34 | }
35 |
36 | type PlannerStore = {
37 | plannerItems: PlannerItem[];
38 | setPlannerItems: (items: PlannerItem[]) => void;
39 | };
40 |
41 | const LOCAL_STORAGE_KEY = 'plannerItems';
42 |
43 | export const usePlannerStore = create((set) => ({
44 | plannerItems: loadFromLocalStorage(LOCAL_STORAGE_KEY, plannerItemsDefault),
45 | setPlannerItems: (items) => {
46 | saveToLocalStorage(LOCAL_STORAGE_KEY, items);
47 | set({ plannerItems: items });
48 | },
49 | }));
50 |
--------------------------------------------------------------------------------
/src/utils/localStorageUtils.ts:
--------------------------------------------------------------------------------
1 | export const saveToLocalStorage = (key: string, value: T) => {
2 | if (typeof window !== 'undefined') {
3 | try {
4 | const serializedValue = JSON.stringify(value);
5 | localStorage.setItem(key, serializedValue);
6 | } catch (error) {
7 | console.warn(`Error saving to localStorage for key "${key}":`, error);
8 | }
9 | }
10 | };
11 |
12 | export const loadFromLocalStorage = (key: string, initialValue: T): T => {
13 | if (typeof window === 'undefined') {
14 | return initialValue;
15 | }
16 |
17 | try {
18 | const storedData = localStorage.getItem(key);
19 | if (!storedData) {
20 | return initialValue;
21 | }
22 |
23 | const parsedData = JSON.parse(storedData) as T;
24 |
25 | // Additional type validation
26 | if (parsedData === null || parsedData === undefined) {
27 | return initialValue;
28 | }
29 |
30 | return parsedData;
31 | } catch (error) {
32 | // If there's any error parsing the data, clear the corrupted data
33 | localStorage.removeItem(key);
34 | console.warn(`Error loading data from localStorage for key "${key}":`, error);
35 | return initialValue;
36 | }
37 | };
--------------------------------------------------------------------------------
/src/assets/icons/WindIcon.tsx:
--------------------------------------------------------------------------------
1 | export const WindIcon = () =>
2 |
17 |
18 |
--------------------------------------------------------------------------------
/src/components/settings/SettingsModals.tsx:
--------------------------------------------------------------------------------
1 | import { useDisclosure } from '@chakra-ui/react';
2 | import { EditUserData } from '@/components/modals/EditUserDataModal';
3 | import { ClearAllData } from '@/components/modals/ClearAllDataModal';
4 |
5 | interface SettingsModalsProps {
6 | onSettingsPanelClose: () => void;
7 | }
8 |
9 | export const SettingsModals: React.FC = ({
10 | onSettingsPanelClose,
11 | }) => {
12 | const {
13 | isOpen: isEditUserDataOpen,
14 | onOpen: onEditUserDataOpen,
15 | onClose: onEditUserDataClose,
16 | } = useDisclosure();
17 | const {
18 | isOpen: isClearAllDataOpen,
19 | onOpen: onClearAllDataOpen,
20 | onClose: onClearAllDataClose,
21 | } = useDisclosure();
22 |
23 | return (
24 | <>
25 | {isEditUserDataOpen && (
26 | {
28 | onEditUserDataClose();
29 | onSettingsPanelClose();
30 | }}
31 | />
32 | )}
33 | {isClearAllDataOpen && (
34 | {
36 | onClearAllDataClose();
37 | onSettingsPanelClose();
38 | }}
39 | />
40 | )}
41 | >
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/hooks/useCurrentDate.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | export const useCurrentDate = () => {
4 | const [currentDate, setCurrentDate] = useState(new Date());
5 |
6 | useEffect(() => {
7 | const timer = setInterval(() => {
8 | setCurrentDate(new Date());
9 | }, 60 * 1000); // Update every minute
10 |
11 | return () => {
12 | clearInterval(timer);
13 | };
14 | }, []);
15 |
16 | const daysOfWeek = [
17 | 'Sunday',
18 | 'Monday',
19 | 'Tuesday',
20 | 'Wednesday',
21 | 'Thursday',
22 | 'Friday',
23 | 'Saturday',
24 | ];
25 |
26 | const dayOfWeek = daysOfWeek[currentDate.getDay()];
27 |
28 | const monthNames = [
29 | 'January',
30 | 'February',
31 | 'March',
32 | 'April',
33 | 'May',
34 | 'June',
35 | 'July',
36 | 'August',
37 | 'September',
38 | 'October',
39 | 'November',
40 | 'December',
41 | ];
42 |
43 | const monthName = monthNames[currentDate.getMonth()];
44 | const dayOfMonth = currentDate.getDate();
45 | const year = currentDate.getFullYear();
46 |
47 | return {
48 | dayOfWeek,
49 | dayOfMonth,
50 | monthName,
51 | year,
52 | };
53 | };
54 |
55 | export default useCurrentDate;
56 |
--------------------------------------------------------------------------------
/src/components/layout/SnakeButton.tsx:
--------------------------------------------------------------------------------
1 | import { SnakeIcon } from '@/assets/icons/SnakeIcon';
2 | import { ViewType } from '@/hooks/useHomepage';
3 | import { Button, Flex, Icon } from '@chakra-ui/react';
4 |
5 | interface SnakeButtonProps {
6 | handleToggleView: (view: ViewType, deviceType: 'mobile' | 'desktop') => void;
7 | desktopView: ViewType;
8 | }
9 |
10 | export const SnakeButton = ({
11 | handleToggleView,
12 | desktopView,
13 | }: SnakeButtonProps) => {
14 | const toggleView = () => {
15 | handleToggleView(
16 | desktopView === 'snake' ? 'dashboard' : 'snake',
17 | 'desktop'
18 | );
19 | };
20 | return (
21 |
29 |
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/src/store/notepadStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | import {
4 | loadFromLocalStorage,
5 | saveToLocalStorage,
6 | } from '@/utils/localStorageUtils';
7 |
8 | interface NotepadStore {
9 | storeNote: string;
10 | setStoreNote: (newText: string) => void;
11 | isNotepadModalConfirmed: boolean;
12 | setIsNotepadModalConfirmed: (value: boolean) => void;
13 | isModalVisible: boolean;
14 | setIsModalVisible: (value: boolean) => void;
15 | }
16 |
17 | const LOCAL_STORAGE_KEY = 'notepadStoreNote';
18 | const LOCAL_STORAGE_CONFIRMED_KEY = 'isNotepadModalConfirmed';
19 |
20 | export const useNotepadStore = create((set) => ({
21 | storeNote: loadFromLocalStorage(LOCAL_STORAGE_KEY, ''),
22 | setStoreNote: (newText: string) => {
23 | saveToLocalStorage(LOCAL_STORAGE_KEY, newText);
24 | set({ storeNote: newText });
25 | },
26 | isNotepadModalConfirmed: loadFromLocalStorage(
27 | LOCAL_STORAGE_CONFIRMED_KEY,
28 | false
29 | ),
30 | setIsNotepadModalConfirmed: (value: boolean) => {
31 | saveToLocalStorage(LOCAL_STORAGE_CONFIRMED_KEY, value);
32 | set({ isNotepadModalConfirmed: value });
33 | },
34 | isModalVisible: false,
35 | setIsModalVisible: (value: boolean) => set({ isModalVisible: value }),
36 | }));
37 |
--------------------------------------------------------------------------------
/src/components/views/homepage/weather/WeatherHourBox.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Text } from '@chakra-ui/react';
2 | import Image from 'next/image';
3 | import { useWeatherUtils } from '@/hooks/useWeatherUtils';
4 |
5 | export interface WeatherHourBoxProps {
6 | date?: string;
7 | hour?: string;
8 | icon?: string;
9 | weather?: { icon: string }[];
10 | dt?: string;
11 | temp?: string;
12 | }
13 |
14 | export const WeatherHourBox = ({
15 | date,
16 | hour,
17 | icon,
18 | temp,
19 | }: WeatherHourBoxProps) => {
20 | const { getWeatherImage } = useWeatherUtils();
21 | const iconUrl = icon ? getWeatherImage(icon) : '/images/weather/01d@2x.png';
22 |
23 | return (
24 |
34 | {date}
35 | {hour}:00
36 |
37 | {temp}°
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/assets/icons/HandIcon.tsx:
--------------------------------------------------------------------------------
1 | export const HandIcon = () =>
2 |
17 |
18 |
--------------------------------------------------------------------------------
/src/services/providers.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ChakraProvider } from '@chakra-ui/react';
4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5 | import { Analytics } from '@vercel/analytics/react';
6 | import { useState, useEffect } from 'react';
7 |
8 | import '@fontsource/roboto/100.css';
9 | import '@fontsource/quicksand';
10 | import '@fontsource/heebo/600.css';
11 | import '@fontsource/heebo/500.css';
12 | import '@fontsource/heebo/400.css';
13 | import '@fontsource/inter/600.css';
14 | import '@fontsource/inter/500.css';
15 | import '@fontsource/inter/400.css';
16 |
17 | import { generateTheme } from '@/theme/generateTheme';
18 | import { extendedColors } from '@/theme/extendedColors';
19 | import { colors } from '@/theme/colors';
20 | import useSettingsStore from '@/store/settingsStore';
21 |
22 | export function Providers({ children }: { children: React.ReactNode }) {
23 | const [queryClient] = useState(() => new QueryClient());
24 | const theme = useSettingsStore((state) => state.theme);
25 | const basicTheme = generateTheme(colors);
26 | const extendedTheme = generateTheme(extendedColors);
27 |
28 | return (
29 |
30 |
32 | {children}
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/settings/SettingsSlider.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Slider,
3 | SliderFilledTrack,
4 | SliderThumb,
5 | SliderTrack,
6 | Tooltip,
7 | } from '@chakra-ui/react';
8 | import { useState } from 'react';
9 |
10 | import useSettingsStore from '@/store/settingsStore';
11 |
12 | export const SettingsSlider = () => {
13 | const [showTooltip, setShowTooltip] = useState(false);
14 | const sliderValue = useSettingsStore((state) => state.sliderValue);
15 | const setSliderValue = useSettingsStore((state) => state.setSliderValue);
16 |
17 | return (
18 | setSliderValue(v)}
25 | onMouseEnter={() => setShowTooltip(true)}
26 | onMouseLeave={() => setShowTooltip(false)}
27 | mb="1.5rem"
28 | mt="1.2rem"
29 | position="relative"
30 | sx={{
31 | '& .chakra-slider__filled-track': {
32 | backgroundColor: 'settingsMainColor !important',
33 | },
34 | }}>
35 |
36 |
37 |
38 |
45 |
46 |
47 |
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mistyloop",
3 | "version": "1.0.2",
4 | "private": true,
5 | "license": "CC-BY-NC-ND-4.0",
6 | "description": "Application designed to be an alternative to default starting page in the browser",
7 | "author": {
8 | "name": "matt765",
9 | "email": "mateusz.wyrebek@gmail.com",
10 | "url": "https://matt765-portfolio.vercel.app/"
11 | },
12 | "scripts": {
13 | "dev": "next dev",
14 | "build": "next build",
15 | "start": "next start",
16 | "lint": "next lint"
17 | },
18 | "dependencies": {
19 | "@chakra-ui/react": "^2.6.1",
20 | "@emotion/react": "^11.10.8",
21 | "@emotion/styled": "^11.10.8",
22 | "@fontsource/heebo": "^4.5.15",
23 | "@fontsource/inter": "^4.5.15",
24 | "@fontsource/quicksand": "^4.5.12",
25 | "@fontsource/roboto": "^4.5.8",
26 | "@hello-pangea/dnd": "^18.0.1",
27 | "@tanstack/react-query": "^5.76.1",
28 | "@types/node": "^18.16.3",
29 | "@types/react": "^18.2.4",
30 | "@types/react-dom": "^18.2.3",
31 | "@vercel/analytics": "^1.0.1",
32 | "axios": "^1.4.0",
33 | "next": "^15.3.2",
34 | "query-string": "^8.1.0",
35 | "react": "^19.1.0",
36 | "react-dom": "^19.1.0",
37 | "typescript": "^5.0.4"
38 | },
39 | "devDependencies": {
40 | "eslint": "^8.39.0",
41 | "eslint-config-next": "^15.3.2",
42 | "eslint-config-prettier": "^8.8.0",
43 | "prettier": "^2.8.8",
44 | "zustand": "^4.3.8"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/views/notepad/NotepadTextArea.tsx:
--------------------------------------------------------------------------------
1 | import { Textarea } from '@chakra-ui/react';
2 |
3 | interface NotepadTextAreaProps {
4 | textAreaRef: React.RefObject;
5 | handleBoxClick: () => void;
6 | handleTextChange: () => void;
7 | onMouseEnter: () => void;
8 | onMouseLeave: () => void;
9 | initialValue?: string;
10 | }
11 |
12 | export const NotepadTextArea = ({
13 | textAreaRef,
14 | handleBoxClick,
15 | handleTextChange,
16 | onMouseEnter,
17 | onMouseLeave,
18 | initialValue = '',
19 | }: NotepadTextAreaProps) => {
20 | return (
21 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/src/theme/components/button.ts:
--------------------------------------------------------------------------------
1 | import { ComponentStyleConfig } from '@chakra-ui/react';
2 |
3 | export const Button: ComponentStyleConfig = {
4 | variants: {
5 | transparent: {
6 | bg: 'transparentButtonBg',
7 | borderColor: 'transparentButtonBorder',
8 | borderStyle: 'solid',
9 | borderWidth: '1px',
10 | borderRadius: '10px',
11 | _hover: {
12 | bg: 'transparentButtonHoverBg',
13 | },
14 | _active: {
15 | bg: 'transparentButtonActiveBg',
16 | },
17 | },
18 | round: {
19 | bg: 'sideButtonBg',
20 | borderRadius: '50%',
21 | borderWidth: '1px',
22 | borderColor: 'rgb(255,255,255,0.15)',
23 | borderStyle: 'solid',
24 | width: '3.5rem',
25 | height: '3.5rem',
26 | padding: "0",
27 | display: 'flex',
28 | justifyContent: 'center',
29 | alignItems: 'center',
30 | _hover: {
31 | bg: 'sideButtonHoverBg',
32 | },
33 | _active: {
34 | bg: 'sideButtonHoverBg',
35 | },
36 | },
37 | settingsUserData: {
38 | bg: 'transparentButtonBg',
39 | borderColor: 'transparentButtonBorder',
40 | borderStyle: 'solid',
41 | borderWidth: '1px',
42 | borderRadius: '10px',
43 | fontSize: { base: '0.8rem', '3xl': '0.9rem' },
44 | paddingBottom: "0.1rem",
45 | _hover: {
46 | bg: 'transparentButtonHoverBg',
47 | },
48 | _active: {
49 | bg: 'transparentButtonActiveBg',
50 | },
51 | }
52 | },
53 | };
54 |
--------------------------------------------------------------------------------
/src/app/api/weather/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 |
3 | const apiKey = process.env.WEATHER_API_KEY;
4 |
5 | export async function GET(request: NextRequest) {
6 | const searchParams = request.nextUrl.searchParams;
7 | const cityValue = searchParams.get('cityValue');
8 | const lat = searchParams.get('lat');
9 | const lon = searchParams.get('lon');
10 |
11 | let query: string;
12 |
13 | if (cityValue) {
14 | query = cityValue;
15 | } else if (lat && lon) {
16 | query = `${lat},${lon}`;
17 | } else {
18 | return NextResponse.json(
19 | { error: 'cityValue or lat and lon are required' },
20 | { status: 400 }
21 | );
22 | }
23 |
24 | try {
25 | const response = await fetch(
26 | `${process.env.WEATHER_API_URL}?key=${apiKey}&q=${query}&days=2`
27 | );
28 |
29 | if (!response.ok) {
30 | const errorText = await response.text();
31 | return NextResponse.json(
32 | { error: `Weather API response error: ${errorText}` },
33 | { status: response.status }
34 | );
35 | }
36 |
37 | const data = await response.json();
38 |
39 | if (data.error) {
40 | return NextResponse.json(
41 | { error: data.error.message },
42 | { status: data.error.code || 400 }
43 | );
44 | }
45 |
46 | return NextResponse.json(data);
47 | } catch (error) {
48 | console.error('Error fetching weather data:', error);
49 | return NextResponse.json(
50 | { error: 'Failed to fetch weather data' },
51 | { status: 500 }
52 | );
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/hooks/useEditUserData.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | import { useUserStoreWrapper } from '@/store/userStore';
4 | import { useWeatherStore } from '@/store/weatherStore';
5 | import { saveToLocalStorage } from '@/utils/localStorageUtils';
6 | import { fetchWeatherData } from '@/services/fetchWeatherData';
7 |
8 | const LOCAL_STORAGE_KEY = 'weatherStoreData';
9 |
10 | export const useEditUserData = (onClose: () => void) => {
11 | const {
12 | name: storeName,
13 | city: storeCity,
14 | setName,
15 | setCity,
16 | } = useUserStoreWrapper();
17 | const { setWeatherData } = useWeatherStore();
18 |
19 | const [name, setNameInput] = useState(storeName);
20 | const [city, setCityInput] = useState(storeCity);
21 | const [isSubmitting, setIsSubmitting] = useState(false);
22 | const [isError, setIsError] = useState(false);
23 |
24 | const handleSubmit = async (e: React.FormEvent) => {
25 | e.preventDefault();
26 | setIsSubmitting(true);
27 | setIsError(false);
28 | try {
29 | const weatherData = await fetchWeatherData(city);
30 | if (!weatherData) {
31 | throw new Error('City not found');
32 | }
33 | saveToLocalStorage(LOCAL_STORAGE_KEY, weatherData);
34 | setWeatherData(weatherData, false);
35 | setName(name);
36 | setCity(city);
37 | onClose();
38 | } catch (error: unknown) {
39 | setIsError(true);
40 | setWeatherData(null, true);
41 | } finally {
42 | setIsSubmitting(false);
43 | }
44 | };
45 |
46 | return {
47 | name,
48 | city,
49 | setNameInput,
50 | setCityInput,
51 | handleSubmit,
52 | isSubmitting,
53 | isError,
54 | };
55 | };
56 |
--------------------------------------------------------------------------------
/src/components/settings/SettingsGithub.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Flex, Icon, Link } from '@chakra-ui/react';
2 |
3 | import { GithubIcon } from '@/assets/icons/GithubIcon';
4 |
5 | export const SettingsGithub = () => {
6 | return (
7 |
29 |
42 |
51 |
52 |
53 |
54 | GitHub Repository
55 |
56 |
57 |
58 | );
59 | };
60 |
--------------------------------------------------------------------------------
/src/services/fetchWeatherData.ts:
--------------------------------------------------------------------------------
1 | import { WeatherData } from '@/hooks/useWeatherData';
2 | import { getCountryCode } from '@/utils/countryMapping';
3 |
4 | export const fetchWeatherData = async (
5 | cityValue: string
6 | ): Promise => {
7 | try {
8 | const response = await fetch(`/api/weather?cityValue=${cityValue}`);
9 | if (!response.ok) {
10 | throw new Error(await response.text());
11 | }
12 | const data = await response.json();
13 |
14 | const weatherData: WeatherData = {
15 | // Use the current temperature (already in Celsius) and round it
16 | temp: Math.round(data.current.temp_c).toString(),
17 | // Weather description from the current condition
18 | desc: data.current.condition.text,
19 | // Convert humidity to string
20 | humidity: data.current.humidity.toString(),
21 | // Use "feels like" (in °C) for the "Feels like" parameter
22 | rain: Math.round(data.current.feelslike_c).toString(),
23 | // Pressure in millibars
24 | pressure: data.current.pressure_mb.toString(),
25 | // Country comes from the location object
26 | country: getCountryCode(data.location.country),
27 | // Wind speed in kph (as a string with one decimal)
28 | wind: data.current.wind_kph.toFixed(1),
29 | // Hourly forecast array from the first forecast day
30 | hourTemp: data.forecast.forecastday[0].hour,
31 | // Icon URL from the current condition (if needed, prepend "https:" if required)
32 | icon: data.current.condition.icon,
33 | // Use the timezone id (or region) from the location object
34 | region: data.location.tz_id || data.location.region,
35 | };
36 | return weatherData;
37 | } catch (error) {
38 | throw error;
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/src/store/userStore.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { create } from 'zustand';
4 | import { useEffect, useState } from 'react';
5 | import {
6 | loadFromLocalStorage,
7 | saveToLocalStorage,
8 | } from '@/utils/localStorageUtils';
9 |
10 | type UserState = {
11 | name: string;
12 | city: string;
13 | setName: (name: string) => void;
14 | setCity: (city: string) => void;
15 | clearUserData: () => void;
16 | isMounted: boolean;
17 | firstMount: boolean;
18 | setFirstMount: (value: boolean) => void;
19 | };
20 |
21 | export const useUserStore = create((set) => ({
22 | name: '',
23 | city: '',
24 | setName: (name) => {
25 | saveToLocalStorage('userStoreName', name);
26 | set({ name });
27 | },
28 | setCity: (city) => {
29 | saveToLocalStorage('userStoreCity', city);
30 | set({ city });
31 | },
32 | clearUserData: () => {
33 | localStorage.removeItem('userStoreName');
34 | localStorage.removeItem('userStoreCity');
35 | localStorage.removeItem('currentView');
36 | localStorage.removeItem('currentMobileView');
37 | set({ name: '', city: '' });
38 | },
39 | isMounted: false,
40 | firstMount: true,
41 | setFirstMount: (value) => set({ firstMount: value }),
42 | }));
43 |
44 | export const useUserStoreWrapper = () => {
45 | const { name, city, setName, setCity, clearUserData, isMounted } =
46 | useUserStore();
47 | const [mounted, setMounted] = useState(false);
48 |
49 | useEffect(() => {
50 | if (!mounted) {
51 | const loadedName = loadFromLocalStorage('userStoreName', '');
52 | const loadedCity = loadFromLocalStorage('userStoreCity', '');
53 | setName(loadedName);
54 | setCity(loadedCity);
55 | setMounted(true);
56 | }
57 | }, [mounted, setName, setCity]);
58 |
59 | return { name, city, setName, setCity, clearUserData, isMounted: mounted };
60 | };
61 |
--------------------------------------------------------------------------------
/src/components/common/PageWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, BoxProps, useColorMode, useMediaQuery } from '@chakra-ui/react';
2 | import { Ref, ReactNode } from 'react';
3 | import useSettingsStore from '@/store/settingsStore';
4 | import { usePathname } from 'next/navigation';
5 |
6 | interface PageWrapperProps extends BoxProps {
7 | children: ReactNode;
8 | ref?: Ref;
9 | }
10 |
11 | export const PageWrapper: React.FC = ({
12 | children,
13 | ref,
14 | ...props
15 | }: PageWrapperProps) => {
16 | const { colorMode } = useColorMode();
17 | const theme = useSettingsStore((state) => state.theme);
18 | const pathname = usePathname();
19 | const isHomepage = pathname === '/';
20 |
21 | const boxShadowStyle =
22 | colorMode === 'light' && theme === 'basicTheme' && !isHomepage
23 | ? { boxShadow: { base: '', lg: '0 3px 8px rgba(0,0,0,.74)' } }
24 | : {};
25 |
26 | const [isDesktop] = useMediaQuery('(min-width: 1280px)');
27 |
28 | if (!isDesktop) {
29 | return (
30 |
31 | {children}
32 |
33 | );
34 | }
35 |
36 | return (
37 |
38 |
52 | {children}
53 |
54 |
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/src/hooks/useSettings.ts:
--------------------------------------------------------------------------------
1 | // hooks/useSettings.ts
2 | import { useColorMode } from '@chakra-ui/react';
3 | import { useState } from 'react';
4 |
5 | import useSettingsStore, {
6 | WelcomeSectionContentType,
7 | } from '@/store/settingsStore';
8 |
9 | export const useSettings = () => {
10 | const {
11 | isFullPlannerVisible,
12 | setFullPlannerVisible,
13 | useFahrenheit,
14 | setUseFahrenheit,
15 | welcomeSectionContent,
16 | setWelcomeSectionContent,
17 | showSnakeButton,
18 | setShowSnakeButton,
19 | theme,
20 | setTheme,
21 | } = useSettingsStore((state) => state);
22 |
23 | const { colorMode, setColorMode } = useColorMode();
24 | const [themeValue, setThemeValue] = useState(`${colorMode}_${theme}`);
25 |
26 | const handleUseFahrenheitChange = () => {
27 | setUseFahrenheit(!useFahrenheit);
28 | };
29 |
30 | const handleFullPlannerVisibleChange = () => {
31 | setFullPlannerVisible(!isFullPlannerVisible);
32 | };
33 |
34 | const handleShowSnakeButtonChange = () => {
35 | setShowSnakeButton(!showSnakeButton);
36 | };
37 |
38 | const handleRadioChange = (newValue: string) => {
39 | setWelcomeSectionContent(newValue as WelcomeSectionContentType);
40 | };
41 |
42 | // This solution will likely be refactored if ChakraUI will introduce native support for more than 2 color modes.
43 | const handleThemeChange = (newTheme: string) => {
44 | const [newColorMode, newThemeName] = newTheme.split('_');
45 | setColorMode(newColorMode);
46 | setTheme(newThemeName);
47 | setThemeValue(newTheme);
48 | };
49 |
50 | return {
51 | theme,
52 | themeValue,
53 | setThemeValue,
54 | colorMode,
55 | isFullPlannerVisible,
56 | useFahrenheit,
57 | welcomeSectionContent,
58 | showSnakeButton,
59 | handleUseFahrenheitChange,
60 | handleFullPlannerVisibleChange,
61 | handleShowSnakeButtonChange,
62 | handleRadioChange,
63 | handleThemeChange,
64 | };
65 | };
66 |
--------------------------------------------------------------------------------
/src/hooks/useIntro.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | import { useUserStore } from '@/store/userStore';
4 | import { useWeatherStore } from '@/store/weatherStore';
5 | import {
6 | loadFromLocalStorage,
7 | saveToLocalStorage,
8 | } from '@/utils/localStorageUtils';
9 | import { fetchWeatherData } from '@/services/fetchWeatherData';
10 |
11 | interface UseIntro {
12 | name: string;
13 | city: string;
14 | setName: (name: string) => void;
15 | setCity: (city: string) => void;
16 | handleSubmit: (e: React.FormEvent) => void;
17 | isSubmitting: boolean;
18 | isError: boolean;
19 | }
20 |
21 | const LOCAL_STORAGE_KEY = 'weatherStoreData';
22 |
23 | export const useIntro = (): UseIntro => {
24 | const [name, setName] = useState('');
25 | const [city, setCity] = useState('');
26 | const [isSubmitting, setIsSubmitting] = useState(false);
27 | const [isError, setIsError] = useState(false);
28 |
29 | const { setName: setNameInStore, setCity: setCityInStore } = useUserStore();
30 | const { setWeatherData } = useWeatherStore();
31 |
32 | const handleSubmit = async (e: React.FormEvent) => {
33 | e.preventDefault();
34 | setIsSubmitting(true);
35 | setIsError(false);
36 | setWeatherData(loadFromLocalStorage(LOCAL_STORAGE_KEY, null), false);
37 | try {
38 | const weatherData = await fetchWeatherData(city);
39 | if (!weatherData) {
40 | throw new Error('City not found');
41 | }
42 | saveToLocalStorage(LOCAL_STORAGE_KEY, weatherData);
43 | setWeatherData(weatherData, false);
44 | setNameInStore(name);
45 | setCityInStore(city);
46 | } catch (error: unknown) {
47 | setIsError(true);
48 | setWeatherData(null, true);
49 | } finally {
50 | setIsSubmitting(false);
51 | }
52 | };
53 |
54 | return {
55 | name,
56 | city,
57 | setName,
58 | setCity,
59 | handleSubmit,
60 | isSubmitting,
61 | isError,
62 | };
63 | };
64 |
--------------------------------------------------------------------------------
/src/components/common/Loader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Flex } from '@chakra-ui/react';
3 | import { keyframes } from '@emotion/react';
4 | import type { SystemStyleObject } from '@chakra-ui/styled-system';
5 |
6 | const spinAnimation = keyframes`
7 | 0% {
8 | opacity: 1;
9 | }
10 | 100% {
11 | opacity: 0;
12 | }
13 | `;
14 |
15 | interface LoaderProps {
16 | isSmall?: boolean;
17 | }
18 |
19 | export const Loader = ({ isSmall = false }: LoaderProps) => {
20 | const size = isSmall ? 35 : 80;
21 | const origin = size / 2;
22 | const barWidth = isSmall ? 2.65 : 6;
23 | const barHeight = isSmall ? 7.25 : 18;
24 | const barLeft = origin - barWidth / 2;
25 |
26 | const boxStyle: SystemStyleObject = {
27 | transformOrigin: `${origin}px ${origin}px`,
28 | animation: `${spinAnimation} 1.2s linear infinite`,
29 | animationDelay: '0s',
30 | transform: 'rotate(0deg)',
31 | };
32 |
33 | return (
34 |
41 |
42 | {Array.from({ length: 12 }).map((_, index) => {
43 | const individualStyle: SystemStyleObject = {
44 | ...boxStyle,
45 | animationDelay: `${-1.1 + index * 0.1}s`,
46 | transform: `rotate(${index * 30}deg)`,
47 | };
48 |
49 | return (
50 |
51 |
61 |
62 | );
63 | })}
64 |
65 |
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/src/theme/generateTheme.ts:
--------------------------------------------------------------------------------
1 | import { extendTheme, type ThemeConfig } from '@chakra-ui/react';
2 |
3 | import { Button } from './components/button';
4 | import { Text } from './components/text';
5 |
6 | export const generateTheme = (colors: any) => {
7 | const config: ThemeConfig = {
8 | initialColorMode: 'dark',
9 | useSystemColorMode: false,
10 | };
11 |
12 | return extendTheme({
13 | config,
14 | breakpoints: {
15 | sm: '30em', // 480px
16 | md: '48em', // 768px
17 | lg: '62em', // 992px
18 | xl: '80em', // 1280px
19 | '2xl': '102em', // 1632px
20 | '3xl': '110em', // 1760px
21 | },
22 | semanticTokens: {
23 | colors: {
24 | ...colors,
25 | },
26 | },
27 | styles: {
28 | global: {
29 | body: {
30 | color: 'primaryText',
31 | padding: 0,
32 | margin: 0,
33 | },
34 | html: {
35 | scrollBehavior: 'smooth',
36 | padding: 0,
37 | margin: 0,
38 | },
39 | '*': {
40 | boxSizing: 'border-box',
41 | maxWidth: '100vw',
42 | fontFamily: 'Quicksand',
43 | scrollbarWidth: 'thin',
44 | scrollbarColor: 'red',
45 | '&::-webkit-scrollbar': {
46 | width: '10px',
47 | },
48 | '&::-webkit-scrollbar-thumb': {
49 | background: 'rgb(255,255,255,0.1)',
50 | _hover: {
51 | background: 'rgb(255,255,255,0.1)',
52 | },
53 | borderRadius: '30px',
54 | border: 'none',
55 | },
56 | '&::-webkit-scrollbar-track': {
57 | background: 'transparent',
58 | },
59 | },
60 | ':root': {
61 | scrollbarColor: 'rgb(255,255,255,0.1) rgba(255, 255, 255, 0.0)',
62 | scrollbarWidth: 'thin',
63 | },
64 | '::-webkit-input-placeholder': {
65 | color: 'rgb(255,255,255,0.4) !important',
66 | },
67 | 'option, optgroup': {
68 | WebkitAppearance: 'none !important',
69 | },
70 | },
71 | },
72 | components: {
73 | Text,
74 | Button,
75 | },
76 | });
77 | };
78 |
--------------------------------------------------------------------------------
/src/hooks/useNotepad.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react';
2 |
3 | import { useNotepadStore } from '@/store/notepadStore';
4 | import { useClickOutside } from '@/hooks/useClickOutside';
5 | import { useColorMode } from '@chakra-ui/react';
6 |
7 | export const useNotepad = () => {
8 | const [editing, setEditing] = useState(false);
9 | const [isModified, setIsModified] = useState(false);
10 | const [hovered, setHovered] = useState(false);
11 | const textAreaRef = useRef(null);
12 | const {
13 | storeNote,
14 | setStoreNote,
15 | isNotepadModalConfirmed,
16 | setIsNotepadModalConfirmed,
17 | isModalVisible,
18 | setIsModalVisible,
19 | } = useNotepadStore();
20 |
21 | useEffect(() => {
22 | if (!isNotepadModalConfirmed) {
23 | setIsModalVisible(true);
24 | }
25 | }, [isNotepadModalConfirmed, setIsModalVisible]);
26 |
27 | useEffect(() => {
28 | if (textAreaRef.current) {
29 | textAreaRef.current.value = storeNote;
30 | }
31 | }, [storeNote]);
32 | const handleBoxClick = () => {
33 | if (!editing) {
34 | setEditing(true);
35 | }
36 | };
37 | const handleSave = () => {
38 | if (textAreaRef.current) {
39 | const updatedNote = textAreaRef.current.value;
40 | setStoreNote(updatedNote);
41 | setEditing(false);
42 | setIsModified(false);
43 | }
44 | };
45 | const handleModalClose = (save: boolean) => {
46 | setIsModalVisible(false);
47 | if (save) {
48 | setIsNotepadModalConfirmed(true);
49 | }
50 | };
51 | const handleTextChange = () => {
52 | if (textAreaRef.current && textAreaRef.current.value !== storeNote) {
53 | setIsModified(true);
54 | } else {
55 | setIsModified(false);
56 | }
57 | };
58 | useClickOutside(textAreaRef, () => {
59 | if (editing && textAreaRef.current) {
60 | const updatedNote = textAreaRef.current.value;
61 | setStoreNote(updatedNote);
62 | setIsModified(false);
63 | setEditing(false);
64 | }
65 | });
66 | const { colorMode } = useColorMode();
67 | return {
68 | textAreaRef,
69 | editing,
70 | isModified,
71 | handleBoxClick,
72 | handleSave,
73 | handleTextChange,
74 | handleModalClose,
75 | isModalVisible,
76 | colorMode,
77 | hovered,
78 | setHovered,
79 | storeNote,
80 | };
81 | };
82 |
--------------------------------------------------------------------------------
/src/hooks/useWeatherData.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query';
2 | import { HandIcon } from '@/assets/icons/HandIcon';
3 | import { HumidityIcon } from '@/assets/icons/HumidityIcon';
4 | import { PressureIcon } from '@/assets/icons/PressureIcon';
5 | import { WindIcon } from '@/assets/icons/WindIcon';
6 | import useSettingsStore from '@/store/settingsStore';
7 | import { useWeatherUtils } from './useWeatherUtils';
8 | import { fetchWeatherData } from '@/services/fetchWeatherData';
9 |
10 | export interface HourlyData {
11 | time_epoch: number;
12 | temp_c: number;
13 | condition: {
14 | icon: string;
15 | text: string;
16 | code: number;
17 | };
18 | }
19 | export interface WeatherData {
20 | temp: string;
21 | desc: string;
22 | humidity: string;
23 | rain: string;
24 | pressure: string;
25 | country: string;
26 | wind: string;
27 | hourTemp: HourlyData[];
28 | icon: string;
29 | region: string;
30 | }
31 |
32 | export const useWeatherData = (cityValue: string) => {
33 | const useFahrenheit = useSettingsStore((state) => state.useFahrenheit);
34 | const { toCelsius, toFahrenheit } = useWeatherUtils();
35 |
36 | const queryResult = useQuery({
37 | queryKey: ['weatherData', cityValue],
38 | queryFn: () => fetchWeatherData(cityValue),
39 | staleTime: Infinity,
40 | });
41 |
42 | if (!queryResult.data) {
43 | return {
44 | data: null,
45 | isLoading: queryResult.isLoading,
46 | isError: true,
47 | weatherParameters: null,
48 | };
49 | }
50 | const weatherParameters = [
51 | {
52 | icon: HumidityIcon,
53 | title: 'Humidity',
54 | value: `${queryResult.data?.humidity}%`,
55 | },
56 | {
57 | icon: HandIcon,
58 | title: 'Feels like',
59 | value: `${
60 | useFahrenheit
61 | ? toFahrenheit(queryResult.data?.rain)
62 | : toCelsius(queryResult.data?.rain)
63 | }°`,
64 | },
65 | {
66 | icon: PressureIcon,
67 | title: 'Air pressure',
68 | value: `${queryResult.data?.pressure} hPa`,
69 | },
70 | {
71 | icon: WindIcon,
72 | title: 'Wind speed',
73 | value: `${queryResult.data?.wind} km/h`,
74 | },
75 | ];
76 |
77 | return {
78 | data: queryResult.data,
79 | isLoading: queryResult.isLoading,
80 | isError: queryResult.isError,
81 | weatherParameters,
82 | };
83 | };
84 |
--------------------------------------------------------------------------------
/src/components/views/homepage/Homepage.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, useColorMode } from '@chakra-ui/react';
2 |
3 | import { ContentBox } from '@/theme/components/contentBox';
4 | import useSettingsStore from '@/store/settingsStore';
5 | import { Weather } from './weather/Weather';
6 | import { Planner } from './planner/Planner';
7 | import { Welcome } from './welcome/Welcome';
8 |
9 | const BlurOverlay = () => (
10 |
21 | );
22 |
23 | export const Homepage = () => {
24 | const isFullPlannerVisible = useSettingsStore(
25 | (state) => state.isFullPlannerVisible
26 | );
27 | const { colorMode } = useColorMode();
28 |
29 | return (
30 |
31 | {!isFullPlannerVisible && (
32 |
38 |
43 |
44 | {colorMode === 'dark' && }
45 |
46 |
51 |
52 | {colorMode === 'dark' && }
53 |
54 |
55 | )}
56 |
65 |
66 | {colorMode === 'dark' && }
67 |
68 |
69 |
70 | );
71 | };
72 |
--------------------------------------------------------------------------------
/src/assets/icons/SnakeIcon.tsx:
--------------------------------------------------------------------------------
1 | export const SnakeIcon = () => (
2 |
28 | );
29 |
--------------------------------------------------------------------------------
/src/components/layout/MobileView.tsx:
--------------------------------------------------------------------------------
1 | import { Flex } from '@chakra-ui/react';
2 |
3 | import { Notepad } from '@/components/views/notepad/Notepad';
4 | import { Weather } from '../views/homepage/weather/Weather';
5 | import { Planner } from '../views/homepage/planner/Planner';
6 | import { Welcome } from '../views/homepage/welcome/Welcome';
7 | import { useMobileViewStore } from '@/store/mobileViewStore';
8 | import { ViewType } from '@/hooks/useHomepage';
9 |
10 | const WelcomeMobileView = () => (
11 |
23 |
24 |
25 |
26 | );
27 |
28 | const WeatherMobileView = () => (
29 |
40 |
41 |
42 | );
43 |
44 | const PlannerMobileView = () => (
45 |
53 |
54 |
55 | );
56 |
57 | const NotepadMobileView = () => (
58 |
67 |
68 |
69 | );
70 |
71 | const SettingsMobileView = () => (
72 |
73 |
74 |
75 | );
76 |
77 | export const MobileView = () => {
78 | const { mobileView } = useMobileViewStore();
79 |
80 | switch (mobileView) {
81 | case 'mobileHome':
82 | return ;
83 | case 'mobileWeather':
84 | return ;
85 | case 'mobilePlanner':
86 | return ;
87 | case 'notepad':
88 | return ;
89 | case 'settings':
90 | return ;
91 | default:
92 | console.log('View not found, defaulting to WelcomeMobileView');
93 | return ;
94 | }
95 | };
96 |
--------------------------------------------------------------------------------
/src/components/views/homepage/planner/PlannerHeader.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Flex, Input, Tooltip } from '@chakra-ui/react';
2 | import React, { useRef } from 'react';
3 |
4 | import { useClickOutside } from '@/hooks/useClickOutside';
5 |
6 | interface PlannerHeaderProps {
7 | inputValue: string;
8 | setInputValue: (value: string) => void;
9 | addTask: (e: React.FormEvent) => void;
10 | showTooltip: boolean;
11 | setShowTooltip: (value: boolean) => void;
12 | }
13 |
14 | export const PlannerHeader = ({
15 | inputValue,
16 | setInputValue,
17 | addTask,
18 | showTooltip,
19 | setShowTooltip,
20 | }: PlannerHeaderProps) => {
21 | const ref = useRef(null);
22 | useClickOutside(ref, () => setShowTooltip(false));
23 |
24 | return (
25 |
71 | );
72 | };
73 |
--------------------------------------------------------------------------------
/src/hooks/useWelcome.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useQuery, useQueryClient } from '@tanstack/react-query';
3 |
4 | import useCurrentDate from '@/hooks/useCurrentDate';
5 | import useSettingsStore from '@/store/settingsStore';
6 | import { useUserStore } from '@/store/userStore';
7 | import { fetchFact } from '@/services/fetchFact';
8 | import { fetchQuote } from '@/services/fetchQuote';
9 |
10 | export const useWelcome = () => {
11 | const userName = useUserStore((state) => state.name);
12 | const { dayOfWeek, dayOfMonth, monthName, year } = useCurrentDate();
13 | const [contentMode, setContentMode] = useState('did_you_know');
14 |
15 | const [isRefetchingContent, setIsRefetchingContent] = useState(false);
16 | const queryClient = useQueryClient();
17 |
18 | const {
19 | isLoading,
20 | error,
21 | data: fact,
22 | } = useQuery({
23 | queryKey: ['fact'],
24 | queryFn: fetchFact,
25 | refetchOnWindowFocus: false,
26 | staleTime: Infinity,
27 | });
28 |
29 | const {
30 | isLoading: isLoadingQuote,
31 | error: errorQuote,
32 | data: quoteData,
33 | } = useQuery({
34 | queryKey: ['quote'],
35 | queryFn: fetchQuote,
36 | refetchOnWindowFocus: false,
37 | staleTime: Infinity,
38 | });
39 |
40 | const quote = quoteData?.quote || '';
41 | const author = quoteData?.author || '';
42 |
43 | const [refreshCooldown, setRefreshCooldown] = useState(false);
44 |
45 | const refetchContent = async () => {
46 | if (refreshCooldown) {
47 | return;
48 | }
49 | setIsRefetchingContent(true);
50 | setRefreshCooldown(true);
51 |
52 | const sleep = (ms: number) =>
53 | new Promise((resolve) => setTimeout(resolve, ms));
54 |
55 | try {
56 | await Promise.all([
57 | contentMode === 'did_you_know'
58 | ? queryClient.invalidateQueries({ queryKey: ['fact'] })
59 | : queryClient.invalidateQueries({ queryKey: ['quote'] }),
60 | sleep(1500),
61 | ]);
62 | } catch (error) {
63 | console.error('Error refetching content:', error);
64 | } finally {
65 | setIsRefetchingContent(false);
66 | setRefreshCooldown(false);
67 | }
68 | };
69 |
70 | const welcomeSectionContent = useSettingsStore(
71 | (state) => state.welcomeSectionContent
72 | );
73 |
74 | useEffect(() => {
75 | setContentMode(welcomeSectionContent);
76 | }, [welcomeSectionContent]);
77 |
78 | return {
79 | userName,
80 | dayOfWeek,
81 | dayOfMonth,
82 | monthName,
83 | year,
84 | contentMode,
85 | isLoading,
86 | error,
87 | fact,
88 | isLoadingQuote,
89 | errorQuote,
90 | quote,
91 | author,
92 | isRefetchingContent,
93 | refetchContent,
94 | };
95 | };
96 |
--------------------------------------------------------------------------------
/src/components/views/notepad/Notepad.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Box, Icon, Button, Flex } from '@chakra-ui/react';
4 | import { NotepadEditIcon } from '@/assets/icons/NotepadEditIcon';
5 | import { useNotepad } from '@/hooks/useNotepad';
6 | import { NotepadTextArea } from '@/components/views/notepad/NotepadTextArea';
7 | import { CheckIcon } from '@/assets/icons/CheckIcon';
8 |
9 | export const Notepad = () => {
10 | const {
11 | textAreaRef,
12 | editing,
13 | isModified,
14 | handleBoxClick,
15 | handleSave,
16 | handleTextChange,
17 | hovered,
18 | setHovered,
19 | storeNote,
20 | } = useNotepad();
21 |
22 | return (
23 |
29 | {!editing && (
30 |
44 | {!storeNote && (
45 |
51 | )}
52 |
53 | )}
54 | setHovered(true)}
59 | onMouseLeave={() => setHovered(false)}
60 | initialValue={storeNote}
61 | />
62 |
63 |
88 |
89 |
90 | );
91 | };
92 |
--------------------------------------------------------------------------------
/src/store/settingsStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | import {
4 | loadFromLocalStorage,
5 | saveToLocalStorage,
6 | } from '@/utils/localStorageUtils';
7 |
8 | export type WelcomeSectionContentType = 'did_you_know' | 'quotes';
9 |
10 | type SettingsState = {
11 | isFullPlannerVisible: boolean;
12 | setFullPlannerVisible: (isFullPlannerVisible: boolean) => void;
13 | welcomeSectionContent: WelcomeSectionContentType;
14 | setWelcomeSectionContent: (
15 | welcomeSectionContent: WelcomeSectionContentType
16 | ) => void;
17 | sliderValue: number;
18 | setSliderValue: (value: number) => void;
19 | showSnakeButton: boolean;
20 | setShowSnakeButton: (showSnakeButton: boolean) => void;
21 | useFahrenheit: boolean;
22 | setUseFahrenheit: (useFahrenheit: boolean) => void;
23 | resetSettings: () => void;
24 | theme: string;
25 | setTheme: (theme: string) => void;
26 | isImageVisible: boolean;
27 | setIsImageVisible: (isImageVisible: boolean) => void;
28 | };
29 |
30 | export const useSettingsStore = create((set) => {
31 | const setAndStore = (
32 | key: K,
33 | value: SettingsState[K]
34 | ) => {
35 | saveToLocalStorage(key, value);
36 | set({ [key]: value } as Pick);
37 | };
38 |
39 | return {
40 | isFullPlannerVisible: loadFromLocalStorage('isFullPlannerVisible', false),
41 | setFullPlannerVisible: (value: boolean) =>
42 | setAndStore('isFullPlannerVisible', value),
43 | welcomeSectionContent: loadFromLocalStorage(
44 | 'welcomeSectionContent',
45 | 'did_you_know'
46 | ),
47 | setWelcomeSectionContent: (value: WelcomeSectionContentType) =>
48 | setAndStore('welcomeSectionContent', value),
49 | sliderValue: loadFromLocalStorage('sliderValue', 40),
50 | setSliderValue: (value: number) => setAndStore('sliderValue', value),
51 | showSnakeButton: loadFromLocalStorage('showSnakeButton', true),
52 | setShowSnakeButton: (value: boolean) =>
53 | setAndStore('showSnakeButton', value),
54 | useFahrenheit: loadFromLocalStorage('useFahrenheit', false),
55 | setUseFahrenheit: (value: boolean) => setAndStore('useFahrenheit', value),
56 | theme: loadFromLocalStorage('theme', 'extendedTheme'),
57 | setTheme: (value: string) => setAndStore('theme', value),
58 | isImageVisible: loadFromLocalStorage('isImageVisible', true),
59 | setIsImageVisible: (value: boolean) => setAndStore('isImageVisible', value),
60 | resetSettings: () => {
61 | setAndStore('isFullPlannerVisible', false);
62 | setAndStore('welcomeSectionContent', 'did_you_know');
63 | setAndStore('sliderValue', 40);
64 | setAndStore('showSnakeButton', true);
65 | setAndStore('useFahrenheit', false);
66 | setAndStore('theme', 'extendedTheme');
67 | setAndStore('isImageVisible', true);
68 | },
69 | };
70 | });
71 |
72 | export default useSettingsStore;
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - MistyLoop -
5 |
6 |
7 |
8 |
9 |
20 |
21 | Application designed to be an alternative to default starting page in the browser
22 |
23 |
24 |
25 |

26 |
27 |
28 | ## ⚙️ Tech stack
29 |
30 | React 19, NextJS 15, TypeScript, ChakraUI, Zustand, React Query
31 |
32 | ## 🔗 Live link
33 |
34 | [https://mistyloop.com/](https://mistyloop.com/)
35 |
36 | ## 📁 Project Structure
37 |
38 | ```
39 | ├── src
40 | │ ├── app
41 | │ │ ├── api
42 | │ │ ├── notepad
43 | │ │ └── snake
44 | │ ├── assets
45 | │ │ ├── icons
46 | │ │ └── images
47 | │ ├── components
48 | │ │ ├── common
49 | │ │ ├── layout
50 | │ │ ├── modals
51 | │ │ ├── settings
52 | │ │ └── views
53 | │ │ ├── homepage
54 | │ │ ├── intro
55 | │ │ ├── notepad
56 | │ │ └── snake
57 | │ ├── hooks
58 | │ ├── services
59 | │ ├── store
60 | │ ├── theme
61 | │ │ └── components
62 | │ └── utils
63 | └── package.json
64 | ```
65 |
66 | ## ✨ Features
67 |
68 | - Current day and date
69 | - Weather forecast
70 | - "Did you know?" facts
71 | - Quotes
72 | - Task list
73 | - Notepad
74 | - Snake game (desktop only)
75 | - Themes
76 | - Customization through settings panel
77 |
78 | ## 🚀 How to run
79 |
80 | All commands are run from the root of the project, from a terminal:
81 |
82 | | Command | Action |
83 | | :-------------- | :------------------------------------------ |
84 | | `npm install` | Installs dependencies |
85 | | `npm run dev` | Starts local dev server at `localhost:3000` |
86 | | `npm run build` | Build your production site |
87 |
88 | ## 💡 Rebranding note
89 |
90 | This project has been renamed in May 2025. It was formerly known as "Daydash" and accessible via the "daydash.app" domain.
91 |
92 | ## 📝 License
93 |
94 | This project is licensed under the CC-BY-NC-ND-4.0 license - see the [license file](https://github.com/matt765/daydash/blob/master/license) for more information.
95 |
96 | Made with ♥ by [matt765](https://matt765-portfolio.vercel.app/)
97 |
--------------------------------------------------------------------------------
/src/components/modals/NotepadAlert.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 | import { Box, Button, Flex, Text, useOutsideClick } from '@chakra-ui/react';
3 |
4 | import { ContentBox } from '@/theme/components/contentBox';
5 |
6 | interface NotepadAlertProps {
7 | handleModalClose: (save: boolean) => void;
8 | }
9 |
10 | export const NotepadAlert = ({ handleModalClose }: NotepadAlertProps) => {
11 | const wrapperRef = useRef(null);
12 |
13 | useOutsideClick({
14 | ref: wrapperRef,
15 | handler: () => handleModalClose(false),
16 | });
17 |
18 | const handleConfirm = () => {
19 | handleModalClose(true);
20 | };
21 |
22 | return (
23 |
33 |
34 |
57 |
58 |
65 | Important:
66 |
67 |
74 | Please remember that both your task list and notepad data are
75 | stored in the browser's local storage. This means that the
76 | data is not accessible outside of your browser and will disappear
77 | if the local storage is cleared.
78 |
79 |
80 |
81 |
92 |
93 |
94 |
95 |
96 | );
97 | };
98 |
--------------------------------------------------------------------------------
/src/theme/components/text.ts:
--------------------------------------------------------------------------------
1 | import type { ComponentStyleConfig } from '@chakra-ui/react';
2 |
3 | export const Text: ComponentStyleConfig = {
4 | variants: {
5 | // Greeting
6 | welcomeTitle: {
7 | fontSize: { base: '3.4rem', 'xl': '2.5rem', '3xl': '3rem' },
8 | fontWeight: 400,
9 | color: 'primaryText',
10 | lineHeight: { base: '3.4rem', 'xl': '2.7rem', '3xl': '3.5rem' },
11 | },
12 | welcomePrimary: {
13 | fontSize: { base: '1.3rem', 'xl': '0.85rem', '3xl': '1rem' },
14 | fontWeight: 400,
15 | color: 'welcomePrimaryText',
16 | },
17 | welcomeSecondary: {
18 | fontSize: { base: '1.3rem', 'xl': '0.85rem', '3xl': '1rem' },
19 | fontWeight: 400,
20 | color: 'welcomeSecondaryText',
21 | },
22 | // Weather forecast
23 | weatherCity: {
24 | fontSize: { base: '1.5rem', '3xl': '2rem' },
25 | fontWeight: 400,
26 | color: 'primaryText',
27 | },
28 | weatherCountry: {
29 | fontSize: '2rem',
30 | fontWeight: 400,
31 | color: 'weatherCountryText',
32 | },
33 | weatherTemperature: {
34 | fontSize: { base: '2.5rem', '3xl': '3rem' },
35 | fontWeight: 400,
36 | color: 'primaryText',
37 | },
38 | weatherDesc: {
39 | fontSize: { base: '0.9rem', '3xl': '1rem' },
40 | fontWeight: 400,
41 | color: 'weatherDescText',
42 | },
43 | weatherParameterTitle: {
44 | fontSize: { base: '0.9rem', '3xl': '1rem' },
45 | fontWeight: 400,
46 | color: 'primaryText',
47 | },
48 | weatherParameterValue: {
49 | fontSize: { base: '1rem', '3xl': '1.25rem' },
50 | fontWeight: 400,
51 | color: 'primaryText',
52 | letterSpacing: '0.5px',
53 | },
54 | weatherBoxDate: {
55 | fontSize: { base: '0.6rem', '3xl': '0.75rem' },
56 | fontWeight: 400,
57 | color: 'primaryText',
58 | lineHeight: { base: '1rem', '3xl': '1.25rem' },
59 | },
60 | weatherBoxValue: {
61 | fontSize: { base: '0.6rem', '3xl': '0.75rem' },
62 | fontWeight: 400,
63 | color: 'primaryText',
64 | lineHeight: '1.25rem',
65 | },
66 | // Planner
67 | plannerPlaceholder: {
68 | fontSize: '1rem',
69 | fontWeight: 400,
70 | color: 'secondaryText',
71 | lineHeight: '1.5rem',
72 | },
73 | plannerButton: {
74 | fontSize: '1rem',
75 | fontWeight: 400,
76 | color: 'primaryText',
77 | },
78 | plannerItem: {
79 | fontSize: { base: '1rem', '3xl': '1.25rem' },
80 | fontWeight: 400,
81 | color: 'primaryText',
82 | lineHeight: { base: '1.8rem', '3xl': '1.25rem' },
83 | },
84 | plannerItemCrossed: {
85 | fontSize: { base: '1rem', '3xl': '1.25rem' },
86 | fontWeight: 400,
87 | color: 'secondaryText',
88 | },
89 | // User data
90 | dataModalTitle: {
91 | fontSize: '2.5rem',
92 | fontWeight: 700,
93 | color: 'primaryText',
94 | letterSpacing: '0.5px',
95 | },
96 | dataModalTitleColored: {
97 | fontSize: '2.5rem',
98 | fontWeight: 700,
99 | color: 'mainColor',
100 | letterSpacing: '0.5px',
101 | },
102 | dataModalSubtitle: {
103 | fontSize: '1.1rem',
104 | fontWeight: 500,
105 | color: 'secondaryText',
106 | },
107 | },
108 | };
109 |
--------------------------------------------------------------------------------
/src/hooks/usePlanner.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { DropResult } from '@hello-pangea/dnd';
3 |
4 | import { usePlannerStore } from '../store/plannerStore';
5 |
6 | interface PlannerItem {
7 | text: string;
8 | isCrossed: boolean;
9 | }
10 |
11 | interface UsePlanner {
12 | plannerItems: PlannerItem[];
13 | inputValue: string;
14 | setInputValue: (value: string) => void;
15 | addTask: (e: React.FormEvent) => void;
16 | updateTask: (index: number, newText: string) => void;
17 | toggleCrossed: (index: number) => void;
18 | removeTask: (index: number) => void;
19 | onDragEnd: (result: DropResult) => void;
20 | loading: boolean;
21 | showTooltip: boolean;
22 | setShowTooltip: (value: boolean) => void;
23 | }
24 |
25 | export const usePlanner = (): UsePlanner => {
26 | const [plannerItems, setPlannerItems] = usePlannerStore((state) => [
27 | state.plannerItems,
28 | state.setPlannerItems,
29 | ]);
30 | const [inputValue, setInputValue] = useState('');
31 | const [loading, setLoading] = useState(false);
32 |
33 | useEffect(() => {
34 | setLoading(false);
35 | }, []);
36 |
37 | const [showTooltip, setShowTooltip] = useState(false);
38 |
39 | const reorder = (
40 | list: PlannerItem[],
41 | startIndex: number,
42 | endIndex: number
43 | ): PlannerItem[] => {
44 | const result = Array.from(list);
45 | const [removed] = result.splice(startIndex, 1);
46 | result.splice(endIndex, 0, removed);
47 | return result;
48 | };
49 |
50 | const addTask = (e: React.FormEvent) => {
51 | e.preventDefault();
52 | if (inputValue && !plannerItems.some((item) => item.text === inputValue)) {
53 | const newPlannerItems = [
54 | ...plannerItems,
55 | {
56 | text: inputValue,
57 | isCrossed: false,
58 | },
59 | ];
60 | setPlannerItems(newPlannerItems);
61 | setInputValue('');
62 | setShowTooltip(false);
63 | } else {
64 | setShowTooltip(true);
65 | }
66 | };
67 |
68 | const updateTask = (index: number, newText: string) => {
69 | const newPlannerItems = plannerItems.map((item, i) => {
70 | if (i === index) {
71 | return {
72 | ...item,
73 | text: newText,
74 | };
75 | }
76 | return item;
77 | });
78 | setPlannerItems(newPlannerItems);
79 | };
80 |
81 | const removeTask = (index: number) => {
82 | const newPlannerItems = plannerItems.filter((_, i) => i !== index);
83 | setPlannerItems(newPlannerItems);
84 | };
85 |
86 | const toggleCrossed = (index: number) => {
87 | const newPlannerItems = plannerItems.map((item, i) => {
88 | if (i === index) {
89 | return {
90 | ...item,
91 | isCrossed: !item.isCrossed,
92 | };
93 | }
94 | return item;
95 | });
96 | setPlannerItems(newPlannerItems);
97 | };
98 |
99 | const onDragEnd = (result: DropResult) => {
100 | if (!result.destination) {
101 | return;
102 | }
103 | const items = reorder(
104 | plannerItems,
105 | result.source.index,
106 | result.destination.index
107 | );
108 | setPlannerItems(items);
109 | };
110 |
111 | return {
112 | plannerItems,
113 | inputValue,
114 | loading,
115 | setInputValue,
116 | addTask,
117 | updateTask,
118 | toggleCrossed,
119 | removeTask,
120 | onDragEnd,
121 | showTooltip,
122 | setShowTooltip,
123 | };
124 | };
125 |
--------------------------------------------------------------------------------
/src/components/settings/Settings.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Drawer,
3 | DrawerBody,
4 | DrawerOverlay,
5 | DrawerContent,
6 | DrawerCloseButton,
7 | Box,
8 | } from '@chakra-ui/react';
9 |
10 | import { ClearAllData } from '../modals/ClearAllDataModal';
11 | import { EditUserData } from '../modals/EditUserDataModal';
12 | import { SettingsContent } from './SettingsContent';
13 |
14 | interface SettingsProps {
15 | isOpen: boolean;
16 | onClose: () => void;
17 | btnRef: React.RefObject;
18 | onEditUserData: () => void;
19 | onEditUserDataClose: () => void;
20 | onClearAllData: () => void;
21 | onClearAllDataClose: () => void;
22 | isDrawerContentVisible: boolean;
23 | setIsDrawerContentVisible: (value: boolean) => void;
24 | isEditUserDataOpen: boolean;
25 | isClearAllDataOpen: boolean;
26 | }
27 |
28 | export const Settings = ({
29 | isOpen,
30 | onClose,
31 | btnRef,
32 | onEditUserData,
33 | onEditUserDataClose,
34 | onClearAllData,
35 | onClearAllDataClose,
36 | isDrawerContentVisible,
37 | setIsDrawerContentVisible,
38 | isEditUserDataOpen,
39 | isClearAllDataOpen,
40 | }: SettingsProps) => {
41 | return (
42 | <>
43 |
48 | {
50 | onClose();
51 | onEditUserDataClose();
52 | onClearAllDataClose();
53 | }}
54 | />
55 | {isDrawerContentVisible && (
56 |
65 |
74 |
75 | setIsDrawerContentVisible(false)}
79 | />
80 |
81 |
82 | )}
83 |
84 |
102 | {isEditUserDataOpen && (
103 | {
105 | onEditUserDataClose();
106 | onClose();
107 | }}
108 | />
109 | )}
110 | {isClearAllDataOpen && (
111 | {
113 | onClearAllDataClose();
114 | onClose();
115 | }}
116 | />
117 | )}
118 |
119 | >
120 | );
121 | };
122 |
--------------------------------------------------------------------------------
/src/components/views/intro/Intro.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Flex, Input, Text } from '@chakra-ui/react';
2 |
3 | import { ContentBox } from '@/theme/components/contentBox';
4 | import { useIntro } from '@/hooks/useIntro';
5 | import { useWeatherStore } from '@/store/weatherStore';
6 | import { Loader } from '@/components/common/Loader';
7 | import { useHomepage } from '@/hooks/useHomepage';
8 |
9 | export const Intro = () => {
10 | const { name, city, setName, setCity, handleSubmit, isSubmitting, isError } =
11 | useIntro();
12 | const { isLoading } = useWeatherStore();
13 |
14 | return (
15 |
33 |
107 |
108 | );
109 | };
110 |
--------------------------------------------------------------------------------
/src/components/settings/SettingsSectionRow.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Flex, Switch, Radio, RadioGroup } from '@chakra-ui/react';
2 | import { useEffect, useState } from 'react';
3 |
4 | interface Props {
5 | name?: string;
6 | type: string;
7 | isChecked?: boolean;
8 | section?: string;
9 | property?: string;
10 | onSwitchChange?: () => void;
11 | options?: Array<{ label: string; value: string }>;
12 | currentValue?: string;
13 | onRadioChange?: (value: string) => void;
14 | checkedValue?: string;
15 | }
16 |
17 | export const SettingsSectionRow = ({
18 | name,
19 | type,
20 | options,
21 | isChecked,
22 | onSwitchChange,
23 | onRadioChange,
24 | checkedValue,
25 | }: Props) => {
26 | const [radioValue, setRadioValue] = useState(checkedValue || '');
27 |
28 | useEffect(() => {
29 | setRadioValue(checkedValue || '');
30 | }, [checkedValue]);
31 |
32 | const handleChange = (value: string) => {
33 | setRadioValue(value);
34 | if (onRadioChange) {
35 | onRadioChange(value);
36 | }
37 | };
38 |
39 | return (
40 |
46 | {type === 'switch' && (
47 |
52 |
57 | {name}
58 |
59 |
60 |
61 |
71 |
72 |
73 | )}
74 | {type === 'radio' && (
75 |
83 | {name && (
84 |
89 | {name}
90 |
91 | )}
92 | handleChange(value)}
97 | value={radioValue}>
98 |
99 | {options &&
100 | options.map((option, index) => (
101 |
106 | onRadioChange && onRadioChange(e.target.value)
107 | }
108 | _checked={{
109 | background: 'settingsMainColor',
110 | borderColor: 'settingsMainColor',
111 | '&::before': {
112 | content: `""`,
113 | display: 'inline-block',
114 | position: 'relative',
115 | width: '50%',
116 | height: '50%',
117 | borderRadius: '50%',
118 | background: 'settingsRadioBefore',
119 | },
120 | span: {},
121 | }}>
122 |
123 | {option.label}
124 |
125 |
126 | ))}
127 |
128 |
129 |
130 | )}
131 |
132 | );
133 | };
134 |
--------------------------------------------------------------------------------
/src/components/views/homepage/planner/Planner.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Flex, useMediaQuery } from '@chakra-ui/react';
4 | import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
5 | import { PlannerHeader } from './PlannerHeader';
6 | import { PlannerItem } from './PlannerItem';
7 | import { usePlanner } from '@/hooks/usePlanner';
8 | import { Loader } from '@/components/common/Loader';
9 | import { useEffect, useState } from 'react';
10 |
11 | export const Planner = () => {
12 | const {
13 | plannerItems,
14 | inputValue,
15 | setInputValue,
16 | addTask,
17 | updateTask,
18 | toggleCrossed,
19 | removeTask,
20 | onDragEnd,
21 | loading,
22 | showTooltip,
23 | setShowTooltip,
24 | } = usePlanner();
25 | const [isLaptop] = useMediaQuery(
26 | '(min-width: 992px) and (max-width: 1632px)'
27 | );
28 |
29 | const [enabled, setEnabled] = useState(false);
30 |
31 | useEffect(() => {
32 | const timeout = setTimeout(() => setEnabled(true), 500);
33 | return () => clearTimeout(timeout);
34 | }, []);
35 |
36 | if (loading) {
37 | return ;
38 | }
39 |
40 | const numberOfItems = isLaptop ? 6 : 6;
41 |
42 | return (
43 | <>
44 |
61 |
68 |
69 | {enabled ? (
70 |
71 |
72 | {(provided) => (
73 |
79 | {plannerItems.map((item, index) => (
80 |
84 | {(provided) => (
85 |
89 |
removeTask(index)}
92 | toggleCrossed={() => toggleCrossed(index)}
93 | onSave={(newText) => updateTask(index, newText)}
94 | />
95 |
96 | )}
97 |
98 | ))}
99 | {provided.placeholder}
100 |
101 | )}
102 |
103 |
104 | ) : (
105 |
109 | {plannerItems.map((item, index) => (
110 |
111 |
removeTask(index)}
114 | toggleCrossed={() => toggleCrossed(index)}
115 | onSave={(newText) => updateTask(index, newText)}
116 | />
117 |
118 | ))}
119 |
120 | )}
121 |
122 | >
123 | );
124 | };
125 |
--------------------------------------------------------------------------------
/src/components/views/snake/Snake.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Box, Button, Flex, Text, useColorMode } from '@chakra-ui/react';
4 | import { useSnake } from '@/hooks/useSnake';
5 | import { PageWrapper } from '@/components/common/PageWrapper';
6 |
7 | export const SnakeGame = () => {
8 | const {
9 | board,
10 | score,
11 | isGameRunning,
12 | gameOver,
13 | record,
14 | newRecord,
15 | shouldResultTitleAppear,
16 | shouldIntroTitleAppear,
17 | startGame,
18 | resetGame,
19 | isSnake,
20 | isFood,
21 | colorMode,
22 | } = useSnake();
23 |
24 | return (
25 |
32 |
40 |
52 | Score: {score}
53 |
54 |
58 | {gameOver && shouldResultTitleAppear && newRecord && 'New record!'}
59 | {gameOver && !newRecord && shouldResultTitleAppear && 'Game over!'}
60 | {shouldIntroTitleAppear && "Let's play!"}
61 |
62 |
73 | Record: {record}
74 |
75 |
76 |
84 | {board.map((row, rowIndex) => (
85 |
86 | {row.map((col, colIndex) => (
87 |
101 | ))}
102 |
103 | ))}
104 |
105 | {/* Render score */}
106 | {board.length > 0 && (
107 |
108 |
135 |
136 | )}
137 |
138 | );
139 | };
140 |
--------------------------------------------------------------------------------
/src/components/modals/EditUserDataModal.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Flex, Input, Text } from '@chakra-ui/react';
2 |
3 | import { useEditUserData } from '@/hooks/useEditUserData';
4 | import { ContentBox } from '@/theme/components/contentBox';
5 | import { useWeatherStore } from '@/store/weatherStore';
6 | import { Loader } from '../common/Loader';
7 |
8 | interface EditUserDataProps {
9 | onClose: () => void;
10 | }
11 |
12 | export const EditUserData = ({ onClose }: EditUserDataProps) => {
13 | const {
14 | name,
15 | city,
16 | setNameInput,
17 | setCityInput,
18 | handleSubmit,
19 | isSubmitting,
20 | isError,
21 | } = useEditUserData(onClose);
22 |
23 | return (
24 |
34 |
58 |
134 |
135 |
136 | );
137 | };
138 |
--------------------------------------------------------------------------------
/src/hooks/useHomepage.ts:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect } from 'react';
2 | import { useColorMode, useDisclosure } from '@chakra-ui/react';
3 |
4 | import { useUserStoreWrapper } from '@/store/userStore';
5 | import useSettingsStore from '@/store/settingsStore';
6 | import { useMobileViewStore } from '@/store/mobileViewStore';
7 |
8 | export type ViewType =
9 | | 'intro'
10 | | 'dashboard'
11 | | 'notepad'
12 | | 'snake'
13 | | 'loading'
14 | | 'mobileHome'
15 | | 'mobileWeather'
16 | | 'mobilePlanner'
17 | | 'settings';
18 |
19 | export const useHomepage = () => {
20 | const { name, city, isMounted } = useUserStoreWrapper();
21 | const [view, _setView] = useState('loading');
22 | const { mobileView, setMobileView } = useMobileViewStore();
23 |
24 | const setView = (view: string) => {
25 | _setView(view as ViewType);
26 | };
27 |
28 | const handleViewChange = (
29 | view: ViewType,
30 | deviceType: 'mobile' | 'desktop'
31 | ) => {
32 | if (deviceType === 'mobile') {
33 | setMobileView(view);
34 | }
35 | };
36 |
37 | const {
38 | isOpen: isSettingsPanelOpen,
39 | onOpen: onSettingsPanelOpen,
40 | onClose: onSettingsPanelClose,
41 | } = useDisclosure();
42 | const {
43 | isOpen: isEditUserDataOpen,
44 | onOpen: onEditUserDataOpen,
45 | onClose: onEditUserDataClose,
46 | } = useDisclosure();
47 | const {
48 | isOpen: isClearAllDataOpen,
49 | onOpen: onClearAllDataOpen,
50 | onClose: onClearAllDataClose,
51 | } = useDisclosure();
52 | const btnRef = useRef(null);
53 | const showSnakeButton = useSettingsStore((state) => state.showSnakeButton);
54 | const [isDrawerContentVisible, setIsDrawerContentVisible] = useState(false);
55 | const { colorMode } = useColorMode();
56 | const theme = useSettingsStore((state) => state.theme);
57 | const [themeAndColorModeReady, setThemeAndColorModeReady] = useState(false);
58 | const [isBgImageLoaded, setIsBgImageLoaded] = useState(false);
59 |
60 | const handleToggleNotepadView = () => {
61 | setViewWithLocalStorage(view === 'notepad' ? 'dashboard' : 'notepad');
62 | };
63 |
64 | const handleToggleSnakeView = () => {
65 | setViewWithLocalStorage(view === 'snake' ? 'dashboard' : 'snake');
66 | };
67 |
68 | const setViewWithLocalStorage = (newView: ViewType) => {
69 | if (newView !== 'intro') {
70 | localStorage.setItem('currentView', newView);
71 | }
72 | setView(newView);
73 | };
74 | const preloadImages = (imageURLs: string[]) => {
75 | imageURLs.forEach((url) => {
76 | const img = new Image();
77 | img.src = url;
78 | });
79 | };
80 | const preloadImage = (imageURL: string) => {
81 | const img = new Image();
82 | img.src = imageURL.slice(4, -1); // removes 'url(' at the start and ')' at the end
83 | img.onload = () => {
84 | setIsBgImageLoaded(true);
85 | };
86 | img.onerror = () => {
87 | setIsBgImageLoaded(true);
88 | };
89 | };
90 |
91 | const isImageVisible = useSettingsStore((state) => state.isImageVisible);
92 |
93 | useEffect(() => {
94 | if (isMounted) {
95 | const savedMobileView = localStorage.getItem(
96 | 'currentMobileView'
97 | ) as ViewType;
98 |
99 | if (name && city && name !== '' && city !== '') {
100 | setMobileView(savedMobileView || 'mobileHome');
101 | } else {
102 | setMobileView('intro');
103 | }
104 |
105 | preloadImage(getBackgroundImage());
106 | setThemeAndColorModeReady(true);
107 | }
108 | }, [isMounted, name, city, theme, colorMode, setMobileView]);
109 |
110 | const getBackgroundImage = () => {
111 | if (themeAndColorModeReady && isImageVisible) {
112 | if (theme === 'basicTheme' && colorMode === 'dark') {
113 | return 'url(cyberpunk.png)';
114 | } else if (theme === 'basicTheme' && colorMode === 'light') {
115 | return 'url(mountains.jpg)';
116 | } else if (theme === 'extendedTheme' && colorMode === 'dark') {
117 | return 'url(fairytale.png)';
118 | } else if (theme === 'extendedTheme' && colorMode === 'light') {
119 | return 'url(postap.png)';
120 | }
121 | }
122 | return 'url(fairytale.png)';
123 | };
124 |
125 | return {
126 | view,
127 | setView,
128 | name,
129 | city,
130 | handleToggleNotepadView,
131 | handleToggleSnakeView,
132 | onSettingsPanelOpen,
133 | onSettingsPanelClose,
134 | onEditUserDataOpen,
135 | onEditUserDataClose,
136 | onClearAllDataOpen,
137 | onClearAllDataClose,
138 | isSettingsPanelOpen,
139 | isEditUserDataOpen,
140 | isClearAllDataOpen,
141 | btnRef,
142 | showSnakeButton,
143 | isDrawerContentVisible,
144 | setIsDrawerContentVisible,
145 | getBackgroundImage,
146 | setViewWithLocalStorage,
147 | isBgImageLoaded,
148 | handleViewChange,
149 | mobileView,
150 | };
151 | };
152 |
--------------------------------------------------------------------------------
/src/components/layout/SideButtons.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Flex, Icon, Tooltip } from '@chakra-ui/react';
3 | import { NotepadEditIcon } from '@/assets/icons/NotepadEditIcon';
4 | import { SettingsIcon } from '@/assets/icons/SettingsIcon';
5 | import { HomeIcon } from '@/assets/icons/HomeIcon';
6 | import { SnakeIcon } from '@/assets/icons/SnakeIcon';
7 | import { ViewType } from '@/hooks/useHomepage';
8 | import Link from 'next/link';
9 | import { usePathname } from 'next/navigation';
10 |
11 | interface SideButtonsProps {
12 | openDrawer: () => void;
13 | showSnakeButton: boolean;
14 | }
15 |
16 | export const SideButtons = ({
17 | openDrawer,
18 | showSnakeButton,
19 | }: SideButtonsProps) => {
20 | const pathname = usePathname();
21 | const currentPath = pathname;
22 |
23 | return (
24 |
39 |
48 |
49 |
63 |
64 |
65 |
74 |
75 |
91 |
92 |
93 | {showSnakeButton && (
94 |
103 |
104 |
120 |
121 |
122 | )}
123 |
132 |
146 |
147 |
148 | );
149 | };
150 |
--------------------------------------------------------------------------------
/src/components/layout/MobileNavigation.tsx:
--------------------------------------------------------------------------------
1 | import { CheckIcon } from '@/assets/icons/CheckIcon';
2 | import { EditIcon } from '@/assets/icons/EditIcon';
3 | import { HomeIcon } from '@/assets/icons/HomeIcon';
4 | import { PlannerMobileIcon } from '@/assets/icons/PlannerMobileIcon';
5 | import { SettingsIcon } from '@/assets/icons/SettingsIcon';
6 | import { WeatherMobileIcon } from '@/assets/icons/WeatherMobileIcon';
7 | import { ViewType } from '@/hooks/useHomepage';
8 | import { useMobileViewStore } from '@/store/mobileViewStore';
9 | import { Flex, Icon, Text } from '@chakra-ui/react';
10 |
11 | interface NavigationItemProps {
12 | title: String;
13 | viewName: ViewType;
14 | icon: React.FC>;
15 | onNavItemClick: (viewName: ViewType) => void;
16 | isSettingsPanelOpen: boolean;
17 | onSettingsPanelClose: () => void;
18 | onEditUserDataClose: () => void;
19 | onClearAllDataClose: () => void;
20 | }
21 |
22 | const NavigationItem = ({
23 | title,
24 | viewName,
25 | icon,
26 | onNavItemClick,
27 | isSettingsPanelOpen,
28 | onSettingsPanelClose,
29 | onEditUserDataClose,
30 | onClearAllDataClose,
31 | }: NavigationItemProps) => (
32 | {
41 | if (isSettingsPanelOpen && title !== 'Settings') {
42 | onSettingsPanelClose();
43 | }
44 | onEditUserDataClose();
45 | onClearAllDataClose();
46 | onNavItemClick(viewName);
47 | }}
48 | cursor="pointer"
49 | sx={{
50 | '& svg': {
51 | fill: title !== 'Settings' && 'rgb(166, 166, 166)',
52 | stroke: title === 'Settings' && 'rgb(166, 166, 166)',
53 | width: title === 'Task list' ? '1.2rem' : '1.4rem',
54 | height: title === 'Task list' ? '1.2rem' : '1.4rem',
55 | },
56 | }}
57 | _hover={{
58 | backgroundColor: 'mobileNavigationHoverBg',
59 | }}>
60 |
61 |
68 | {title}
69 |
70 |
71 | );
72 |
73 | const navigationItems = [
74 | { title: 'Home', viewName: 'mobileHome' as ViewType, icon: HomeIcon },
75 | {
76 | title: 'Weather',
77 | viewName: 'mobileWeather' as ViewType,
78 | icon: WeatherMobileIcon,
79 | },
80 | {
81 | title: 'Task list',
82 | viewName: 'mobilePlanner' as ViewType,
83 | icon: PlannerMobileIcon,
84 | },
85 | { title: 'Notepad', viewName: 'notepad' as ViewType, icon: EditIcon },
86 | { title: 'Settings', viewName: 'settings' as ViewType, icon: SettingsIcon },
87 | ];
88 |
89 | interface MobileNavigationProps {
90 | handleViewChange?: (view: ViewType, deviceType: 'mobile' | 'desktop') => void;
91 | openDrawer: () => void;
92 | isSettingsPanelOpen: boolean;
93 | onSettingsPanelClose: () => void;
94 | mobileView?: string;
95 | onEditUserDataClose: () => void;
96 | onClearAllDataClose: () => void;
97 | }
98 |
99 | export const MobileNavigation = ({
100 | handleViewChange,
101 | openDrawer,
102 | isSettingsPanelOpen,
103 | onSettingsPanelClose,
104 | mobileView,
105 | onEditUserDataClose,
106 | onClearAllDataClose,
107 | }: MobileNavigationProps) => {
108 | const { setMobileView } = useMobileViewStore();
109 |
110 | const handleNavItemClick = (viewName: ViewType) => {
111 | if (viewName === 'settings') {
112 | openDrawer();
113 | } else {
114 | setMobileView(viewName);
115 |
116 | if (handleViewChange) {
117 | handleViewChange(viewName, 'mobile');
118 | }
119 | }
120 | };
121 |
122 | return (
123 | :not(:last-child)': {
137 | borderWidth: '0 1px 0 0',
138 | borderStyle: 'solid',
139 | borderColor: 'rgb(255,255,255,0.15)',
140 | },
141 | }}>
142 | {navigationItems.map((item, index) => (
143 |
154 | ))}
155 |
156 | );
157 | };
158 |
--------------------------------------------------------------------------------
/src/components/layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { ReactNode, useEffect, useState } from 'react';
4 | import { Flex, useMediaQuery } from '@chakra-ui/react';
5 | import { SideButtons } from '@/components/layout/SideButtons';
6 | import { Settings } from '@/components/settings/Settings';
7 | import { MobileNavigation } from '@/components/layout/MobileNavigation';
8 | import { Loader } from '@/components/common/Loader';
9 | import { useHomepage, ViewType } from '@/hooks/useHomepage';
10 | import { useRouter } from 'next/router';
11 | import { useUserStoreWrapper } from '@/store/userStore';
12 | import { PageWrapper } from '../common/PageWrapper';
13 | import { useNotepadStore } from '@/store/notepadStore';
14 | import { NotepadAlert } from '../modals/NotepadAlert';
15 |
16 | interface LayoutProps {
17 | children: ReactNode;
18 | currentView?: string;
19 | requiresAuth?: boolean;
20 | mobileView?: string;
21 | handleViewChange?: (view: ViewType, deviceType: 'mobile' | 'desktop') => void;
22 | }
23 |
24 | export const Layout = ({ children }: LayoutProps) => {
25 | const {
26 | name,
27 | city,
28 | onSettingsPanelOpen,
29 | onSettingsPanelClose,
30 | onEditUserDataOpen,
31 | onEditUserDataClose,
32 | onClearAllDataOpen,
33 | onClearAllDataClose,
34 | isSettingsPanelOpen,
35 | isEditUserDataOpen,
36 | isClearAllDataOpen,
37 | btnRef,
38 | showSnakeButton,
39 | isDrawerContentVisible,
40 | setIsDrawerContentVisible,
41 | getBackgroundImage,
42 | isBgImageLoaded,
43 | mobileView,
44 | handleViewChange,
45 | } = useHomepage();
46 |
47 | const { isMounted } = useUserStoreWrapper();
48 |
49 | const isUserDataPresent = name && city;
50 |
51 | const [isDesktop] = useMediaQuery('(min-width: 1280px)');
52 |
53 | const { isModalVisible, setIsModalVisible, setIsNotepadModalConfirmed } =
54 | useNotepadStore();
55 |
56 | if (!isMounted || !isBgImageLoaded) {
57 | return (
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | const handleModalClose = (save: boolean) => {
65 | setIsModalVisible(false);
66 | if (save) {
67 | setIsNotepadModalConfirmed(true);
68 | }
69 | };
70 |
71 | return (
72 |
82 |
83 | {isDesktop ? (
84 |
89 | {isUserDataPresent ? (
90 | {children}
91 | ) : (
92 | children
93 | )}
94 |
95 | ) : (
96 |
102 | {isUserDataPresent ? (
103 | {children}
104 | ) : (
105 | children
106 | )}
107 |
108 | )}
109 |
110 | {isUserDataPresent && isDesktop && (
111 | {
113 | onSettingsPanelOpen();
114 | setIsDrawerContentVisible(true);
115 | }}
116 | showSnakeButton={showSnakeButton}
117 | />
118 | )}
119 |
132 | {!isDesktop && isUserDataPresent && (
133 | {
135 | if (isSettingsPanelOpen) {
136 | onSettingsPanelClose();
137 | setIsDrawerContentVisible(false);
138 | } else {
139 | onSettingsPanelOpen();
140 | setIsDrawerContentVisible(true);
141 | }
142 | }}
143 | isSettingsPanelOpen={isSettingsPanelOpen}
144 | onSettingsPanelClose={onSettingsPanelClose}
145 | mobileView={mobileView}
146 | onEditUserDataClose={onEditUserDataClose}
147 | onClearAllDataClose={onClearAllDataClose}
148 | handleViewChange={handleViewChange}
149 | />
150 | )}
151 | {isModalVisible && }
152 |
153 | );
154 | };
155 |
--------------------------------------------------------------------------------
/src/components/views/homepage/welcome/Welcome.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Flex, Icon, Text } from '@chakra-ui/react';
2 |
3 | import { useWelcome } from '@/hooks/useWelcome';
4 |
5 | import { RefreshIcon } from '@/assets/icons/RefreshIcon';
6 | import { Loader } from '@/components/common/Loader';
7 |
8 | export const Welcome = () => {
9 | const {
10 | userName,
11 | dayOfWeek,
12 | dayOfMonth,
13 | monthName,
14 | year,
15 | contentMode,
16 | isLoading,
17 | error,
18 | fact,
19 | isLoadingQuote,
20 | errorQuote,
21 | quote,
22 | author,
23 | isRefetchingContent,
24 | refetchContent,
25 | } = useWelcome();
26 |
27 | if (isLoading) {
28 | return ;
29 | }
30 |
31 | return (
32 |
33 |
46 |
57 | Hello
58 | {userName}
59 |
60 |
65 |
66 | Today is
67 |
68 |
69 | {`${dayOfWeek}, ${dayOfMonth} ${monthName} ${year}`}
70 |
71 |
72 | {contentMode === 'did_you_know' ? (
73 | isLoading ? (
74 |
75 |
76 |
77 | ) : error ? (
78 | Error fetching fact
79 | ) : (
80 |
81 |
91 |
95 | Did you know?
96 |
97 |
98 |
99 |
100 |
101 | {isRefetchingContent ? (
102 |
103 |
104 |
105 | ) : (
106 |
110 | {fact}
111 |
112 | )}
113 |
114 | )
115 | ) : isLoadingQuote || isLoading || isRefetchingContent ? (
116 |
117 |
118 |
119 | ) : errorQuote ? (
120 | Error fetching quote
121 | ) : (
122 |
123 |
124 | "{quote}"
125 |
126 |
132 |
145 |
146 |
147 |
153 | {author}
154 |
155 |
156 |
157 | )}
158 |
159 |
160 | );
161 | };
162 |
--------------------------------------------------------------------------------
/src/components/modals/ClearAllDataModal.tsx:
--------------------------------------------------------------------------------
1 | import { ContentBox } from '@/theme/components/contentBox';
2 | import { clearAllData } from '@/utils/clearAllData';
3 | import {
4 | Button,
5 | Flex,
6 | Input,
7 | ListItem,
8 | Text,
9 | UnorderedList,
10 | useToast,
11 | } from '@chakra-ui/react';
12 | import { useState } from 'react';
13 | import { useRouter } from 'next/navigation';
14 |
15 | interface ClearAllDataProps {
16 | onClose: () => void;
17 | }
18 |
19 | export const ClearAllData = ({ onClose }: ClearAllDataProps) => {
20 | const [value, setValue] = useState('');
21 | const [showError, setShowError] = useState(false);
22 | const router = useRouter();
23 | const toast = useToast();
24 |
25 | const handleSubmit = (event: React.FormEvent) => {
26 | event.preventDefault();
27 | if (value === 'yes' || value === 'Yes') {
28 | clearAllData();
29 | onClose();
30 | toast({
31 | title: 'Data cleared',
32 | status: 'success',
33 | duration: 3000,
34 | isClosable: true,
35 | });
36 | router.replace('/');
37 | } else {
38 | setShowError(true);
39 | }
40 | };
41 |
42 | return (
43 |
53 |
78 |
187 |
188 |
189 | );
190 | };
191 |
--------------------------------------------------------------------------------
/src/components/views/homepage/planner/PlannerItem.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Icon, Input, Text } from '@chakra-ui/react';
2 | import { useState, useRef } from 'react';
3 |
4 | import { DeleteIcon } from '@/assets/icons/DeleteIcon';
5 | import { EditIcon } from '@/assets/icons/EditIcon';
6 | import { CloseIcon } from '@/assets/icons/CloseIcon';
7 | import { CheckIcon } from '@/assets/icons/CheckIcon';
8 | import { useClickOutside } from '@/hooks/useClickOutside';
9 |
10 | interface PlannerItemProps {
11 | item: { text: string; isCrossed: boolean };
12 | onDelete: () => void;
13 | toggleCrossed: () => void;
14 | onSave: (newText: string) => void;
15 | }
16 |
17 | export const PlannerItem = ({
18 | item,
19 | onDelete,
20 | toggleCrossed,
21 | onSave,
22 | }: PlannerItemProps) => {
23 | const [isEditing, setIsEditing] = useState(false);
24 | const [editedText, setEditedText] = useState(item.text);
25 | const taskRef = useRef(null);
26 |
27 | const handleEdit = (event: React.MouseEvent) => {
28 | event.stopPropagation();
29 | setIsEditing(true);
30 | };
31 |
32 | const handleSave = (e: React.KeyboardEvent | null) => {
33 | if (e === null || e.key === 'Enter') {
34 | setIsEditing(false);
35 | onSave(editedText);
36 | }
37 | };
38 |
39 | useClickOutside(taskRef, () => {
40 | if (isEditing) {
41 | handleSave(null);
42 | }
43 | });
44 |
45 | return (
46 |
65 |
76 |
83 | ✓
84 |
85 | {isEditing ? (
86 | setEditedText(e.target.value)}
89 | onKeyDown={handleSave}
90 | autoFocus
91 | maxLength={60}
92 | w="100%"
93 | fontSize={{ base: '1rem', '3xl': '1.25rem' }}
94 | fontWeight="400"
95 | color="primaryText"
96 | ml="-1rem"
97 | h="100%"
98 | />
99 | ) : (
100 |
107 | {item.text}
108 |
109 | )}
110 |
111 |
122 |
123 | {isEditing ? (
124 |
125 | {
136 | e.stopPropagation();
137 | setIsEditing(false);
138 | setEditedText(item.text);
139 | }}>
140 | handleSave(null)}
145 | />
146 |
147 | {
158 | e.stopPropagation();
159 | handleSave(null);
160 | }}>
161 |
162 |
163 |
164 | ) : (
165 |
178 |
179 |
180 | )}
181 |
182 | {
184 | e.stopPropagation();
185 | if (!isEditing) {
186 | onDelete();
187 | }
188 | }}
189 | sx={{
190 | '&:hover': {
191 | '& svg': {
192 | color: 'plannerItemIconHover',
193 | fill: 'plannerItemIconHover',
194 | },
195 | },
196 | '& svg': {
197 | width: { base: '20px', '3xl': '20px' },
198 | height: { base: '20px', '3xl': '20px' },
199 | },
200 | }}>
201 |
202 |
203 |
204 |
205 | );
206 | };
207 |
--------------------------------------------------------------------------------
/src/theme/colors.ts:
--------------------------------------------------------------------------------
1 | // Default theme: "Mountains"
2 | // Dark theme: "Cyberpunk"
3 |
4 | export const colors = {
5 | homepageBg: {
6 | default: 'rgb(125, 132, 170)',
7 | _dark: 'rgb(31, 39, 47)',
8 | },
9 | primaryText: {
10 | default: 'rgb(255,255,255)',
11 | _dark: 'rgb(255, 255, 255)',
12 | },
13 | secondaryText: {
14 | default: 'rgba(255, 255, 255, 0.87)',
15 | _dark: 'rgba(255, 255, 255, 0.87)',
16 | },
17 | disabledText: {
18 | default: 'rgb(255,255,255,0.4)',
19 | _dark: 'rgba(255, 255, 255, 0.4)',
20 | },
21 | coloredText: {
22 | default: 'rgb(161, 170, 199)',
23 | _dark: 'rgb(255, 140, 40)',
24 | },
25 | contentBg: {
26 | default: 'rgba(45, 53, 80, 0.8)',
27 | _dark: 'rgba(57, 68, 81, 0.5)',
28 | },
29 | contentBorder: {
30 | default: 'rgb(255,255,255,0)',
31 | _dark: 'rgba(255, 255, 255, 0.1)',
32 | },
33 | screenBg: {
34 | default: '#2c2f35',
35 | _dark: '#222222',
36 | },
37 | mainColor: {
38 | default: 'rgba(44, 165, 236, 0.726)',
39 | _dark: 'rgb(236, 142, 65)',
40 | },
41 | mainColorHover: {
42 | default: 'rgba(44, 165, 236, 0.926)',
43 | _dark: 'rgb(255, 120, 10)',
44 | },
45 | coloredButtonBg: {
46 | default: 'rgba(44, 165, 236, 0.726)',
47 | _dark: 'rgb(187, 118, 61)',
48 | },
49 | coloredButtonHoverBg: {
50 | default: 'rgba(44, 165, 236, 0.926)',
51 | _dark: 'rgb(242, 154, 81)',
52 | },
53 | // Greeting
54 | welcomePrimaryText: {
55 | default: 'rgb(255,255,255)',
56 | _dark: 'rgb(236, 142, 65)',
57 | },
58 | welcomeSecondaryText: {
59 | default: 'rgb(161, 170, 199)',
60 | _dark: 'rgba(255, 255, 255, 0.67)',
61 | },
62 | welcomeIcon: {
63 | default: 'rgb(137, 150, 192)',
64 | _dark: 'rgb(255, 255, 255, 0.7)',
65 | },
66 | welcomeIconHover: {
67 | default: 'rgb(156, 168, 208)',
68 | _dark: 'rgb(255, 255, 255, 0.8)',
69 | },
70 | // Weather forecast
71 | weatherCountryText: {
72 | default: 'rgb(161, 170, 199)',
73 | _dark: 'rgb(255, 255, 255, 0.8)',
74 | },
75 | weatherDescText: {
76 | default: 'rgb(161, 170, 199)',
77 | _dark: 'rgb(255, 255, 255, 0.8)',
78 | },
79 | weatherIcon: {
80 | default: 'rgb(137, 150, 192)',
81 | _dark: 'rgb(236, 142, 65)',
82 | },
83 | weatherIconHover: {
84 | default: 'rgb(156, 168, 208)',
85 | _dark: 'rgb(255, 255, 255, 0.8)',
86 | },
87 | weatherBoxBg: {
88 | default: 'rgba(255, 255, 255, 0.034)',
89 | _dark: 'rgba(115, 130, 147, 0.2)',
90 | },
91 | weatherBoxHoverBg: {
92 | default: 'rgba(255, 255, 255, 0.1)',
93 | _dark: 'rgba(115, 130, 147, 0.7)',
94 | },
95 | // Task list
96 | plannerInputBg: {
97 | default: 'rgba(255, 255, 255, 0.068)',
98 | _dark: 'rgba(115, 130, 147, 0.2)',
99 | },
100 | plannerInputHoverBg: {
101 | default: 'rgba(255, 255, 255, 0.1)',
102 | _dark: 'rgba(115, 130, 147, 0.45)',
103 | },
104 | plannerItemBg: {
105 | default: 'rgba(196, 201, 216, 0.09)',
106 | _dark: 'rgba(115, 130, 147, 0.2)',
107 | },
108 | plannerItemHoverBg: {
109 | default: 'rgba(196, 201, 216, 0.15)',
110 | _dark: 'rgba(115, 130, 147, 0.45)',
111 | },
112 | plannerItemCrossedBg: {
113 | default: 'rgba(196, 201, 216, 0.04)',
114 | _dark: 'rgba(115, 130, 147, 0.05)',
115 | },
116 | plannerItemCrossedHoverBg: {
117 | default: 'rgba(196, 201, 216, 0.06)',
118 | _dark: 'rgba(115, 130, 147, 0.2)',
119 | },
120 | plannerItemIcon: {
121 | default: 'rgb(107, 118, 153)',
122 | _dark: 'rgb(255, 255, 255, 0.4)',
123 | },
124 | plannerItemIconHover: {
125 | default: 'rgb(137, 150, 192)',
126 | _dark: 'rgb(255, 255, 255, 0.8)',
127 | },
128 | // Transparent button
129 | transparentButtonBorder: {
130 | default: 'rgb(111, 143, 196)',
131 | _dark: 'rgb(236, 142, 65)',
132 | },
133 | transparentButtonBg: {
134 | default: 'rgb(161, 192, 241, 0)',
135 | _dark: 'rgba(255, 140, 40, 0)',
136 | },
137 | transparentButtonHoverBg: {
138 | default: 'rgb(161, 192, 241, 0.1)',
139 | _dark: 'rgba(115, 130, 147, 0.25)',
140 | },
141 | transparentButtonActiveBg: {
142 | default: 'rgb(161, 192, 241, 0.3)',
143 | _dark: 'rgba(115, 130, 147, 0.5)',
144 | },
145 | // Notepad
146 | notepadButtonActiveBg: {
147 | default: 'rgb(161, 192, 241, 0.4)',
148 | _dark: 'rgba(255, 140, 40, 0.4)',
149 | },
150 | notepadBorder: {
151 | default: 'rgb(87, 111, 149)',
152 | _dark: 'rgba(255, 255, 255, 0.2)',
153 | },
154 | notepadPlaceholder: {
155 | default: 'rgba(255, 255, 255, 0.5)',
156 | _dark: 'rgba(255, 255, 255, 0.5)',
157 | },
158 | notepadIcon: {
159 | default: 'rgb(107, 118, 153, 0.8)',
160 | _dark: 'rgb(255, 255, 255, 0.4)',
161 | },
162 | notepadIconHover: {
163 | default: 'rgb(107, 118, 153)',
164 | _dark: 'rgb(255, 255, 255, 0.6)',
165 | },
166 | // Side buttons
167 | sideButtonBg: {
168 | default: 'rgba(45, 53, 80, 0.8)',
169 | _dark: 'rgba(57, 68, 81, 0.5)',
170 | },
171 | sideButtonHoverBg: {
172 | default: 'rgba(58, 67, 98, 0.8)',
173 | _dark: 'rgba(57, 68, 81, 0.8)',
174 | },
175 | sideButtonIcon: {
176 | default: 'rgba(182, 210, 227, 0.83)',
177 | _dark: 'rgba(255, 255, 255, 0.83)',
178 | },
179 | // Modals
180 | modalBg: {
181 | default: 'rgba(150, 167, 220, 0.15)',
182 | _dark: 'rgba(62, 74, 88, 0.5)',
183 | },
184 | modalBorder: {
185 | default: 'rgba(255,255,255,0.1)',
186 | _dark: 'rgba(68, 68, 68, 0.1)',
187 | },
188 | modalInputBg: {
189 | default: 'rgba(255, 255, 255, 0.068)',
190 | _dark: 'rgba(115, 130, 147, 0.2)',
191 | },
192 | modalInputHoverBg: {
193 | default: 'rgba(255, 255, 255, 0.1)',
194 | _dark: 'rgba(115, 130, 147, 0.65)',
195 | },
196 | // Snake game
197 | snakeMainBorder: {
198 | default: 'rgba(255,255,255,0.1)',
199 | _dark: 'rgba(255, 255, 255, 0.1)',
200 | },
201 | snakeSquareBorder: {
202 | default: '#CBD5E012',
203 | _dark: 'rgba(255, 255, 255, 0.05)',
204 | },
205 | snakeSquareBg: {
206 | default: 'rgba(48, 56, 83, 0.4)',
207 | _dark: 'rgba(0, 0, 0, 0.1)',
208 | },
209 | snakeGameBg: {
210 | default: 'rgba(255,255,255,0.1)',
211 | _dark: 'rgba(68, 68, 68, 0.1)',
212 | },
213 | snakeStartButtonBg: {
214 | default: 'green.600',
215 | _dark: 'green.600',
216 | },
217 | snakeStartButtonHoverBg: {
218 | default: 'green.500',
219 | _dark: 'green.500',
220 | },
221 | snakeRestartButtonBg: {
222 | default: 'blue.500',
223 | _dark: 'blue.500',
224 | },
225 | snakeRestartButtonHoverBg: {
226 | default: 'blue.400',
227 | _dark: 'blue.400',
228 | },
229 | snakeStartButtonText: {
230 | default: 'rgba(255,255,255,0.1)',
231 | _dark: 'secondaryText',
232 | },
233 | // Settings
234 | settingsBg: {
235 | default: 'rgb(47, 60, 76)',
236 | _dark: 'rgba(57, 68, 81, 0.5)',
237 | },
238 | settingsMainColor: {
239 | default: 'rgba(44, 165, 236, 0.726)',
240 | _dark: 'rgb(249, 161, 90)',
241 | },
242 | settingsRadioBefore: {
243 | default: 'rgba(255, 255, 255, 0.83)',
244 | _dark: 'rgba(32, 32, 32, 0.97)',
245 | },
246 | loaderBg: {
247 | default: 'rgb(179, 187, 213)',
248 | _dark: 'rgba(255, 255, 255, 0.83)',
249 | },
250 | // Mobile navigation
251 | mobileNavigationBg: {
252 | default: 'rgb(63, 77, 95)',
253 | _dark: 'rgb(48, 58, 69)',
254 | },
255 | mobileNavigationHoverBg: {
256 | default: 'rgb(77, 92, 112)',
257 | _dark: 'rgb(60, 72, 84)',
258 | },
259 | };
260 |
--------------------------------------------------------------------------------
/src/theme/extendedColors.ts:
--------------------------------------------------------------------------------
1 | // Default theme: "Post-Apo"
2 | // Dark theme: "Fairytale"
3 |
4 | export const extendedColors = {
5 | homepageBg: {
6 | default: 'rgb(28, 28, 28)',
7 | _dark: 'rgba(60, 60, 60, 0.5)',
8 | },
9 | primaryText: {
10 | default: 'rgb(255,255,255)',
11 | _dark: 'rgb(255, 255, 255)',
12 | },
13 | secondaryText: {
14 | default: 'rgba(255, 255, 255, 0.87)',
15 | _dark: 'rgba(255, 255, 255, 0.87)',
16 | },
17 | disabledText: {
18 | default: 'rgb(255,255,255,0.4)',
19 | _dark: 'rgba(255, 255, 255, 0.4)',
20 | },
21 | coloredText: {
22 | default: 'rgb(161, 170, 199)',
23 | _dark: 'rgb(99, 208, 90)',
24 | },
25 | contentBg: {
26 | default: 'rgba(57, 57, 57, 0.88)',
27 | _dark: 'rgba(57, 68, 81, 0.5)',
28 | },
29 | contentBorder: {
30 | default: 'rgb(255,255,255,0)',
31 | _dark: 'rgba(255, 255, 255, 0.1)',
32 | },
33 | screenBg: {
34 | default: '#2c2f35',
35 | _dark: '#222222',
36 | },
37 | mainColor: {
38 | default: 'rgb(185, 152, 139)',
39 | _dark: 'rgb(99, 208, 90)',
40 | },
41 | mainColorHover: {
42 | default: 'rgba(44, 165, 236, 0.926)',
43 | _dark: 'rgb(255, 120, 10)',
44 | },
45 | coloredButtonBg: {
46 | default: 'rgb(125, 97, 85)',
47 | _dark: 'rgb(99, 157, 95)',
48 | },
49 | coloredButtonHoverBg: {
50 | default: 'rgb(146, 117, 104)',
51 | _dark: 'rgb(123, 183, 119)',
52 | },
53 | // Greeting
54 | welcomePrimaryText: {
55 | default: 'rgb(255,255,255)',
56 | _dark: 'rgb(99, 208, 90)',
57 | },
58 | welcomeSecondaryText: {
59 | default: 'rgba(255, 255, 255, 0.75)',
60 | _dark: 'rgba(255, 255, 255, 0.75)',
61 | },
62 | welcomeIcon: {
63 | default: 'rgba(255, 255, 255, 0.67)',
64 | _dark: 'rgb(99, 208, 90)',
65 | },
66 | welcomeIconHover: {
67 | default: 'rgb(185, 152, 139)',
68 | _dark: 'rgb(255, 255, 255, 0.8)',
69 | },
70 | // Weather forecast
71 | weatherCountryText: {
72 | default: 'rgba(255, 255, 255, 0.67)',
73 | _dark: 'rgb(255, 255, 255, 0.8)',
74 | },
75 | weatherDescText: {
76 | default: 'rgba(255, 255, 255, 0.87)',
77 | _dark: 'rgb(255, 255, 255, 0.8)',
78 | },
79 | weatherIcon: {
80 | default: 'rgb(185, 152, 139)',
81 | _dark: 'rgb(99, 208, 90)',
82 | },
83 | weatherIconHover: {
84 | default: 'rgb(156, 168, 208)',
85 | _dark: 'rgb(255, 255, 255, 0.8)',
86 | },
87 | weatherBoxBg: {
88 | default: 'rgba(255, 255, 255, 0.034)',
89 | _dark: 'rgba(115, 130, 147, 0.2)',
90 | },
91 | weatherBoxHoverBg: {
92 | default: 'rgba(255, 255, 255, 0.1)',
93 | _dark: 'rgba(115, 130, 147, 0.4)',
94 | },
95 | // Task list
96 | plannerInputBg: {
97 | default: 'rgba(255, 255, 255, 0.068)',
98 | _dark: 'rgba(115, 130, 147, 0.2)',
99 | },
100 | plannerInputHoverBg: {
101 | default: 'rgba(255, 255, 255, 0.1)',
102 | _dark: 'rgba(115, 130, 147, 0.45)',
103 | },
104 | plannerItemBg: {
105 | default: 'rgba(196, 201, 216, 0.09)',
106 | _dark: 'rgba(115, 130, 147, 0.2)',
107 | },
108 | plannerItemHoverBg: {
109 | default: 'rgba(196, 201, 216, 0.15)',
110 | _dark: 'rgba(115, 130, 147, 0.45)',
111 | },
112 | plannerItemCrossedBg: {
113 | default: 'rgba(196, 201, 216, 0.04)',
114 | _dark: 'rgba(115, 130, 147, 0.05)',
115 | },
116 | plannerItemCrossedHoverBg: {
117 | default: 'rgba(196, 201, 216, 0.06)',
118 | _dark: 'rgba(115, 130, 147, 0.2)',
119 | },
120 | plannerItemIcon: {
121 | default: 'rgba(255, 255, 255, 0.3)',
122 | _dark: 'rgb(255, 255, 255, 0.3)',
123 | },
124 | plannerItemIconHover: {
125 | default: 'rgb(255, 255, 255, 0.8)',
126 | _dark: 'rgb(255, 255, 255, 0.8)',
127 | },
128 | // Transparent button
129 | transparentButtonBorder: {
130 | default: 'rgb(153, 107, 89)',
131 | _dark: 'rgb(99, 208, 90)',
132 | },
133 | transparentButtonBg: {
134 | default: 'rgb(161, 192, 241, 0)',
135 | _dark: 'rgba(255, 140, 40, 0)',
136 | },
137 | transparentButtonHoverBg: {
138 | default: 'rgb(185, 152, 139, 0.1)',
139 | _dark: 'rgb(161, 192, 241, 0.09)',
140 | },
141 | transparentButtonActiveBg: {
142 | default: 'rgb(185, 152, 139, 0.2)',
143 | _dark: 'rgb(161, 192, 241, 0.2)',
144 | },
145 | // Notepad
146 | notepadButtonActiveBg: {
147 | default: 'rgb(185, 152, 139, 0.3)',
148 | _dark: 'rgb(161, 192, 241, 0.4)',
149 | },
150 | notepadBorder: {
151 | default: 'rgba(255, 255, 255, 0.25)',
152 | _dark: 'rgba(255, 255, 255, 0.25)',
153 | },
154 | notepadPlaceholder: {
155 | default: 'rgba(255, 255, 255, 0.5)',
156 | _dark: 'rgba(255, 255, 255, 0.5)',
157 | },
158 | notepadIcon: {
159 | default: 'rgb(255, 255, 255, 0.35)',
160 | _dark: 'rgb(255, 255, 255, 0.35)',
161 | },
162 | notepadIconHover: {
163 | default: 'rgb(255, 255, 255, 0.45)',
164 | _dark: 'rgb(255, 255, 255, 0.45)',
165 | },
166 | // Side buttons
167 | sideButtonBg: {
168 | default: 'rgba(77, 77, 77, 0.6)',
169 | _dark: 'rgba(57, 68, 81, 0.5)',
170 | },
171 | sideButtonHoverBg: {
172 | default: 'rgba(77, 77, 77, 0.8)',
173 | _dark: 'rgba(57, 68, 81, 0.8)',
174 | },
175 | sideButtonIcon: {
176 | default: 'rgba(255, 255, 255, 0.83)',
177 | _dark: 'rgba(255, 255, 255, 0.83)',
178 | },
179 | // Modals
180 | modalBg: {
181 | default: 'rgba(57, 57, 57, 0.88)',
182 | _dark: 'rgba(57, 68, 81, 0.4)',
183 | },
184 | modalBorder: {
185 | default: 'rgba(255,255,255,0.1)',
186 | _dark: 'rgba(68, 68, 68, 0.1)',
187 | },
188 | modalInputBg: {
189 | default: 'rgba(255, 255, 255, 0.068)',
190 | _dark: 'rgba(115, 130, 147, 0.2)',
191 | },
192 | modalInputHoverBg: {
193 | default: 'rgba(255, 255, 255, 0.1)',
194 | _dark: 'rgba(115, 130, 147, 0.65)',
195 | },
196 | // Snake game
197 | snakeMainBorder: {
198 | default: 'rgba(255,255,255,0.1)',
199 | _dark: 'rgba(255, 255, 255, 0.1)',
200 | },
201 | snakeSquareBorder: {
202 | default: '#CBD5E012',
203 | _dark: 'rgba(255, 255, 255, 0.05)',
204 | },
205 | snakeSquareBg: {
206 | default: 'rgba(72, 70, 66, 0.51)',
207 | _dark: 'rgba(57, 68, 81, 0.5)',
208 | },
209 | snakeGameBg: {
210 | default: 'rgba(255,255,255,0.1)',
211 | _dark: 'rgba(68, 68, 68, 0.1)',
212 | },
213 | snakeStartButtonBg: {
214 | default: 'green.600',
215 | _dark: 'green.600',
216 | },
217 | snakeStartButtonHoverBg: {
218 | default: 'green.500',
219 | _dark: 'green.500',
220 | },
221 | snakeRestartButtonBg: {
222 | default: 'blue.500',
223 | _dark: 'blue.500',
224 | },
225 | snakeRestartButtonHoverBg: {
226 | default: 'blue.400',
227 | _dark: 'blue.400',
228 | },
229 | snakeStartButtonText: {
230 | default: 'rgba(255,255,255,0.1)',
231 | _dark: 'secondaryText',
232 | },
233 | // Settings
234 | settingsBg: {
235 | default: 'rgba(57, 57, 57, 0.68)',
236 | _dark: 'rgba(57, 68, 81, 0.5)',
237 | },
238 | settingsMainColor: {
239 | default: 'rgb(153, 107, 89)',
240 | _dark: 'rgb(99, 208, 90)',
241 | },
242 | settingsRadioBefore: {
243 | default: 'rgba(255, 255, 255, 0.83)',
244 | _dark: 'rgba(32, 32, 32, 0.97)',
245 | },
246 | loaderBg: {
247 | default: 'rgb(181, 178, 178)',
248 | _dark: 'rgba(255, 255, 255, 0.83)',
249 | },
250 | // Mobile navigation
251 | mobileNavigationBg: {
252 | default: 'rgb(65, 65, 65)',
253 | _dark: 'rgb(48, 58, 69)',
254 | },
255 | mobileNavigationHoverBg: {
256 | default: 'rgba(94, 93, 93, 0.65)',
257 | _dark: 'rgb(73, 85, 99)',
258 | },
259 | };
260 |
--------------------------------------------------------------------------------
/src/utils/countryMapping.ts:
--------------------------------------------------------------------------------
1 | const countryToCode: { [key: string]: string } = {
2 | Afghanistan: 'AF',
3 | Albania: 'AL',
4 | Algeria: 'DZ',
5 | 'American Samoa': 'AS',
6 | Andorra: 'AD',
7 | Angola: 'AO',
8 | Anguilla: 'AI',
9 | Antarctica: 'AQ',
10 | 'Antigua and Barbuda': 'AG',
11 | Argentina: 'AR',
12 | Armenia: 'AM',
13 | Aruba: 'AW',
14 | Australia: 'AU',
15 | Austria: 'AT',
16 | Azerbaijan: 'AZ',
17 | Bahamas: 'BS',
18 | Bahrain: 'BH',
19 | Bangladesh: 'BD',
20 | Barbados: 'BB',
21 | Belarus: 'BY',
22 | Belgium: 'BE',
23 | Belize: 'BZ',
24 | Benin: 'BJ',
25 | Bermuda: 'BM',
26 | Bhutan: 'BT',
27 | Bolivia: 'BO',
28 | 'Bosnia and Herzegovina': 'BA',
29 | Botswana: 'BW',
30 | Brazil: 'BR',
31 | 'British Indian Ocean Territory': 'IO',
32 | Brunei: 'BN',
33 | Bulgaria: 'BG',
34 | 'Burkina Faso': 'BF',
35 | Burundi: 'BI',
36 | Cambodia: 'KH',
37 | Cameroon: 'CM',
38 | Canada: 'CA',
39 | 'Cape Verde': 'CV',
40 | 'Cayman Islands': 'KY',
41 | 'Central African Republic': 'CF',
42 | Chad: 'TD',
43 | Chile: 'CL',
44 | China: 'CN',
45 | 'Christmas Island': 'CX',
46 | 'Cocos Islands': 'CC',
47 | Colombia: 'CO',
48 | Comoros: 'KM',
49 | Congo: 'CG',
50 | 'Cook Islands': 'CK',
51 | 'Costa Rica': 'CR',
52 | Croatia: 'HR',
53 | Cuba: 'CU',
54 | Curacao: 'CW',
55 | Cyprus: 'CY',
56 | 'Czech Republic': 'CZ',
57 | Czechia: 'CZ',
58 | 'Democratic Republic of the Congo': 'CD',
59 | Denmark: 'DK',
60 | Djibouti: 'DJ',
61 | Dominica: 'DM',
62 | 'Dominican Republic': 'DO',
63 | 'East Timor': 'TL',
64 | Ecuador: 'EC',
65 | Egypt: 'EG',
66 | 'El Salvador': 'SV',
67 | 'Equatorial Guinea': 'GQ',
68 | Eritrea: 'ER',
69 | Estonia: 'EE',
70 | Ethiopia: 'ET',
71 | 'Falkland Islands': 'FK',
72 | 'Faroe Islands': 'FO',
73 | Fiji: 'FJ',
74 | Finland: 'FI',
75 | France: 'FR',
76 | 'French Guiana': 'GF',
77 | 'French Polynesia': 'PF',
78 | Gabon: 'GA',
79 | Gambia: 'GM',
80 | Georgia: 'GE',
81 | Germany: 'DE',
82 | Ghana: 'GH',
83 | Gibraltar: 'GI',
84 | Greece: 'GR',
85 | Greenland: 'GL',
86 | Grenada: 'GD',
87 | Guadeloupe: 'GP',
88 | Guam: 'GU',
89 | Guatemala: 'GT',
90 | Guernsey: 'GG',
91 | Guinea: 'GN',
92 | 'Guinea-Bissau': 'GW',
93 | Guyana: 'GY',
94 | Haiti: 'HT',
95 | Honduras: 'HN',
96 | 'Hong Kong': 'HK',
97 | Hungary: 'HU',
98 | Iceland: 'IS',
99 | India: 'IN',
100 | Indonesia: 'ID',
101 | Iran: 'IR',
102 | Iraq: 'IQ',
103 | Ireland: 'IE',
104 | 'Isle of Man': 'IM',
105 | Israel: 'IL',
106 | Italy: 'IT',
107 | 'Ivory Coast': 'CI',
108 | Jamaica: 'JM',
109 | Japan: 'JP',
110 | Jersey: 'JE',
111 | Jordan: 'JO',
112 | Kazakhstan: 'KZ',
113 | Kenya: 'KE',
114 | Kiribati: 'KI',
115 | Kosovo: 'XK',
116 | Kuwait: 'KW',
117 | Kyrgyzstan: 'KG',
118 | Laos: 'LA',
119 | Latvia: 'LV',
120 | Lebanon: 'LB',
121 | Lesotho: 'LS',
122 | Liberia: 'LR',
123 | Libya: 'LY',
124 | Liechtenstein: 'LI',
125 | Lithuania: 'LT',
126 | Luxembourg: 'LU',
127 | Macao: 'MO',
128 | Madagascar: 'MG',
129 | Malawi: 'MW',
130 | Malaysia: 'MY',
131 | Maldives: 'MV',
132 | Mali: 'ML',
133 | Malta: 'MT',
134 | 'Marshall Islands': 'MH',
135 | Martinique: 'MQ',
136 | Mauritania: 'MR',
137 | Mauritius: 'MU',
138 | Mayotte: 'YT',
139 | Mexico: 'MX',
140 | Micronesia: 'FM',
141 | Moldova: 'MD',
142 | Monaco: 'MC',
143 | Mongolia: 'MN',
144 | Montenegro: 'ME',
145 | Montserrat: 'MS',
146 | Morocco: 'MA',
147 | Mozambique: 'MZ',
148 | Myanmar: 'MM',
149 | Namibia: 'NA',
150 | Nauru: 'NR',
151 | Nepal: 'NP',
152 | Netherlands: 'NL',
153 | 'New Caledonia': 'NC',
154 | 'New Zealand': 'NZ',
155 | Nicaragua: 'NI',
156 | Niger: 'NE',
157 | Nigeria: 'NG',
158 | Niue: 'NU',
159 | 'Norfolk Island': 'NF',
160 | 'North Korea': 'KP',
161 | 'North Macedonia': 'MK',
162 | 'Northern Mariana Islands': 'MP',
163 | Norway: 'NO',
164 | Oman: 'OM',
165 | Pakistan: 'PK',
166 | Palau: 'PW',
167 | Palestine: 'PS',
168 | Panama: 'PA',
169 | 'Papua New Guinea': 'PG',
170 | Paraguay: 'PY',
171 | Peru: 'PE',
172 | Philippines: 'PH',
173 | Pitcairn: 'PN',
174 | Poland: 'PL',
175 | Portugal: 'PT',
176 | 'Puerto Rico': 'PR',
177 | Qatar: 'QA',
178 | Romania: 'RO',
179 | Russia: 'RU',
180 | 'Russian Federation': 'RU',
181 | Rwanda: 'RW',
182 | Reunion: 'RE',
183 | 'Saint Barthelemy': 'BL',
184 | 'Saint Helena': 'SH',
185 | 'Saint Kitts and Nevis': 'KN',
186 | 'Saint Lucia': 'LC',
187 | 'Saint Martin': 'MF',
188 | 'Saint Pierre and Miquelon': 'PM',
189 | 'Saint Vincent and the Grenadines': 'VC',
190 | Samoa: 'WS',
191 | 'San Marino': 'SM',
192 | 'Sao Tome and Principe': 'ST',
193 | 'Saudi Arabia': 'SA',
194 | Senegal: 'SN',
195 | Serbia: 'RS',
196 | Seychelles: 'SC',
197 | 'Sierra Leone': 'SL',
198 | Singapore: 'SG',
199 | 'Sint Maarten': 'SX',
200 | Slovakia: 'SK',
201 | Slovenia: 'SI',
202 | 'Solomon Islands': 'SB',
203 | Somalia: 'SO',
204 | 'South Africa': 'ZA',
205 | 'South Georgia and the South Sandwich Islands': 'GS',
206 | 'South Korea': 'KR',
207 | 'South Sudan': 'SS',
208 | Spain: 'ES',
209 | 'Sri Lanka': 'LK',
210 | Sudan: 'SD',
211 | Suriname: 'SR',
212 | 'Svalbard and Jan Mayen': 'SJ',
213 | Sweden: 'SE',
214 | Switzerland: 'CH',
215 | Syria: 'SY',
216 | Taiwan: 'TW',
217 | Tajikistan: 'TJ',
218 | Tanzania: 'TZ',
219 | Thailand: 'TH',
220 | 'Timor-Leste': 'TL',
221 | Togo: 'TG',
222 | Tokelau: 'TK',
223 | Tonga: 'TO',
224 | 'Trinidad and Tobago': 'TT',
225 | Tunisia: 'TN',
226 | Turkey: 'TR',
227 | Turkmenistan: 'TM',
228 | 'Turks and Caicos Islands': 'TC',
229 | Tuvalu: 'TV',
230 | Uganda: 'UG',
231 | Ukraine: 'UA',
232 | 'United Arab Emirates': 'AE',
233 | 'United Kingdom': 'GB',
234 | 'United States': 'US',
235 | 'United States of America': 'US',
236 | USA: 'US',
237 | Uruguay: 'UY',
238 | Uzbekistan: 'UZ',
239 | Vanuatu: 'VU',
240 | 'Vatican City': 'VA',
241 | Venezuela: 'VE',
242 | Vietnam: 'VN',
243 | 'Virgin Islands, British': 'VG',
244 | 'Virgin Islands, U.S.': 'VI',
245 | 'Wallis and Futuna': 'WF',
246 | 'Western Sahara': 'EH',
247 | Yemen: 'YE',
248 | Zambia: 'ZM',
249 | Zimbabwe: 'ZW',
250 | 'Aland Islands': 'AX',
251 | };
252 |
253 | // Add common variations of country names
254 | const countryVariations: { [key: string]: string } = {
255 | UK: 'GB',
256 | US: 'US',
257 | USA: 'US',
258 | 'United States': 'US',
259 | Britain: 'GB',
260 | 'Great Britain': 'GB',
261 | Korea: 'KR',
262 | 'Republic of Korea': 'KR',
263 | 'North Korea': 'KP',
264 | "Democratic People's Republic of Korea": 'KP',
265 | Russia: 'RU',
266 | 'Russian Federation': 'RU',
267 | Taiwan: 'TW',
268 | 'Taiwan, Province of China': 'TW',
269 | Iran: 'IR',
270 | 'Iran, Islamic Republic of': 'IR',
271 | Venezuela: 'VE',
272 | 'Venezuela, Bolivarian Republic of': 'VE',
273 | Vietnam: 'VN',
274 | 'Viet Nam': 'VN',
275 | Syria: 'SY',
276 | 'Syrian Arab Republic': 'SY',
277 | Tanzania: 'TZ',
278 | 'Tanzania, United Republic of': 'TZ',
279 | 'Czech Republic': 'CZ',
280 | Czechia: 'CZ',
281 | };
282 |
283 | export const getCountryCode = (countryName: string): string => {
284 | // First try the main mapping
285 | const code = countryToCode[countryName];
286 | if (code) return code;
287 |
288 | // Then try variations
289 | const variationCode = countryVariations[countryName];
290 | if (variationCode) return variationCode;
291 |
292 | // If no mapping found, return the original name and log a warning in development
293 | if (process.env.NODE_ENV === 'development') {
294 | console.warn(`No country code mapping found for: ${countryName}`);
295 | }
296 | return countryName;
297 | };
298 |
--------------------------------------------------------------------------------
/src/hooks/useSnake.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useColorMode } from '@chakra-ui/react';
3 |
4 | import { useSnakeStore } from '@/store/snakeStore';
5 | import { useSettingsStore } from '@/store/settingsStore';
6 |
7 | export const useSnake = () => {
8 | const [board, setBoard] = useState([]);
9 | const [snake, setSnake] = useState([{ row: 10, col: 10 }]);
10 | const [direction, setDirection] = useState({ row: 0, col: 1 });
11 | const [food, setFood] = useState({ row: 5, col: 5 });
12 | const [score, setScore] = useState(0); //
13 | const [isGameRunning, setIsGameRunning] = useState(false);
14 | const [gameOver, setGameOver] = useState(false);
15 | const [restart, setRestart] = useState(false);
16 | const { record, setRecord } = useSnakeStore();
17 | const [canChangeDirection, setCanChangeDirection] = useState(true);
18 | const { sliderValue } = useSettingsStore();
19 | const [newRecord, setNewRecord] = useState(false);
20 | const [shouldResultTitleAppear, setShouldResultTitleAppear] = useState(true);
21 | const [shouldIntroTitleAppear, setShouldIntroTitleAppear] = useState(true);
22 |
23 | const startGame = () => {
24 | if (!isGameRunning) {
25 | setIsGameRunning(true);
26 | setShouldResultTitleAppear(false);
27 | setShouldIntroTitleAppear(false);
28 | setGameOver(false);
29 | setRestart(false);
30 | setNewRecord(false);
31 | if (gameOver) {
32 | resetGame();
33 | }
34 | }
35 | };
36 | const getIntervalFromSlider = (sliderValue: number) => {
37 | return (200 - (sliderValue / 100) * 180) * 0.5;
38 | };
39 |
40 | const resetGame = () => {
41 | setIsGameRunning(false);
42 | setGameOver(false);
43 | setScore(0);
44 | setSnake([{ row: 10, col: 10 }]);
45 | setDirection({ row: 0, col: 1 });
46 | setFood({ row: 5, col: 5 });
47 | initializeBoard();
48 | setRestart(true);
49 | setShouldIntroTitleAppear(true);
50 | setNewRecord(false);
51 | };
52 |
53 | useEffect(() => {
54 | initializeBoard();
55 | generateFood();
56 | }, []);
57 |
58 | useEffect(() => {
59 | const handleKeyPress = (e: KeyboardEvent) => {
60 | if (!isGameRunning) {
61 | if (
62 | e.key === 'ArrowUp' ||
63 | e.key === 'ArrowDown' ||
64 | e.key === 'ArrowLeft' ||
65 | e.key === 'ArrowRight'
66 | ) {
67 | startGame();
68 | } else {
69 | return;
70 | }
71 | }
72 |
73 | if (!canChangeDirection) return;
74 |
75 | if (
76 | e.key === 'ArrowUp' &&
77 | JSON.stringify(direction) !== JSON.stringify({ row: 1, col: 0 }) &&
78 | JSON.stringify(direction) !== JSON.stringify({ row: -1, col: 0 })
79 | ) {
80 | setDirection({ row: -1, col: 0 });
81 | setCanChangeDirection(false);
82 | }
83 | if (
84 | e.key === 'ArrowDown' &&
85 | JSON.stringify(direction) !== JSON.stringify({ row: -1, col: 0 }) &&
86 | JSON.stringify(direction) !== JSON.stringify({ row: 1, col: 0 })
87 | ) {
88 | setDirection({ row: 1, col: 0 });
89 | setCanChangeDirection(false);
90 | }
91 | if (
92 | e.key === 'ArrowLeft' &&
93 | JSON.stringify(direction) !== JSON.stringify({ row: 0, col: 1 }) &&
94 | JSON.stringify(direction) !== JSON.stringify({ row: 0, col: -1 })
95 | ) {
96 | setDirection({ row: 0, col: -1 });
97 | setCanChangeDirection(false);
98 | }
99 | if (
100 | e.key === 'ArrowRight' &&
101 | JSON.stringify(direction) !== JSON.stringify({ row: 0, col: -1 }) &&
102 | JSON.stringify(direction) !== JSON.stringify({ row: 0, col: 1 })
103 | ) {
104 | setDirection({ row: 0, col: 1 });
105 | setCanChangeDirection(false);
106 | }
107 | };
108 |
109 | window.addEventListener('keydown', handleKeyPress);
110 |
111 | return () => {
112 | window.removeEventListener('keydown', handleKeyPress);
113 | };
114 | }, [isGameRunning, gameOver, direction, canChangeDirection]);
115 |
116 | useEffect(() => {
117 | if (gameOver) {
118 | if (score > record) {
119 | setNewRecord(true);
120 | setRecord(score);
121 | useSnakeStore.getState().setRecord(score);
122 | } else {
123 | setNewRecord(false);
124 | setShouldResultTitleAppear(true);
125 | }
126 | }
127 | }, [gameOver]);
128 |
129 | useEffect(() => {
130 | setShouldResultTitleAppear(true);
131 | }, [newRecord]);
132 | useEffect(() => {
133 | const moveSnake = () => {
134 | if (!isGameRunning || gameOver) return;
135 |
136 | const newHead = {
137 | row: snake[0].row + direction.row,
138 | col: snake[0].col + direction.col,
139 | };
140 |
141 | // Check if the snake has collided with itself
142 | if (isSnake(newHead.row, newHead.col)) {
143 | console.log('Game over! You collided with your body.');
144 | setIsGameRunning(false);
145 | setGameOver(true);
146 | return;
147 | }
148 |
149 | // Check if the snake has collided with a wall
150 | if (
151 | newHead.row < 0 ||
152 | newHead.row >= board.length ||
153 | newHead.col < 0 ||
154 | newHead.col >= board[0].length
155 | ) {
156 | console.log('Game over! You collided with a wall.');
157 | setIsGameRunning(false);
158 | setGameOver(true);
159 | return;
160 | }
161 | if (newHead.row === food.row && newHead.col === food.col) {
162 | // Generate new food
163 | generateFood();
164 | // Increment score by 10
165 | setScore(score + 10);
166 | } else {
167 | // Remove the tail if the snake hasn't eaten food
168 | snake.pop();
169 | }
170 |
171 | // Add the new head to the snake
172 | snake.unshift(newHead);
173 | setSnake([...snake]);
174 |
175 | // Allow changing direction after the snake has moved
176 | setCanChangeDirection(true);
177 | };
178 |
179 | let interval: any;
180 |
181 | if (isGameRunning && !gameOver) {
182 | interval = setInterval(moveSnake, getIntervalFromSlider(sliderValue));
183 | }
184 |
185 | return () => {
186 | clearInterval(interval);
187 | };
188 | }, [snake, direction, food, isGameRunning, gameOver]);
189 |
190 | const initializeBoard = () => {
191 | const newBoard: any[][] = [];
192 | for (let row = 0; row < 20; row++) {
193 | newBoard.push([]);
194 | for (let col = 0; col < 30; col++) {
195 | // Change the number of columns to 30
196 | newBoard[row].push(null);
197 | }
198 | }
199 | setBoard(newBoard);
200 | };
201 |
202 | const isSnake = (row: number, col: number) => {
203 | return snake.some((segment) => segment.row === row && segment.col === col);
204 | };
205 |
206 | const generateFood = () => {
207 | let newFoodPosition;
208 |
209 | do {
210 | newFoodPosition = {
211 | row: Math.floor(Math.random() * 20),
212 | col: Math.floor(Math.random() * 30), // Change the random column generation multiplier to 30
213 | };
214 | } while (isSnake(newFoodPosition.row, newFoodPosition.col));
215 |
216 | setFood(newFoodPosition);
217 | };
218 |
219 | const isFood = (row: number, col: number) => {
220 | return food.row === row && food.col === col;
221 | };
222 | const { colorMode } = useColorMode();
223 | return {
224 | board,
225 | snake,
226 | direction,
227 | food,
228 | score,
229 | isGameRunning,
230 | gameOver,
231 | restart,
232 | record,
233 | canChangeDirection,
234 | sliderValue,
235 | newRecord,
236 | shouldResultTitleAppear,
237 | shouldIntroTitleAppear,
238 | startGame,
239 | getIntervalFromSlider,
240 | resetGame,
241 | isSnake,
242 | isFood,
243 | colorMode,
244 | };
245 | };
246 |
--------------------------------------------------------------------------------
/src/components/settings/SettingsContent.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Flex, Icon, Select, Tooltip } from '@chakra-ui/react';
2 |
3 | import { SettingsSection } from './SettingsSection';
4 | import { SettingsTitle } from './SettingsTitle';
5 | import { SettingsSectionRow } from './SettingsSectionRow';
6 | import { SettingsGithub } from './SettingsGithub';
7 | import { SettingsSlider } from './SettingsSlider';
8 | import { useSettings } from '@/hooks/useSettings';
9 | import { ArrowLeftIcon } from '@/assets/icons/ArrowLeftIcon';
10 | import { PictureIcon } from '@/assets/icons/PictureIcon';
11 | import useSettingsStore from '@/store/settingsStore';
12 |
13 | interface SettingsContentProps {
14 | onEditUserData: () => void;
15 | onClearAllData: () => void;
16 | onCloseSettings: () => void;
17 | }
18 |
19 | export const SettingsContent = ({
20 | onEditUserData,
21 | onCloseSettings,
22 | onClearAllData,
23 | }: SettingsContentProps) => {
24 | const {
25 | colorMode,
26 | isFullPlannerVisible,
27 | useFahrenheit,
28 | welcomeSectionContent,
29 | showSnakeButton,
30 | handleUseFahrenheitChange,
31 | handleFullPlannerVisibleChange,
32 | handleShowSnakeButtonChange,
33 | handleRadioChange,
34 | handleThemeChange,
35 | theme,
36 | setThemeValue,
37 | } = useSettings();
38 | const { isImageVisible, setIsImageVisible } = useSettingsStore(
39 | (state) => state
40 | );
41 | const handleArrowClick = () => {
42 | const themeOrder = [
43 | 'light_basicTheme',
44 | 'dark_basicTheme',
45 | 'light_extendedTheme',
46 | 'dark_extendedTheme',
47 | ];
48 | const currentIndex = themeOrder.indexOf(`${colorMode}_${theme}`);
49 | const nextIndex = (currentIndex + 1) % themeOrder.length;
50 | const nextTheme = themeOrder[nextIndex];
51 | handleThemeChange(nextTheme);
52 | setThemeValue(nextTheme);
53 | };
54 | return (
55 | <>
56 |
67 |
68 |
76 | Settings
77 |
78 |
79 |
80 |
81 |
82 |
83 |
106 | {/*
116 | setIsImageVisible(!isImageVisible)}
125 | cursor="pointer"
126 | sx={{
127 | '& svg': {
128 | fill: 'rgb(255,255,255,0.5)',
129 | },
130 | }}
131 | _hover={{
132 | '& svg': {
133 | fill: 'rgb(255,255,255,0.8)',
134 | },
135 | }}>
136 |
137 |
138 | */}
139 |
154 |
159 |
160 |
161 |
162 |
163 |
164 |
173 |
174 |
175 |
176 |
182 |
183 |
184 |
185 |
191 |
192 |
193 |
194 |
200 |
204 | Snake speed
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
223 |
233 |
234 |
235 |
236 |
237 |
238 | >
239 | );
240 | };
241 |
--------------------------------------------------------------------------------