Copied all potty chart code without changes.

This commit is contained in:
Lucid Kobold
2023-11-09 17:21:03 -05:00
parent 5d0660345c
commit d92384a3b8
68 changed files with 8272 additions and 4230 deletions

View File

@@ -13,12 +13,12 @@ name: "CodeQL"
on: on:
push: push:
branches: [ main ] branches: [main, stable]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ main ] branches: [main, stable]
schedule: schedule:
- cron: '21 19 * * 4' - cron: "21 19 * * 4"
jobs: jobs:
analyze: analyze:
@@ -32,39 +32,39 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: [ 'javascript' ] language: ["javascript"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support # Learn more about CodeQL language support at https://git.io/codeql-language-support
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file. # By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file. # Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main # queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project # and modify them (or add more) to build your code if your project
# uses a compiled language # uses a compiled language
#- run: | #- run: |
# make bootstrap # make bootstrap
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@v2

View File

@@ -6,24 +6,35 @@ name: Node.js CI
on: on:
# Trigger the workflow on push or pull request, # Trigger the workflow on push or pull request,
push: push:
branches: branches: [main, stable]
# All branches
- 'main'
pull_request: pull_request:
# These types of PRs # These types of PRs
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [14.x, 16.x, 18.x] node-version: [14.x, 16.x, 18.x, 20.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
# NPM
# - name: Use Node.js ${{ matrix.node-version }}
# uses: actions/setup-node@v2
# with:
# node-version: ${{ matrix.node-version }}
# cache: 'npm'
# - run: npm i
# - run: npm run lint
# - run: npm run build
# # - run: npm run start
# - run: npm run test
# YARN
- uses: borales/actions-yarn@v3.0.0 - uses: borales/actions-yarn@v3.0.0
with: with:
cmd: install # will run `yarn install` command cmd: install # will run `yarn install` command

View File

@@ -10,26 +10,26 @@ name: njsscan sarif
on: on:
push: push:
branches: [ main ] branches: [main, stable]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ main ] branches: [main, stable]
schedule: schedule:
- cron: '15 15 * * 5' - cron: "15 15 * * 5"
jobs: jobs:
njsscan: njsscan:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: njsscan code scanning name: njsscan code scanning
steps: steps:
- name: Checkout the code - name: Checkout the code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: nodejsscan scan - name: nodejsscan scan
id: njsscan id: njsscan
uses: ajinabraham/njsscan-action@7237412fdd36af517e2745077cedbf9d6900d711 uses: ajinabraham/njsscan-action@7237412fdd36af517e2745077cedbf9d6900d711
with: with:
args: '. --sarif --output results.sarif || true' args: ". --sarif --output results.sarif || true"
- name: Upload njsscan report - name: Upload njsscan report
uses: github/codeql-action/upload-sarif@v1 uses: github/codeql-action/upload-sarif@v2
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

@@ -14,4 +14,5 @@ next-env.d
package* package*
tsconfig.json tsconfig.json
yarn.lock yarn.lock
package.lock
next.config.js next.config.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

893
.yarn/releases/yarn-4.0.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

7
.yarnrc copy.yml Normal file
View File

@@ -0,0 +1,7 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.6.4.cjs

View File

@@ -1,7 +1,7 @@
compressionLevel: mixed
enableGlobalCache: false
nodeLinker: node-modules nodeLinker: node-modules
plugins: yarnPath: .yarn/releases/yarn-4.0.1.cjs
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.2.1.cjs

64
data/stickerSeeder.ts Normal file
View File

@@ -0,0 +1,64 @@
import {
format,
getDaysInMonth,
isBefore,
setDate,
startOfDay
} from "date-fns";
/**
* Generated a valid sticker value for use when generating a sticker obj.
* @returns {ValidStickerVal} a number that will represent a valid sticker value.
*/
const generateSticker = (): -2 | -1 | 0 | 1 | 2 => {
const sticker = Math.floor(Math.random() * (2 - -2 + 1)) + -2;
if (
sticker === -2 ||
sticker === -1 ||
sticker === 0 ||
sticker === 1 ||
sticker === 2
) {
return sticker;
}
};
// TODO: Update so seeder takes in a month or date to then generate the stickers for it.
/**
* This seeder is to simulate the date and sticker info from the database.
* Filling up an array for the current month with sticker from ths first to
* the day before the current date, leaving the rest of the month empty.
* @returns {StickerDays} an array with populated sticker objects that correspond to the current month's info.
*/
const stickersSeeder = (): StickerDays => {
const stickers = [] as Sticker[];
const now = startOfDay(new Date());
const daysOfThisMonth = getDaysInMonth(now);
for (let i = 1; i <= daysOfThisMonth; i++) {
const currDate = setDate(now, i);
const sticker = isBefore(currDate, now) ? generateSticker() : null;
const id =
format(currDate, "yyyyddLL") + `/${sticker === null ? 0 : sticker}`;
const newSticker: Sticker = {
id: id,
date: currDate.toJSON(),
sticker: sticker,
edited: false,
manual: false
};
stickers.push(newSticker);
}
if (stickers.length === daysOfThisMonth) {
return stickers;
}
};
export default stickersSeeder;

View File

@@ -1,10 +0,0 @@
// You can include shared interfaces/types in a separate file
// and then use them in any component by importing them. For
// example, to import the interface below do:
//
// import { User } from 'path/to/interfaces';
export type User = {
id: number;
name: string;
};

23
lib/findValidDateRange.ts Normal file
View File

@@ -0,0 +1,23 @@
import { startOfMonth, endOfMonth } from "date-fns";
interface ValidDateRange {
start: Date;
end: Date;
}
/**
* A function that will determine the valid date range for the navigation of the charts.
* @returns An object with a start and end key with the given date for the start and end of the range.
*/
const findValidDateRange = (): ValidDateRange => {
const currDate = new Date(); // Current date.
const startDate = startOfMonth(currDate); // Will eventually be the creation date of the account or the creation date the selected chart. Whichever is older.
const endDate = endOfMonth(currDate); // Always needs to be the last day on the current month within the current year.
return {
start: startDate,
end: endDate
};
};
export default findValidDateRange;

179
lib/populateMonth.ts Normal file
View File

@@ -0,0 +1,179 @@
import {
getDate,
endOfMonth,
format,
startOfMonth,
set,
isAfter,
isBefore,
subMonths,
addDays
} from "date-fns";
const weekDays: WeekDays = {
sunday: [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
],
monday: [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
]
};
/**
* Using date-fns, this function checks if currDate is within the month of selectedDate or not.
* @param {Date} selectedDate The current month.
* @param {Date} currDate The date to be compared to the selected month.
* @returns True if currDate is outside of the month of selectedDate, false if otherwise.
*/
const isOverflow = (
selectedDate: Date,
currDate: Date
): {
isOverflow: boolean;
overflowDirection: "prev" | "next" | null;
} => {
let flag = false;
let direction: "next" | "prev" | null = null;
const start = startOfMonth(selectedDate);
const end = endOfMonth(selectedDate);
if (isBefore(currDate, start)) {
flag = true;
direction = "prev";
}
if (isAfter(currDate, end)) {
flag = true;
direction = "next";
}
return { isOverflow: flag, overflowDirection: direction };
};
/**
* A function that will return a month layout when given a date. It produces
* an object with 6 weeks that include overflow from the previous and next month
* with all dates aligned with the day of the week.
* @param selectedDate The date of the month to generate a month layout for.
* @returns The month layout object for the provided month.
*/
const populateMonth = (selectedDate: Date): MonthLayout => {
const endLastMonth = getDate(endOfMonth(subMonths(selectedDate, 1)));
const startOfSelectedMonth = format(startOfMonth(selectedDate), "iii");
const ISOToIndex = {
sunday: {
Sun: 0,
Mon: 1,
Tue: 2,
Wed: 3,
Thu: 4,
Fri: 5,
Sat: 6
},
monday: {
Mon: -1,
Tue: 0,
Wed: 1,
Thu: 2,
Fri: 3,
Sat: 4,
Sun: 5
}
};
const sundays = {
week1: new Array(7).fill(null),
week2: new Array(7).fill(null),
week3: new Array(7).fill(null),
week4: new Array(7).fill(null),
week5: new Array(7).fill(null),
week6: new Array(7).fill(null)
};
// The date of the first day in the overflow
const sunStartDay =
endLastMonth - (ISOToIndex.sunday[startOfSelectedMonth] - 1);
let sunCurrDate = set(subMonths(selectedDate, 1), {
date: sunStartDay
});
for (const week in sundays) {
const thisWeek = sundays[week];
thisWeek.forEach((e, i) => {
const overflowInfo = isOverflow(selectedDate, sunCurrDate);
const day: MonthDay = {
...overflowInfo,
date: sunCurrDate.toJSON()
};
sunCurrDate = addDays(sunCurrDate, 1);
sundays[week][i] = day;
});
}
const mondays = {
week1: new Array(7).fill(null),
week2: new Array(7).fill(null),
week3: new Array(7).fill(null),
week4: new Array(7).fill(null),
week5: new Array(7).fill(null),
week6: new Array(7).fill(null)
};
// The date of the first day in the overflow
const monStartDay = endLastMonth - ISOToIndex.monday[startOfSelectedMonth];
let monCurrDate = set(subMonths(selectedDate, 1), {
date: monStartDay
});
for (const week in mondays) {
const thisWeek = mondays[week];
thisWeek.forEach((e, i) => {
const overflowInfo = isOverflow(selectedDate, monCurrDate);
const day: MonthDay = {
...overflowInfo,
date: monCurrDate.toJSON()
};
monCurrDate = addDays(monCurrDate, 1);
mondays[week][i] = day;
});
}
const output = {
sunday: {
weekdays: weekDays.sunday,
month: sundays
},
monday: {
weekdays: weekDays.monday,
month: mondays
}
};
return output;
};
export default populateMonth;

View File

@@ -0,0 +1,22 @@
/**
* Function to convert the version string to a number tha represents the most recent major release.
* @param {string }version The version string.
* @returns {number} a number that represents the most recent major release.
*/
const versionStringToNumber = (version: string): number => {
const versionStrArr: string[] = version.split(".");
const versionArr: number[] = versionStrArr.map((str) => parseInt(str));
if (versionArr[0] === 0 && versionArr[1] === 0 && versionArr[2] > 1) {
versionArr[1] = 1;
}
const versionStr = `${versionArr[0]}` + "." + `${versionArr[1]}`;
const versionNum: number = parseFloat(versionStr);
return versionNum;
};
export default versionStringToNumber;

View File

@@ -1,8 +1,8 @@
{ {
"private": true, "private": true,
"name": "lucid-creations-media-potty-chart", "name": "lucid-creations-website",
"homepage": "https://lucidcreations.media/introducing-code-name-potty-chart/", "homepage": "https://new.lucidcreations.media/",
"version": "v0.0.9.3-alpha", "version": "0.0.2",
"author": { "author": {
"name": "Lucid Creations Media", "name": "Lucid Creations Media",
"url": "https://lucidcreations.media", "url": "https://lucidcreations.media",
@@ -16,31 +16,34 @@
"pretty": "prettier --write ." "pretty": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@chakra-ui/react": "^2.0.2", "@chakra-ui/react": "^2.8.1",
"@emotion/react": "^11.9.0", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.8.1", "@emotion/styled": "^11.11.0",
"@iconify/react": "^3.2.1", "@iconify/react": "^4.1.1",
"@types/react": "^18.0.9", "@reduxjs/toolkit": "^1.9.7",
"date-fns": "^2.28.0", "date-fns": "^2.30.0",
"formik": "^2.2.9", "formik": "^2.4.5",
"framer-motion": "^6.3.3", "framer-motion": "^10.16.4",
"next": "12.1.6", "next": "14.0.1",
"react": "^18.1.0", "react": "^18.2.0",
"react-dom": "^18.1.0", "react-dom": "^18.2.0",
"sharp": "^0.30.4" "react-redux": "^8.1.3",
"sharp": "^0.32.6"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^17.0.35", "@types/node": "^20.8.10",
"@typescript-eslint/eslint-plugin": "^5.25.0", "@types/react": "^18.2.34",
"eslint": "^8.16.0", "@types/react-redux": "^7.1.28",
"eslint-config-next": "^12.1.6", "@typescript-eslint/eslint-plugin": "^6.9.1",
"eslint-config-prettier": "^8.5.0", "@typescript-eslint/parser": "^6.9.1",
"eslint-plugin-jsx-a11y": "^6.5.1", "eslint": "^8.52.0",
"eslint-plugin-react": "^7.30.0", "eslint-config-next": "<13.4.9",
"eslint-plugin-react-hooks": "^4.5.0", "eslint-config-prettier": "^9.0.0",
"prettier": "^2.6.2", "eslint-plugin-jsx-a11y": "^6.8.0",
"prisma": "^3.14.0", "eslint-plugin-react": "^7.33.2",
"typescript": "^4.6.4" "eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^3.0.3",
"typescript": "^5.2.2"
}, },
"packageManager": "yarn@3.2.1" "packageManager": "yarn@4.0.1"
} }

View File

@@ -1,19 +0,0 @@
import type { AppProps } from "next/app";
import Head from "next/head";
import React, { Fragment } from "react";
function LucidCreationWebsite({ Component, pageProps }: AppProps): JSX.Element {
return (
<Fragment>
<Head>
<meta charSet="utf-8" />
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<link rel="icon" href="/logo/favicon/favicon.ico" />
</Head>
<Component {...pageProps} />
</Fragment>
);
}
export default LucidCreationWebsite;

View File

@@ -1,25 +0,0 @@
import Document, { Html, Main, NextScript, Head } from "next/document";
import Footer from "../components/layout/Footer";
import React from "react";
class MyDocument extends Document {
render(): JSX.Element {
return (
<Html>
<Head>
<meta
name="description"
content="The new and improved Lucid Creations Media website."
/>
</Head>
<body>
<Main />
<NextScript />
</body>
<Footer />
</Html>
);
}
}
export default MyDocument;

View File

@@ -1,19 +0,0 @@
import Head from "next/head";
import React, { Fragment } from "react";
const IndexPage = (): JSX.Element => {
return (
<div>
<Head>
<title>Lucid Creations Media</title>
</Head>
<Fragment>
<div>
<span>Hello world!!</span>
</div>
</Fragment>
</div>
);
};
export default IndexPage;

View File

@@ -1,11 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

1
public/images/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 144.01 157.2"><defs><style>.cls-1{fill:#61a3d7;}</style></defs><path class="cls-1" d="M90.37,123A3.86,3.86,0,0,0,85,124.63q-7.68,13.26-15.31,26.55a11.53,11.53,0,0,0-.75,2,4.09,4.09,0,0,0,3,3.86,3.78,3.78,0,0,0,4.39-1.93q7.71-13.32,15.38-26.65A3.86,3.86,0,0,0,90.37,123Z"/><path class="cls-1" d="M116.26,123a3.84,3.84,0,0,0-5.34,1.59q-6.39,11-12.73,22.06a14.1,14.1,0,0,0-.8,2.11,4.26,4.26,0,0,0,2.93,3.76,3.86,3.86,0,0,0,4.54-1.89q6.4-11.07,12.79-22.16A3.84,3.84,0,0,0,116.26,123Z"/><path class="cls-1" d="M63.53,122.65a3.79,3.79,0,0,0-4.44,1.84q-6.48,11.17-12.89,22.37a4.49,4.49,0,0,0-.51,2.42,3.71,3.71,0,0,0,2.94,3.3,3.76,3.76,0,0,0,4.25-1.81q6.52-11.21,13-22.46a10,10,0,0,0,.62-1.82A4.18,4.18,0,0,0,63.53,122.65Z"/><path class="cls-1" d="M142.6,84.35C139,74,131.3,68.43,120.54,66.76c-.23-1.85-.35-3.69-.69-5.49A34.64,34.64,0,0,0,111.21,44l-5.49,5.48a27.13,27.13,0,0,1,7,20.22c-.19,3,1.76,4.82,4.68,4.78,7.72-.11,13.62,3.21,16.94,10.2a18,18,0,0,1-16.26,25.91c-10.16,0-20.32,0-30.48,0h-30a23,23,0,0,1-10.51-2.5l-5.68,5.68a30.08,30.08,0,0,0,16.13,4.63q30.14,0,60.28,0a29.86,29.86,0,0,0,4.81-.43C138.1,115.3,147.78,99.14,142.6,84.35Z"/><path class="cls-1" d="M139.83,3.83,136.54.54a1.82,1.82,0,0,0-2.59,0L.54,134a1.82,1.82,0,0,0,0,2.59l3.29,3.29a1.83,1.83,0,0,0,1.3.54,1.81,1.81,0,0,0,1.29-.54L139.83,6.42A1.82,1.82,0,0,0,139.83,3.83Z"/><path class="cls-1" d="M36.62,72.45A2.22,2.22,0,0,1,37.29,74c-.7,4.86-1.44,9.72-2.18,14.57L48.59,75.09h0l25.79-25.8h0L84,39.65a1.94,1.94,0,0,1-1.46-1.18C79.06,31.52,75.49,24.6,72,17.64c-1.34-2.68-3.35-4.34-6.35-4.35s-5,1.64-6.37,4.31c-3.5,7-7.08,14-10.59,20.94a1.84,1.84,0,0,1-1.52,1.14c-5.64.9-11.27,1.86-16.91,2.79-2.16.35-4.33.67-6.48,1a6.68,6.68,0,0,0-5.62,4.79A6.77,6.77,0,0,0,20,55.45Q28.33,63.94,36.62,72.45Z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

23
public/robots.txt Normal file
View File

@@ -0,0 +1,23 @@
# Used for many other (non-commercial) purposes as well
User-agent: CCBot
Disallow: /
# For new training only
User-agent: GPTBot
Disallow: /
# Not for training, only for user requests
User-agent: ChatGPT-User
Disallow: /
# Marker for disabling Bard and Vertex AI
User-agent: Google-Extended
Disallow: /
# Speech synthesis only?
User-agent: FacebookBot
Disallow: /
# Multi-purpose, commercial uses; including LLMs
User-agent: Omgilibot
Disallow: /

View File

@@ -0,0 +1,23 @@
import React from "react";
import { Box, Link, Button, BoxProps } from "@chakra-ui/react";
import { motion } from "framer-motion";
interface CustomButtonProps {
text: string;
link: string;
type: "primary" | "secondary" | "footer";
}
const MotionBox = motion<BoxProps>(Box);
const CustomButton = ({ text, link, type }: CustomButtonProps): JSX.Element => {
return (
<MotionBox whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
<Link href={link} target="_blank" rel="noopener">
<Button variant={type}>{text}</Button>
</Link>
</MotionBox>
);
};
export default CustomButton;

View File

@@ -0,0 +1,24 @@
import React from "react";
import { Box, Link, Button, BoxProps } from "@chakra-ui/react";
import { Icon } from "@iconify/react";
import { motion } from "framer-motion";
const MotionBox = motion<BoxProps>(Box);
const KoFi = (): JSX.Element => {
return (
<MotionBox whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
<Link
href="https://ko-fi.com/lucidcreationsmedia"
target="_blank"
rel="noopener"
>
<Button variant="kofi" leftIcon={<Icon icon="cib:ko-fi" />}>
{"Fund The App"}
</Button>
</Link>
</MotionBox>
);
};
export default KoFi;

View File

@@ -0,0 +1,30 @@
export interface LinkObj {
href?: string;
name?: string;
type: "primary" | "secondary" | "updates" | "ko-fi";
}
type Links = LinkObj[];
const links: Links = [
{
href: "https://docs.google.com/document/d/1hrerGKHTO3iach8A-CabtfIB4lyZWlgO8EGTyOCrI2Y",
name: "Roadmap and Progress",
type: "secondary"
},
{
href: "https://lucidcreations.media/lcm-potty-chart/",
name: "Official Announcement",
type: "secondary"
},
{
type: "ko-fi"
},
{
href: "https://t.me/LucidCreationsMedia",
name: "Dev Updates",
type: "secondary"
}
];
export default links;

View File

@@ -0,0 +1,68 @@
import React from "react";
import { Box, HStack, VStack } from "@chakra-ui/react";
import CustomButton from "./Custom";
import links, { LinkObj } from "./data/links";
import KoFi from "./KoFi";
const Buttons = (): JSX.Element => {
return (
<Box h="auto" w="100%">
<HStack
display={{ base: "none", lg: "flex" }}
h="auto"
w="100%"
justifyContent="center"
alignContent="center"
spacing={4}
>
{links.map((link: LinkObj) => {
const { href, name, type } = link;
if (type === "primary" || type === "secondary") {
return (
<CustomButton
key={name.replaceAll(" ", "-")}
link={href}
text={name}
type={type}
/>
);
}
if (type === "ko-fi") {
return <KoFi key={type} />;
}
})}
</HStack>
<VStack
display={{ base: "flex", lg: "none" }}
h="auto"
w="100%"
justifyContent="center"
alignContent="center"
spacing={4}
>
{links.map((link: LinkObj) => {
const { href, name, type } = link;
if (type === "primary" || type === "secondary") {
return (
<CustomButton
key={name.replaceAll(" ", "-")}
link={href}
text={name}
type={type}
/>
);
}
if (type === "ko-fi") {
return <KoFi key={type} />;
}
})}
</VStack>
</Box>
);
};
export default Buttons;

View File

@@ -0,0 +1,70 @@
import React from "react";
import { useAppSelector } from "../../redux/hooks";
import { useRouter } from "next/router";
import { HStack, IconButton } from "@chakra-ui/react";
import { Icon } from "@iconify/react";
import { format, isSameMonth, addMonths, subMonths } from "date-fns";
import findValidDateRange from "../../../lib/findValidDateRange";
import DatePicker from "./DatePicker";
interface CalenderNavProps {
isLoading: boolean;
title: string;
}
/**
* @param {boolean} isLoading is the component loading?
* @param {string} title the title for the current date.
*/
const CalenderNav = ({ title, isLoading }: CalenderNavProps): JSX.Element => {
const selectedDate = useAppSelector(
(state) => state.calender.selectedDateInfo
);
const { date } = selectedDate;
const selectedDateObj = new Date(date);
const validDateRange = findValidDateRange();
const { start: validStart, end: validEnd } = validDateRange;
const router = useRouter();
const handleNavButtons = (direction: "next" | "prev") => {
if (direction === "next") {
const newMonth = addMonths(selectedDateObj, 1);
const year = format(newMonth, "y");
const month = format(newMonth, "L");
router.push(`/calendar/${year}/${month}`);
} else if (direction === "prev") {
const newMonth = subMonths(selectedDateObj, 1);
const year = format(newMonth, "y");
const month = format(newMonth, "L");
router.push(`/calendar/${year}/${month}`);
}
};
return (
<HStack spacing={10} as="nav" w="auto" h="10vh" textAlign="center">
<IconButton
isDisabled={isSameMonth(selectedDateObj, validStart)}
aria-label="Previous Month"
icon={<Icon icon="akar-icons:chevron-left" />}
onClick={() => handleNavButtons("prev")}
/>
<DatePicker isLoading={isLoading} title={title} />
<IconButton
isDisabled={isSameMonth(selectedDateObj, validEnd)}
aria-label="Next Month"
icon={<Icon icon="akar-icons:chevron-right" />}
onClick={() => handleNavButtons("next")}
/>
</HStack>
);
};
export default CalenderNav;

View File

@@ -0,0 +1,284 @@
import React, { useRef, useState } from "react";
import { useRouter } from "next/router";
import {
Button,
FormControl,
FormErrorMessage,
FormLabel,
Heading,
HStack,
Input,
Popover,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverHeader,
PopoverTrigger,
Skeleton,
VStack
} from "@chakra-ui/react";
import {
Formik,
// FormikHelpers,
FormikProps,
Form,
Field,
FieldProps
} from "formik";
import { format } from "date-fns";
import findValidDateRange from "../../../lib/findValidDateRange";
import FormValidateEmoji from "./FormValidateEmoji";
interface DatePickerProps {
isLoading: boolean;
title: string;
}
/**
* @param {boolean} isLoading is the component loading?
* @param {string} title the title for the current date.
*/
const DatePicker = ({ title, isLoading }: DatePickerProps): JSX.Element => {
const router = useRouter();
const [valid, setValid] = useState<boolean>(false);
const validDateRange = findValidDateRange();
const validateDate = (
dateString?: string | undefined
): string | undefined => {
let dateError;
if (dateString) {
const dateArr = dateString.split("-");
if (dateArr.length !== 3) {
dateError = "Please select a date.";
setValid(false);
} else if (dateArr.length === 3) {
const date: UpdateCalenderPropsDateLayout = {
year: parseInt(dateArr[0]),
month: parseInt(dateArr[1]),
day: parseInt(dateArr[2])
};
if (!/^(19|20)\d{2}$/.test(`${date.year}`)) {
dateError = "Please use a year between 1900 and 2099";
setValid(false);
}
if (date.month < 1 || date.month > 12) {
dateError = "Please use a month between 1 and 12";
setValid(false);
}
if (date.day < 1 || date.day > 31) {
dateError = "Please use a day between 1 and 31";
setValid(false);
}
setValid(true);
} else {
setValid(true);
}
} else if (dateString.length === 0) {
dateError = "Please select a date.";
setValid(false);
} else {
setValid(true);
}
return dateError;
};
const handleSubmit = (formInput?: { date?: string }): Promise<unknown> => {
return new Promise((resolve, reject) => {
if (formInput.date) {
if (!validateDate(formInput.date)) {
const dateArr = formInput.date.split("-");
const date: UpdateCalenderPropsDateLayout = {
year: parseInt(dateArr[0]),
month: parseInt(dateArr[1]),
day: parseInt(dateArr[2])
};
return resolve(router.push(`/calendar/${date.year}/${date.month}`));
} else {
return reject("Error validating date.");
}
} else {
return reject("Date not provided.");
}
});
};
// Field theme
const fieldTheme = {
width: "auto",
bg: "gray.900",
borderColor: "white",
_placeholder: {
color: "white"
},
_focus: {
bg: "#000",
color: "#FFF",
borderColor: "#63b3ed",
boxShadow: "0 0 0 1px #63b3ed",
zIndex: "1"
}
};
const initRef = useRef();
return (
<Popover placement="bottom" initialFocusRef={initRef}>
<PopoverTrigger>
<Button border="none" variant="outline">
{isLoading ? (
<Skeleton>
<Heading w="100%" h="auto">
{title}
</Heading>
</Skeleton>
) : (
<Heading w="100%" h="auto">
{title}
</Heading>
)}
</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverHeader py={2} fontWeight="semibold">
<Heading size="md" as="h3">
{"Choose a Date"}
</Heading>
</PopoverHeader>
<PopoverCloseButton />
<PopoverBody textAlign="center">
<Formik
initialValues={{
date: ""
}}
onSubmit={(data, actions) => {
handleSubmit(data)
.then(() => {
actions.setSubmitting(false);
actions.resetForm({
values: {
date: ""
}
});
})
.catch(() => {
actions.setSubmitting(false);
});
}}
>
{(
formProps: FormikProps<{
date: string;
}>
) => (
<Form
style={{
width: "100%",
height: "auto"
}}
>
<VStack
alignItems="center"
alignContent="flex-start"
w="100%"
h="auto"
spacing={6}
py={4}
>
<Heading as="h4" size="sm" fontWeight="semibold">
{"Required fields indicated with"}
<FormValidateEmoji type="Required" />
</Heading>
<Field name="date" validate={validateDate}>
{({ field, form }: FieldProps) => (
<FormControl
isInvalid={
form.errors.date && form.touched.date ? true : false
}
>
<VStack
alignContent="center"
alignItems="center"
spacing={2}
w="100%"
h="auto"
>
<HStack
alignContent="center"
alignItems="center"
pl={4}
w="100%"
h="auto"
spacing={2}
>
<FormLabel fontWeight="semibold" htmlFor="date">
{"Date:"}
</FormLabel>
<Input
required
{...fieldTheme}
type="date"
isDisabled={formProps.isSubmitting}
{...field}
id="date"
textAlign="center"
min={format(validDateRange.start, "yyyy-MM-dd")}
max={format(validDateRange.end, "yyyy-MM-dd")}
{...(!form.errors.date && form.touched.date
? {
borderColor: "brand.valid",
boxShadow: "0 0 0 1px #00c17c",
_hover: {
borderColor: "brand.valid",
boxShadow: "0 0 0 1px #00c17c"
}
}
: "")}
/>
{!form.touched.date && (
<FormValidateEmoji type="Required" />
)}
{form.errors.name && form.touched.date && (
<FormValidateEmoji type="Error" />
)}
{!form.errors.name && form.touched.date && (
<FormValidateEmoji type="Valid" />
)}
</HStack>
<FormErrorMessage>
{typeof form.errors.date === "string" &&
form.errors.date}
</FormErrorMessage>
</VStack>
</FormControl>
)}
</Field>
<Button
isDisabled={!valid}
background={valid ? "brand.valid" : "brand.danger"}
isLoading={formProps.isSubmitting}
type="submit"
>
{"Select this date"}
</Button>
</VStack>
</Form>
)}
</Formik>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default DatePicker;

View File

@@ -0,0 +1,262 @@
import React, { useState } from "react";
import { Provider } from "react-redux";
import { store } from "../../redux/store";
import { Box, Skeleton, VStack } from "@chakra-ui/react";
import {
add,
getYear,
getMonth,
sub,
getDate,
isBefore,
endOfDay,
isToday as isTodayFun
} from "date-fns";
import router from "next/router";
import AddUpdateSticker from "./modals/AddUpdateSticker";
import DemoStickers from "./stickers/DemoStickers";
interface DayProps {
isLoading: boolean;
isOverflow?: boolean;
overflowDirection?: "next" | "prev" | null;
currSticker: StickerVal;
date: string;
selectedDate: string;
currDate: Date;
tutorial?: "add" | "edit";
}
/**
* The individual days in the calender component.
* @param {boolean} isLoading is the component loading?
* @param {boolean} isOverflow is the current date being given before or after the current month.
* @param {"next" | "prev" | null} overflowDirection the direction the overflow is. This will navigate the calender forward or backwards 1 month.
* @param {StickerVal} currSticker the sticker for this date.
* @param {date} date the date for this day.
* @param {date} selectedDate the date for the selected month.
* @param {Date} currDate today's date.
*/
const Day = ({
isLoading,
isOverflow,
overflowDirection,
currSticker,
date,
selectedDate,
currDate,
tutorial
}: DayProps): JSX.Element => {
const selectedDateObj = new Date(selectedDate);
const currDateObj = new Date(date);
const isToday = isTodayFun(currDateObj);
const handleNav = (direction: "next" | "prev") => {
if (direction === "next") {
console.log(overflowDirection);
const newMonth = add(selectedDateObj, { months: 1 });
const year = getYear(newMonth);
const month = getMonth(newMonth) + 1;
router.push(`/calendar/${year}/${month}`);
} else if (direction === "prev") {
const newMonth = sub(selectedDateObj, { months: 1 });
const year = getYear(newMonth);
const month = getMonth(newMonth) + 1;
router.push(`/calendar/${year}/${month}`);
}
};
// This handles the modal for the day.
const [isOpen, setIsOpen] = useState<boolean>(false);
// The step the modal is at.
const [step, setStep] = useState<number>(0);
// The current selected sticker. (To be added or updated)
const [selectedSticker, setSelectedSticker] = useState<StickerVal>(null);
/**
* TODO: Add logic to remove the onClick within overflow dates.
* Do not give dates for the next month an onClick.
* Do not give dates in the past an onClick there is nothing before that month.
* (Creation date of a chart)
*/
// TODO: When the valid date range is created, disallow pointer cursor outside of the date range.
return isOverflow ? (
<VStack
w="100%"
h="100%"
bg="transparent"
pt={2}
color="gray.600"
border="1px solid #181d8f"
_hover={{
cursor: isBefore(currDateObj, endOfDay(currDate))
? selectedSticker !== null
? "pointer"
: "default"
: "default",
background: "gray.700",
border: "1px solid #FFF",
color: "whiteAlpha.900"
}}
onClick={() =>
selectedSticker !== null ? handleNav(overflowDirection) : ""
}
spacing="0.5rem"
alignContent="center"
justifyContent="flex-start"
>
<Box w="1.8rem" h="1.8rem" textAlign="center" p={0} m={0}>
{`${getDate(currDateObj)}`}
</Box>
{isLoading ? (
<Skeleton key={currSticker}>
<Box fontSize="1.5rem">
<DemoStickers stickerVal={0} />
</Box>
</Skeleton>
) : (
<Box key={currSticker} fontSize="1.5rem">
<DemoStickers stickerVal={currSticker} />
</Box>
)}
</VStack>
) : (
<VStack
w="100%"
h="100%"
bg={
tutorial
? tutorial === "add" && isToday
? "gray.600"
: tutorial === "edit" &&
!isToday &&
isBefore(currDateObj, endOfDay(currDate))
? "gray.600"
: "transparent"
: "transparent"
}
border={
tutorial
? tutorial === "add" && isToday
? "1px solid #00ff3c"
: tutorial === "edit" &&
!isToday &&
isBefore(currDateObj, endOfDay(currDate))
? "1px solid #00ff3c"
: "1px solid #0068ff"
: "1px solid #0068ff"
}
onClick={() => {
setStep(0);
setSelectedSticker(null);
setIsOpen(true);
}}
alignContent="center"
justifyContent="flex-start"
pt={2}
_hover={{
cursor: isBefore(currDateObj, endOfDay(currDate))
? "pointer"
: "default",
bg: tutorial
? tutorial === "add" && isToday
? "gray.600"
: tutorial === "edit" &&
!isToday &&
isBefore(currDateObj, endOfDay(currDate))
? "gray.600"
: "transparent"
: "transparent",
border: "1px solid #FFF"
}}
>
{isToday ? (
<Box
border="1px solid #0068ff"
borderRadius="50%"
w="1.8rem"
h="1.8rem"
textAlign="center"
p={0}
m={0}
>
{`${getDate(currDateObj)}`}
</Box>
) : (
<Box w="1.8rem" h="1.8rem" textAlign="center" p={0} m={0}>
{`${getDate(currDateObj)}`}
</Box>
)}
{isLoading ? (
<Skeleton key={currSticker}>
<Box fontSize="1.5rem">
<DemoStickers stickerVal={0} />
</Box>
</Skeleton>
) : (
<Box key={currSticker} fontSize="1.5rem">
<DemoStickers stickerVal={currSticker} />
</Box>
)}
{tutorial ? (
<Provider store={store}>
{tutorial.toLowerCase() === "add" && isToday && !isLoading && (
<AddUpdateSticker
stickerDate={date}
isOpen={isOpen}
updateIsOpen={setIsOpen}
currSticker={currSticker}
step={step}
updateStep={setStep}
selectedSticker={selectedSticker}
updateSelectedSticker={setSelectedSticker}
currDate={currDate}
/>
)}
{tutorial.toLowerCase() === "edit" &&
!isToday &&
isBefore(currDateObj, endOfDay(currDate)) &&
!isLoading && (
<AddUpdateSticker
stickerDate={date}
isOpen={isOpen}
updateIsOpen={setIsOpen}
currSticker={currSticker}
step={step}
updateStep={setStep}
selectedSticker={selectedSticker}
updateSelectedSticker={setSelectedSticker}
currDate={currDate}
/>
)}
</Provider>
) : (
<Provider store={store}>
{isBefore(currDateObj, endOfDay(currDate)) && !isLoading && (
<AddUpdateSticker
stickerDate={date}
isOpen={isOpen}
updateIsOpen={setIsOpen}
currSticker={currSticker}
step={step}
updateStep={setStep}
selectedSticker={selectedSticker}
updateSelectedSticker={setSelectedSticker}
currDate={currDate}
/>
)}
</Provider>
)}
</VStack>
);
};
export default Day;

View File

@@ -0,0 +1,35 @@
import React, { FC } from "react";
interface FormValidateEmojiProps {
type: string;
}
const FormValidateEmoji: FC<FormValidateEmojiProps> = ({
type
}: FormValidateEmojiProps) => {
interface Validations {
[key: string]: JSX.Element;
}
const validations: Validations = {
Required: (
<span role="img" aria-label="Explication Mark">
</span>
),
Error: (
<span role="img" aria-label="X">
</span>
),
Valid: (
<span role="img" aria-label="Check">
</span>
)
};
return validations[`${type}`];
};
export default FormValidateEmoji;

View File

@@ -0,0 +1,166 @@
import React, { useEffect } from "react";
import { useAppDispatch, useAppSelector } from "../../redux/hooks";
import { updateCurrDate, updateMonth } from "../../features/calender";
import { Box, HStack, SimpleGrid, Text, VStack } from "@chakra-ui/react";
import { isSameDay, format } from "date-fns";
import CalenderNav from "./CalenderNav";
import Day from "./Day";
const Calender = ({
date: newDate,
isLoading
}: UpdateCalendarProps): JSX.Element => {
const dispatch = useAppDispatch();
// * Month * //
const currDate: string = useAppSelector((state) => state.calender.currDate);
const selectedDate: SelectedDateInfo = useAppSelector(
(state) => state.calender.selectedDateInfo
);
const { layout, title, date: currentSelectedDateStr } = selectedDate;
const currDateObj = new Date(currDate);
// * Stickers * //
const stickersMonth: StickerDays = useAppSelector(
(state) => state.stickers.stickersMonth
);
useEffect(() => {
if (newDate && newDate.year && newDate.month && newDate.day) {
const { year, month, day } = newDate;
if (year > 0 && month > 0 && day > 0) {
const generatedDate: Date = new Date(year, month - 1, day);
const currSelectedDateObj = new Date(currentSelectedDateStr);
const dateString: string = generatedDate.toJSON();
if (!isSameDay(currSelectedDateObj, generatedDate)) {
dispatch(updateMonth(dateString));
}
} else {
console.warn("Invalid date format: ", newDate);
}
}
}, [currentSelectedDateStr, dispatch, newDate]);
useEffect(() => {
// console.info("Check to update date.");
const currDateObj = new Date(currDate);
if (!isSameDay(currDateObj, new Date())) {
// console.info("Updated date.");
dispatch(updateCurrDate());
}
}, [currDate, dispatch]);
// Simulated user settings.
const userSettings = {
theme: "default",
startOfWeek: "Sunday"
};
const currMonth: WeekLayout =
layout[`${userSettings.startOfWeek.toLowerCase()}`];
const { month, weekdays } = currMonth;
// TODO: Move the weekdays into it's own component for responsiveness.
return (
<VStack h="92vh" w="100%" mb="5vh">
<CalenderNav title={title} isLoading={isLoading} />
<VStack h="100%" w="100%" spacing={0}>
<HStack
w="100%"
h="auto"
px={{ base: 1, sm: 2, md: 6 }}
spacing={0}
alignContent="center"
alignItems="center"
>
{weekdays.map((weekDay) => {
return (
<Box
key={weekDay}
display="flex"
w="100%"
h={10}
bg="transparent"
border="1px solid #0068ff"
alignContent="center"
alignItems="center"
>
<Text display={{ base: "none", md: "block" }} w="100%" h="auto">
{weekDay}
</Text>
<Text
display={{ base: "none", sm: "block", md: "none" }}
w="100%"
h="auto"
>
{weekDay.substring(0, 3)}
</Text>
<Text display={{ base: "block", sm: "none" }} w="100%" h="auto">
{weekDay.substring(0, 2)}
</Text>
</Box>
);
})}
</HStack>
<SimpleGrid
w="100%"
h="100%"
px={{ base: 1, sm: 2, md: 6 }}
columns={7}
alignItems="center"
>
{Object.keys(month).map((week) => {
const thisWeek = month[week];
return thisWeek.map((day: MonthDay) => {
const { date, isOverflow, overflowDirection } = day;
const toDateObj: Date = new Date(date);
let sticker = null;
let id = "";
stickersMonth.map((stickerDay) => {
const { date: stickerDate } = stickerDay;
if (isSameDay(new Date(stickerDate), toDateObj)) {
sticker = stickerDay.sticker;
id = stickerDay.id;
}
});
return (
<Day
isLoading={isLoading}
isOverflow={isOverflow}
overflowDirection={overflowDirection}
currSticker={sticker}
date={date}
selectedDate={selectedDate.date}
currDate={currDateObj}
key={
id.length
? id
: format(toDateObj, "yyyyddLL") +
`/${sticker === null ? 0 : sticker}`
}
/>
);
});
})}
</SimpleGrid>
</VStack>
</VStack>
);
};
export default Calender;

View File

@@ -0,0 +1,271 @@
import React, { useState, useRef } from "react";
import { useAppDispatch } from "../../../redux/hooks";
import { addEditSticker } from "../../../features/calender/stickers";
import {
Button,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Heading,
HStack,
Text,
VStack,
SimpleGrid,
Box
} from "@chakra-ui/react";
import { format, isSameDay } from "date-fns";
import { Icon } from "@iconify/react";
import StickerSelector from "./StickerSelector";
import DemoStickers from "../stickers/DemoStickers";
interface AddStickerProps {
isOpen: boolean;
updateIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
stickerDate: string;
currSticker: StickerVal;
step: number;
updateStep: React.Dispatch<React.SetStateAction<number>>;
selectedSticker: StickerVal;
updateSelectedSticker: React.Dispatch<React.SetStateAction<StickerVal>>;
currDate: Date;
}
/**
* Handles adding and modifying the stickers for the given month.
* @param {boolean} isOpen Tells the component when the modal should be open.
* @param {React.Dispatch<React.SetStateAction<boolean>>} updateIsOpen Used to close the modal.
* @param {date} stickerDate The date for which the sticker will be added or modified.
* @param {StickerVal} currSticker The current sticker for the date.
* @param {number} step A numerical variable that represents the page the modal should be at.
* @param {React.Dispatch<React.SetStateAction<number>>} updateStep Used to navigate the pages of the modal by updating the step the modal is on.
* @param {StickerVal} selectedSticker the value of the selected sticker.
* @param {React.Dispatch<React.SetStateAction<StickerVal>>} updateSelectedSticker The react state function to update the selected sticker that will be added or updated.
* @param {Date} currDate the current date.
*/
const AddUpdateSticker = ({
isOpen,
updateIsOpen,
stickerDate,
currSticker,
step,
updateStep,
selectedSticker,
updateSelectedSticker,
currDate
}: AddStickerProps): JSX.Element => {
const dispatch = useAppDispatch();
const stickerDateObj = new Date(stickerDate);
const [modalVariant] = useState<"add" | "edit">(
isSameDay(stickerDateObj, currDate) ? "add" : "edit"
);
const handleClose = () => {
updateIsOpen(false);
};
// TODO: Validate that the provided sticker is not the current sticker. Throw an error if the same sticker is attempted.
const handleSubmit = (sticker: StickerVal) => {
dispatch(addEditSticker({ stickerDate, sticker }));
handleClose();
};
// The first sticker to have focus when the modal opens.
const initialRef = useRef();
// * Double check that the submit button is disabled if the selected sticker is the same as the current sticker.
const variants = {
add: [
{
header: `Which sticker did you earn for ${format(
stickerDateObj,
"LLL d, y"
)}?`,
body: (
<VStack
w="100%"
h="auto"
justifyContent="space-between"
alignContent="center"
spacing="4"
>
<Heading textAlign="center" as="h3" size="md" w="100%" h="auto">
{"Select a sticker"}
</Heading>
<StickerSelector
stickerSet="Demo"
currSticker={currSticker}
selectedSticker={selectedSticker}
updateSelectedSticker={updateSelectedSticker}
initialSticker={initialRef}
/>
</VStack>
),
footer: (
<Button
variant="submit"
isDisabled={
selectedSticker === null || selectedSticker === currSticker
}
onClick={() => handleSubmit(selectedSticker)}
>
{"Submit"}
</Button>
)
}
],
edit: [
{
header: `Which sticker did you want to update for ${format(
stickerDateObj,
"LLL d, y"
)}?`,
body: (
<VStack
w="100%"
h="auto"
justifyContent="space-between"
alignContent="center"
>
<Heading textAlign="center" as="h3" size="md" w="100%" h="auto">
{"Current Sticker"}
</Heading>
<Text fontSize="4rem">
<DemoStickers stickerVal={currSticker} />
</Text>
<Heading textAlign="center" as="h3" size="md" w="100%" h="auto">
{"Select your new sticker"}
</Heading>
<StickerSelector
stickerSet="Demo"
currSticker={currSticker}
selectedSticker={selectedSticker}
updateSelectedSticker={updateSelectedSticker}
initialSticker={initialRef}
/>
</VStack>
),
footer: (
<Button
variant="primary"
isDisabled={
selectedSticker === null || selectedSticker === currSticker
}
onClick={() => updateStep(step + 1)}
>
{"Next"}
</Button>
)
},
{
header: `Are you sure you want to change the sticker for ${format(
stickerDateObj,
"M/d/y"
)}?`,
body: (
<SimpleGrid
my={{ base: "0px", sm: "6" }}
mx={{ base: "0px", sm: "10", md: "16" }}
w="auto"
h="100%"
columns={3}
>
<Heading textAlign="center" as="h3" size="md" w="100%" h="auto">
{"Previous Sticker"}
</Heading>
<Box></Box>
<Heading textAlign="center" as="h3" size="md" w="100%" h="auto">
{"New Sticker"}
</Heading>
<Text textAlign="center" w="100%" fontSize="4rem">
<DemoStickers stickerVal={currSticker} />
</Text>
<Box fontSize="4rem" m="auto">
<Icon fontSize="4rem" icon="bi:arrow-right" />
</Box>
<Text textAlign="center" w="100%" fontSize="4rem">
<DemoStickers stickerVal={selectedSticker} />
</Text>
</SimpleGrid>
),
footer: (
<HStack
w="100%"
h="auto"
justifyContent="space-between"
alignContent="center"
>
<Button variant="primary" onClick={() => updateStep(step - 1)}>
{"Previous"}
</Button>
<HStack w="auto" h="auto" alignContent="center" spacing={6}>
<Button
backgroundColor="transparent"
_hover={{ backgroundColor: "brand.danger" }}
onClick={() => updateIsOpen(!isOpen)}
>
{"Cancel"}
</Button>
<Button
variant="submit"
isDisabled={
selectedSticker === null || selectedSticker === currSticker
}
onClick={() => handleSubmit(selectedSticker)}
>
{"Confirm"}
</Button>
</HStack>
</HStack>
)
}
]
};
return (
<Modal
isCentered
initialFocusRef={initialRef}
isOpen={isOpen}
onClose={() => handleClose()}
motionPreset="slideInBottom"
scrollBehavior="inside"
size={modalVariant === "add" ? "xl" : "2xl"}
>
<ModalOverlay />
<ModalContent>
<ModalHeader>
<HStack
w="100%"
h="auto"
justifyContent="space-between"
alignContent="center"
>
<Heading textAlign="center" as="h2" size="md" w="100%" h="auto">
{modalVariant && variants[modalVariant][step].header}
</Heading>
<Button
fontSize="2rem"
px="1"
onClick={() => updateIsOpen(!isOpen)}
>
<Icon icon="bi:x" />
</Button>
</HStack>
</ModalHeader>
<ModalBody>
{modalVariant && variants[modalVariant][step].body}
</ModalBody>
<ModalFooter>
{modalVariant && variants[modalVariant][step].footer}
</ModalFooter>
</ModalContent>
</Modal>
);
};
export default AddUpdateSticker;

View File

@@ -0,0 +1,74 @@
import { HStack, Button } from "@chakra-ui/react";
import React from "react";
import DemoStickers from "../stickers/DemoStickers";
interface StickerSelectorProps {
stickerSet: "Demo";
currSticker: StickerVal;
selectedSticker: StickerVal;
updateSelectedSticker: React.Dispatch<React.SetStateAction<StickerVal>>;
initialSticker: React.MutableRefObject<undefined>;
}
/**
* Handles displaying a list of dynamic stickers to be selected.
* @param {string} stickerSet The name of the stickers that should be displayed.
* @param {StickerVal} currSticker The current sticker for the date.
* @param {StickerVal} selectedSticker The selected sticker for the current. date
* @param {React.Dispatch<React.SetStateAction<StickerVal>>} updateSelectedSticker TThe react state function to update the selected sticker that will be added or updated.
* @param {React.MutableRefObject<undefined>} initialSticker the sticker that should have be in focus when the modal opens.
*/
const StickerSelector = ({
stickerSet,
currSticker,
selectedSticker,
updateSelectedSticker,
initialSticker
}: StickerSelectorProps): JSX.Element => {
const stickers = {
Demo: (
<HStack
w="100%"
h="auto"
justifyContent="center"
alignContent="center"
spacing={14}
>
<Button
isDisabled={currSticker >= 1}
ref={currSticker <= 1 ? initialSticker : null}
border={selectedSticker === 1 ? "1px solid #FFF" : "opx"}
bg={selectedSticker === 1 && "gray.800"}
onClick={() => updateSelectedSticker(1)}
variant="stickerButton"
>
<DemoStickers stickerVal={1} />
</Button>
<Button
isDisabled={currSticker === 0}
ref={currSticker >= 1 ? initialSticker : null}
border={selectedSticker === 0 ? "1px solid #FFF" : "opx"}
bg={selectedSticker === 0 && "gray.800"}
onClick={() => updateSelectedSticker(0)}
variant="stickerButton"
>
<DemoStickers stickerVal={0} />
</Button>
<Button
isDisabled={currSticker <= -1}
border={selectedSticker === -1 ? "1px solid #FFF" : "opx"}
bg={selectedSticker === -1 && "gray.800"}
onClick={() => updateSelectedSticker(-1)}
variant="stickerButton"
>
<DemoStickers stickerVal={-1} />
</Button>
</HStack>
)
};
return stickers[stickerSet];
};
export default StickerSelector;

View File

@@ -0,0 +1,56 @@
import React, { FC } from "react";
// TODO: When themes are made import the theme from user settings store. Refactor to use whatever those SVGs are.
interface DemoStickersProps {
stickerVal: StickerVal;
}
const DemoStickers: FC<DemoStickersProps> = ({
stickerVal
}: DemoStickersProps) => {
// If sticker is null return an empty space.
if (stickerVal === null) {
return <span aria-label="spacer">&nbsp;</span>;
}
interface StickerToEmoji {
[key: string]: JSX.Element;
}
/**
* ? Temporarily using values -1 to 1.
* ? In the full app the values will be between -2 and 2.
*/
let key = "0";
if (stickerVal > 0) {
key = "1";
} else if (stickerVal < 0) {
key = "-1";
}
// Link value to an emoji representing a sticker.
const stickerToEmoji: StickerToEmoji = {
"1": (
<span role="img" aria-label="Sun">
</span>
),
"0": (
<span role="img" aria-label="Cloud">
</span>
),
"-1": (
<span role="img" aria-label="Raining Cloud">
🌧
</span>
)
};
// Return the appropriate sticker.
return stickerToEmoji[`${key}`];
};
export default DemoStickers;

View File

@@ -0,0 +1,37 @@
import React from "react";
import {
Box,
Modal,
ModalBody,
ModalContent,
ModalOverlay
} from "@chakra-ui/react";
import LoadingSpinner from "./LoadingSpinner";
const LoadingOverlay = (): JSX.Element => {
return (
<Modal
isCentered
isOpen
onClose={() => null}
motionPreset="slideInBottom"
scrollBehavior="inside"
size="xs"
>
<ModalOverlay bg="loading.overlayBg" />
<ModalContent bg="transparent" boxShadow="none">
{/* <ModalHeader>
</ModalHeader> */}
<ModalBody border="0px">
<Box h="100%" w="100%" textAlign="center">
<LoadingSpinner />
</Box>
</ModalBody>
{/* <ModalFooter>
</ModalFooter> */}
</ModalContent>
</Modal>
);
};
export default LoadingOverlay;

View File

@@ -0,0 +1,16 @@
import React from "react";
import { Spinner } from "@chakra-ui/react";
const LoadingSpinner = (): JSX.Element => {
return (
<Spinner
thickness="4px"
speed="0.50s"
emptyColor="loading.spinnerEmptySpace"
color="loading.spinnerColor"
size="xl"
/>
);
};
export default LoadingSpinner;

View File

@@ -0,0 +1,210 @@
import React, { useEffect } from "react";
import { useAppDispatch, useAppSelector } from "../../redux/hooks";
import { updateMonth } from "../../features/calender";
import { Box, HStack, SimpleGrid, Text, VStack } from "@chakra-ui/react";
import { format, isSameDay, isToday } from "date-fns";
import Day from "../calender/Day";
import { setCurrentWeek } from "../../features/tutorial";
interface CalenderExampleProps {
type: "add" | "edit";
isLoading: boolean;
}
const CalenderExample = ({
type,
isLoading
}: CalenderExampleProps): JSX.Element => {
// TODO: Check if the current date is the start of the user's preferred start of the week and use the previous week for the edit example.
const currDateStr: string = useAppSelector(
(state) => state.calender.currDate
);
const currDateObj: Date = new Date(currDateStr);
const dispatch = useAppDispatch();
// * Current Month * //
const selectedDate: SelectedDateInfo = useAppSelector(
(state) => state.calender.selectedDateInfo
);
const { layout, date: currSelectedDateStr } = selectedDate;
// * Stickers * //
const stickersMonth: StickerDays = useAppSelector(
(state) => state.stickers.stickersMonth
);
// Simulated user settings.
const userSettings = {
theme: "default",
startOfWeek: "Sunday"
};
// * Week Names * //
const currMonth: WeekLayout =
layout[`${userSettings.startOfWeek.toLowerCase()}`];
const { month, weekdays } = currMonth;
useEffect(() => {
const currDateObj: Date = new Date(currDateStr);
const currSelectedDateOj: Date = new Date(currSelectedDateStr);
if (!isSameDay(currDateObj, currSelectedDateOj)) {
dispatch(updateMonth(currDateObj.toJSON()));
}
}, [currDateStr, currSelectedDateStr, dispatch]);
// * The current week * //
const currWeek = useAppSelector((state) => state.tutorial.currWeek);
useEffect(() => {
const getCurrentWeek = (): MonthDay[] => {
let foundWeek: MonthDay[];
for (const week in month) {
const currWeek = month[week];
currWeek.forEach((day: MonthDay) => {
const { date } = day;
if (isToday(new Date(date))) {
foundWeek = currWeek;
}
});
}
return foundWeek || ([] as MonthDay[]);
};
if (currWeek === null) {
dispatch(setCurrentWeek(getCurrentWeek()));
}
}, [currWeek, dispatch, month]);
return (
<VStack
h="auto"
w="100%"
alignContent="center"
alignItems="center"
spacing={2}
>
<VStack
h="8.5rem"
w="100%"
alignContent="center"
alignItems="center"
spacing={0}
>
<HStack
w="100%"
h="auto"
alignContent="center"
alignItems="center"
spacing={0}
px={{ base: 1, sm: 2, md: 6 }}
>
{weekdays.map((weekDay) => {
return (
<Box
key={weekDay}
display="flex"
w="100%"
h={10}
bg="transparent"
border="1px solid #0068ff"
alignContent="center"
alignItems="center"
>
<Text display={{ base: "none", md: "block" }} w="100%" h="auto">
{weekDay}
</Text>
<Text
display={{ base: "none", sm: "block", md: "none" }}
w="100%"
h="auto"
>
{weekDay.substring(0, 3)}
</Text>
<Text display={{ base: "block", sm: "none" }} w="100%" h="auto">
{weekDay.substring(0, 2)}
</Text>
</Box>
);
})}
</HStack>
<SimpleGrid
w="100%"
h="100%"
columns={7}
px={{ base: 1, sm: 2, md: 6 }}
alignItems="center"
>
{currWeek &&
currWeek.map((day: MonthDay) => {
const { date, isOverflow, overflowDirection } = day;
const toDateObj: Date = new Date(date);
let sticker = null;
let id = "";
stickersMonth.map((stickerDay) => {
const { date: stickerDate } = stickerDay;
if (isSameDay(new Date(stickerDate), toDateObj)) {
sticker = stickerDay.sticker;
id = stickerDay.id;
}
});
return (
<Day
isLoading={isLoading}
isOverflow={isOverflow}
overflowDirection={overflowDirection}
currSticker={sticker}
date={date}
selectedDate={selectedDate.date}
currDate={currDateObj}
tutorial={type}
key={
id.length
? id
: format(toDateObj, "yyyyddLL") +
`/${sticker === null ? 0 : sticker}`
}
/>
);
})}
</SimpleGrid>
</VStack>
{type === "edit" && (
<VStack
w="100%"
h="auto"
alignContent="center"
alignItems="center"
spacing={2}
>
<Text fontSize="sm" color="whiteAlpha.800">
{
"Not being able to edit within this tutorial when the current date is the start of the week or month is a known bug."
}
</Text>
<Text fontSize="sm" color="whiteAlpha.800">
{"This bug will be fixed in beta v2."}
</Text>
<Text fontSize="sm" color="whiteAlpha.800">
{"You can skip the tutorial and try again tomorrow."}
</Text>
</VStack>
)}
</VStack>
);
};
export default CalenderExample;

View File

@@ -0,0 +1,10 @@
type AboutApp = string[];
const aboutApp: AboutApp = [
"The Potty Chart is an app that mimics a potty/star chart commonly used while potty training toddler or child.",
"The app can be used to track behavior, habits, diaper training, potty training (good luck), daily chores/tasks, or anything else you might want to track in a fun and visual way with colorful themes, stickers, and even receive encouraging messaged from your big/dom, followers, and friends.",
"The final app will have settings to disable any mentions and references of ABDL to allow a more general audience to use, such as for a master and pet relationship.",
"This is a beta build of the app. Some functionality may not work as intended, is not fully functional, and may be missing entirely."
];
export default aboutApp;

View File

@@ -0,0 +1,9 @@
type AppFunctionality = string[];
const appFunctionality: AppFunctionality = [
"The app will generate stickers to display from the 1st of the month to the day before today. This is to simulate previous and continued use.",
"Ability to add a sticker to the current date.",
"Ability to add edit a sticker from a previous date with a confirmation prompt."
];
export default appFunctionality;

View File

@@ -0,0 +1,38 @@
import React from "react";
import { VStack } from "@chakra-ui/react";
import TutorialCalender from "./sections/TutorialCalender";
import TutorialLinks from "./sections/TutorialLinks";
import TutorialHeading from "./sections/TutorialHeading";
import TutorialAboutApp from "./sections/TutorialAboutApp";
import TutorialSubmitButtons from "./sections/TutorialSubmitButtons";
import TutorialAppFunctionality from "./sections/TutorialAppFunctionality";
interface TutorialProps {
isLoading: boolean;
}
const Tutorial = ({ isLoading }: TutorialProps): JSX.Element => {
return (
<VStack
h="auto"
w="auto"
justifyContent="center"
alignContent="center"
my={8}
mx={{ base: 0, sm: 2, md: 4 }}
py={4}
px={{ base: 0, sm: 2, md: 4 }}
bg="gray.700"
borderRadius={{ base: "", sm: "2xl" }}
>
<TutorialHeading />
<TutorialAboutApp />
<TutorialAppFunctionality />
<TutorialCalender isLoading={isLoading} />
<TutorialLinks />
<TutorialSubmitButtons isLoading={isLoading} />
</VStack>
);
};
export default Tutorial;

View File

@@ -0,0 +1,33 @@
import React from "react";
import { VStack, Heading, Divider, Text } from "@chakra-ui/react";
import aboutApp from "../data/aboutApp";
const TutorialAboutApp = (): JSX.Element => {
return (
<VStack
h="auto"
w="100%"
justifyContent="center"
alignContent="center"
spacing={4}
>
<Heading as="h3" size="lg">
{"About the App"}
</Heading>
<VStack
h="auto"
w="100%"
justifyContent="start"
alignContent="center"
spacing={1}
>
{aboutApp.map((string: string) => {
return <Text key={string.replaceAll(" ", "-")}>{string}</Text>;
})}
</VStack>
<Divider orientation="horizontal" />
</VStack>
);
};
export default TutorialAboutApp;

View File

@@ -0,0 +1,33 @@
import React from "react";
import { VStack, Heading, Divider, Text } from "@chakra-ui/react";
import appFunctionality from "../data/appFunctionality";
const TutorialAppFunctionality = (): JSX.Element => {
return (
<VStack
h="auto"
w="100%"
justifyContent="center"
alignContent="center"
spacing={4}
>
<Heading as="h3" size="lg">
{"App Functionality"}
</Heading>
<VStack
h="auto"
w="100%"
justifyContent="start"
alignContent="center"
spacing={1}
>
{appFunctionality.map((string: string) => {
return <Text key={string.replaceAll(" ", "-")}>{string}</Text>;
})}
</VStack>
<Divider orientation="horizontal" />
</VStack>
);
};
export default TutorialAppFunctionality;

View File

@@ -0,0 +1,74 @@
import React from "react";
import { Divider, Heading, HStack, Text, VStack } from "@chakra-ui/react";
import CalenderExample from "../CalenderExample";
interface CalenderExampleProps {
isLoading: boolean;
}
const TutorialCalender = ({ isLoading }: CalenderExampleProps): JSX.Element => {
return (
<VStack
h="auto"
w="100%"
justifyContent="center"
alignContent="center"
spacing={4}
>
<Heading as="h3" size="lg">
{"How to Use The Calender"}
</Heading>
<VStack
h="auto"
w="100%"
justifyContent="center"
alignItems="center"
alignContent="center"
spacing={4}
>
<Heading as="h4" size="md">
{"Add a Sticker to Today's Date"}
</Heading>
<HStack
w="100%"
h="auto"
alignContent="center"
justifyContent="center"
spacing={1}
>
<Text>{"Select the date with the"}</Text>
<Text color="#00ff3c">{" green "}</Text>
<Text>{"border."}</Text>
</HStack>
<CalenderExample type={"add"} isLoading={isLoading} />
</VStack>
<VStack
h="auto"
w="100%"
justifyContent="center"
alignItems="center"
alignContent="center"
spacing={4}
>
<Heading as="h4" size="md">
{"Add a Sticker to Previous Dates"}
</Heading>
<HStack
w="100%"
h="auto"
alignContent="center"
justifyContent="center"
spacing={1}
>
<Text>{"Select a date with a"}</Text>
<Text color="#00ff3c">{" green "}</Text>
<Text>{"border."}</Text>
</HStack>
<CalenderExample type={"edit"} isLoading={isLoading} />
</VStack>
<Divider orientation="horizontal" />
</VStack>
);
};
export default TutorialCalender;

View File

@@ -0,0 +1,22 @@
import React from "react";
import { VStack, Heading, Divider } from "@chakra-ui/react";
const TutorialHeading = (): JSX.Element => {
return (
<VStack
h="auto"
w="100%"
justifyContent="center"
alignContent="center"
spacing={4}
>
<Heading as="h2">{"Welcome to Code Name: LCM Potty Chart"}</Heading>
<Heading as="h3" size="md">
{"A Lucid Creations Media Project"}
</Heading>
<Divider orientation="horizontal" />
</VStack>
);
};
export default TutorialHeading;

View File

@@ -0,0 +1,23 @@
import React from "react";
import { Divider, Heading, VStack } from "@chakra-ui/react";
import Buttons from "../../buttons";
const TutorialLinks = (): JSX.Element => {
return (
<VStack
h="auto"
w="100%"
justifyContent="center"
alignContent="center"
spacing={4}
>
<Heading as="h3" size="lg">
{"More Info"}
</Heading>
<Buttons />
<Divider orientation="horizontal" />
</VStack>
);
};
export default TutorialLinks;

View File

@@ -0,0 +1,83 @@
import { HStack, Button, VStack, Checkbox } from "@chakra-ui/react";
import React from "react";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import {
setTutorialCompleted,
setTempTutorialComplete,
toggleRememberCompleted
} from "../../../features/tutorial";
interface TutorialSubmitButtonsProps {
isLoading: boolean;
}
const TutorialSubmitButtons = ({
isLoading
}: TutorialSubmitButtonsProps): JSX.Element => {
const rememberComplete: boolean = useAppSelector(
(state) => state.tutorial.rememberCompleted
);
const dispatch = useAppDispatch();
const handleComplete = (): void => {
if (rememberComplete) {
dispatch(setTutorialCompleted());
}
if (!rememberComplete) {
dispatch(setTempTutorialComplete());
}
};
const handleSkip = (): void => {
dispatch(setTempTutorialComplete());
};
const handleUpdateCheck = (): void => {
dispatch(toggleRememberCompleted());
};
return (
<HStack
h="auto"
w="90%"
justifyContent="space-between"
alignItems="flex-start"
pt={8}
>
<Button
type="button"
isDisabled={isLoading}
onClick={() => handleSkip()}
variant="skip"
>
{"Skip"}
</Button>
<VStack
h="auto"
w="auto"
justifyContent="center"
alignItems="center"
spacing={2}
>
<Button
type="button"
isDisabled={isLoading}
onClick={() => handleComplete()}
variant="primary"
>
{"Complete Tutorial"}
</Button>
<Checkbox
isChecked={rememberComplete}
isDisabled={isLoading}
onChange={() => handleUpdateCheck()}
>
{"Remember completed?"}
</Checkbox>
</VStack>
</HStack>
);
};
export default TutorialSubmitButtons;

View File

@@ -0,0 +1,66 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { format } from "date-fns";
import populate from "../../../lib/populateMonth";
interface CalenderSlice {
currDate: string;
selectedDateInfo: SelectedDateInfo;
isLoading: boolean;
}
const getCurrDate = (): Date => new Date();
const dateParse = (date: Date) => date.toJSON();
const dateFormatter = (date: Date): string => format(date, "LLLL uuuu");
const initialState: CalenderSlice = {
currDate: dateParse(getCurrDate()),
selectedDateInfo: {
date: dateParse(getCurrDate()),
title: dateFormatter(getCurrDate()),
layout: populate(getCurrDate())
},
isLoading: true
};
// TODO: Add a function that validated if a month has at least one sticker in it. Use that within the nav function (when filter is enabled).
// TODO: Add a function that will give the closest date, if available, when the nav func detects an empty month.
// Use the chart creation date to aid with this. (When filter is enabled)
/**
* TODO: Add logic that prevents navigation to the future and too far in the past. (Use chart creation date)
* Update to use a promise and return appropriate errors. Display those errors on the front end.
* Update the use of this function on the front to handle the fails of the promise.
*/
// TODO: (When filter is enabled) Update the calender update function that will take in a direction so that the the navigation buttons will take the user to the next month with stickers. Assuming there was a gap with empty months.
const calenderSlice = createSlice({
name: "Calender",
initialState,
reducers: {
// Update month info
updateMonth(state: CalenderSlice, action: PayloadAction<string>) {
const { payload } = action;
const toDateObj: Date = new Date(payload);
state.selectedDateInfo.date = payload;
state.selectedDateInfo.title = dateFormatter(toDateObj);
state.selectedDateInfo.layout = populate(toDateObj);
},
// Update current date
updateCurrDate(state: CalenderSlice) {
state.currDate = dateParse(new Date());
},
// Update isLoading
updateLoading(state: CalenderSlice, action: PayloadAction<boolean>) {
const { payload } = action;
state.isLoading = payload;
}
}
});
export const { updateMonth, updateCurrDate, updateLoading } =
calenderSlice.actions;
export default calenderSlice.reducer;

View File

@@ -0,0 +1,62 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { format, getDate, isSameDay } from "date-fns";
import stickersSeeder from "../../../data/stickerSeeder";
interface StickersSlice {
stickersMonth: StickerDays;
}
interface UpdateStickerSlicePayload {
stickerDate: string;
sticker: StickerVal;
}
const initialState: StickersSlice = {
stickersMonth: stickersSeeder()
};
const stickersSlice = createSlice({
name: "Stickers",
initialState,
reducers: {
addEditSticker(
state: StickersSlice,
actions: PayloadAction<UpdateStickerSlicePayload>
) {
const { stickerDate, sticker } = actions.payload;
const dateObj = new Date(stickerDate);
// Getting index for the stickers array, sticker from the stickers array, and the date from the sticker.
const index: number = getDate(dateObj) - 1;
const currSticker: Sticker = state.stickersMonth[index];
// Updating the edited status by checking if the sticker date is today's date.
const edited = currSticker.edited
? true
: isSameDay(new Date(stickerDate), new Date())
? false
: true;
currSticker.edited = edited;
// TODO: Add manually added here.
// Updating the id of the sticker.
const id = format(dateObj, "yyyyddLL") + sticker;
// Updating the information of the sticker.
const newSticker: Sticker = {
id: id,
date: stickerDate,
sticker: sticker,
edited: edited,
manual: false
};
state.stickersMonth[index] = newSticker;
}
}
});
export const { addEditSticker } = stickersSlice.actions;
export default stickersSlice.reducer;

View File

@@ -0,0 +1,140 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { addMonths, endOfDay } from "date-fns";
import versionStringToNumber from "../../../lib/versionStringToNumber";
export interface StorageState {
exp: string;
version: number;
completed: boolean;
}
const endOfToday: Date = endOfDay(new Date());
const generateExpDate = (): string => {
return endOfDay(addMonths(endOfToday, 1)).toJSON();
};
const generateVersion = (): number => {
const versionStr: string = process.env.NEXT_PUBLIC_APP_VERSION;
return versionStringToNumber(versionStr);
};
// * Storage Helpers * //
const setTempStorage = (storageState: StorageState): void => {
sessionStorage.setItem("completedTutorial", JSON.stringify(storageState));
};
const getTempStorage = (): StorageState | null => {
return JSON.parse(sessionStorage.getItem("completedTutorial"));
};
const clearTempStorage = (): void => {
sessionStorage.removeItem("completedTutorial");
};
const setStorage = (storageState: StorageState): void => {
localStorage.setItem("completedTutorial", JSON.stringify(storageState));
};
const getStorage = (): StorageState | null => {
return JSON.parse(localStorage.getItem("completedTutorial"));
};
const clearStorage = (): void => {
localStorage.removeItem("completedTutorial");
};
interface TutorialSlice {
completedTutorial: boolean | null;
storageState: StorageState | null;
rememberCompleted: boolean;
currWeek: MonthDay[] | null;
}
const initialState: TutorialSlice = {
completedTutorial: null,
storageState: null,
rememberCompleted: false,
currWeek: null
};
const tutorialSlice = createSlice({
name: "Tutorial",
initialState,
reducers: {
// Set temp complete
setTempTutorialComplete(state: TutorialSlice) {
const exp: string = generateExpDate();
const version: number = generateVersion();
const storageState: StorageState = {
exp,
version,
completed: true
};
setTempStorage(storageState);
state.storageState = storageState;
state.completedTutorial = true;
},
// Set completed (remember)
setTutorialCompleted(state: TutorialSlice) {
const exp: string = generateExpDate();
const version: number = generateVersion();
const storageState: StorageState = {
exp,
version,
completed: true
};
setStorage(storageState);
state.storageState = storageState;
state.completedTutorial = true;
},
// Clear states and storages
clearTutorialCompleted(state: TutorialSlice) {
clearTempStorage();
clearStorage();
state.storageState = null;
state.completedTutorial = null;
},
// Get and set states
getAndSetTutorial(state: TutorialSlice) {
const temp = getTempStorage();
const local = getStorage();
if (temp !== null || local !== null) {
state.storageState = temp !== null ? temp : local;
state.completedTutorial =
temp !== null ? temp.completed : local.completed;
}
if (temp === null && local === null) {
state.completedTutorial = false;
}
},
// Toggle remember completed
toggleRememberCompleted(state: TutorialSlice) {
const { rememberCompleted } = state;
state.rememberCompleted = !rememberCompleted;
},
// Set current week
setCurrentWeek(state: TutorialSlice, action: PayloadAction<MonthDay[]>) {
const { payload } = action;
state.currWeek = payload;
}
}
});
export const {
setTempTutorialComplete,
setTutorialCompleted,
clearTutorialCompleted,
getAndSetTutorial,
toggleRememberCompleted,
setCurrentWeek
} = tutorialSlice.actions;
export default tutorialSlice.reducer;

31
src/pages/_app.tsx Normal file
View File

@@ -0,0 +1,31 @@
import type { AppProps } from "next/app";
import React from "react";
import { ChakraProvider } from "@chakra-ui/react";
import AppTheme from "../theme/AppTheme";
import { Provider } from "react-redux";
import { store } from "../redux/store";
import Layout from "../theme/layout/Layout";
import Head from "next/head";
function LCMPottyChart({ Component, pageProps }: AppProps): JSX.Element {
return (
<React.StrictMode>
<ChakraProvider theme={AppTheme}>
<Layout {...pageProps}>
<Head>
<title>{"LCM Potty Chart"}</title>
<meta
name="viewport"
content="width=device-width, user-scalable=yes, initial-scale=1.0"
/>
</Head>
<Provider store={store}>
<Component {...pageProps} />
</Provider>
</Layout>
</ChakraProvider>
</React.StrictMode>
);
}
export default LCMPottyChart;

61
src/pages/_document.tsx Normal file
View File

@@ -0,0 +1,61 @@
import NextDocument, { Html, Head, Main, NextScript } from "next/document";
import React from "react";
import { ColorModeScript } from "@chakra-ui/react";
import AppTheme from "../theme/AppTheme";
const description =
// "Behavior and progress tracker for ABDLs and babyfurs alike. Track multiple littles and create any trackers you would like.";
"Beta preview of a, calender like, 'star chart' behavior and progress tracker for ABDLs, diaperfurs, and babyfurs.";
const logo = "images/logo.svg";
const logoOG = "/images/logo.png";
class Document extends NextDocument {
render(): JSX.Element {
return (
<Html>
<Head>
<meta name="theme-color" content="#3138dc" />
<link rel="icon" href={logo} sizes="32x32 192x192" />
<link rel="apple-touch-icon" href={logo} />
<meta property="og:title" content="LCM Potty Chart" />
<meta name="og:description" content={description} />
<meta property="og:type" content="Progress Tracking" />
<meta property="og:image" content={logoOG} />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:alt" content="LCM Potty Chart Logo" />
<meta property="og:url" content="https://lucidcreations.media" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="title" content="LCM Potty Chart" />
<meta name="description" content={description} />
<meta property="type" content="Progress Tracking" />
<meta property="image" content={logoOG} />
<meta property="image:type" content="image/png" />
<meta property="image:alt" content="LCM Potty Chart Logo" />
<meta property="url" content="https://https://lucidcreations.media" />
<meta httpEquiv="content-language" content="en_US" />
<meta charSet="UTF-8" />
<meta
name="keywords"
content="ABDL Adult Baby Diaper Lover Furry Babyfur ab/dl AB/DL potty chart training progress behavior tracker habbit"
/>
<meta name="copyright" content="Lucid Creations Media" />
<meta name="page-topic" content="Progress Tracking" />
<meta name="page-type" content="Calender" />
<meta name="audience" content="18+" />
<meta name="robots" content="index, follow" />
</Head>
<html lang="en" />
<body>
<ColorModeScript
initialColorMode={AppTheme.config.initialColorMode}
/>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default Document;

View File

@@ -0,0 +1,209 @@
import React, { useEffect, useState } from "react";
import { Provider } from "react-redux";
import { store } from "../../redux/store";
import { Box } from "@chakra-ui/react";
import { useRouter } from "next/router";
import {
endOfMonth,
getDate
// getMonth,
// getYear,
// isAfter,
// isBefore,
// isSameMonth
} from "date-fns";
// import findValidDateRange from "../../lib/findValidDateRange";
import ErrorPage from "next/error";
import Calender from "../../components/calender";
const DateRoute: React.FC<unknown> = () => {
const router = useRouter();
const { date: slug } = router.query;
const [date, setDate] = useState<UpdateCalenderPropsDateLayout | null>(null);
const [error, setError] = useState<boolean>(false);
// const dateRange = useRef(findValidDateRange());
// const validDateRange = Object.assign({}, dateRange.current);
const validateDateInput = (
dateArr: number[]
): UpdateCalenderPropsDateLayout => {
if (!(dateArr.length >= 2) && !(dateArr.length <= 3)) {
return {
year: 0,
month: 0,
day: 0
};
}
const date = {
year: 0,
month: 0,
day: 0
};
if (/^(19|20)\d{2}$/.test(`${dateArr[0]}`)) {
date.year = dateArr[0];
}
if (dateArr[1] > 0 && dateArr[1] <= 12) {
date.month = dateArr[1];
}
if (date.month && date.year) {
const lastDay = getDate(
endOfMonth(new Date(date.year, date.month - 1, 1))
);
if (dateArr[2] && dateArr[2] > 0 && dateArr[2] <= lastDay) {
date.day = dateArr[2];
} else if (!dateArr[2]) {
date.day = 1;
}
}
return date;
};
/**
* ! This function does not work as is. It is causing infinite loops whe used within the useEffect.
*/
// const validateDateRange = (
// slugDate: Date
// ): [Date, "after" | "before" | "valid"] => {
// const { start: validStart, end: validEnd } = validDateRange;
// // Check if the slug date is beyond the valid end date.
// if (isAfter(slugDate, validEnd)) {
// // router.push("/calender/now");
// console.warn(
// "Slug date is after the valid date range for this calendar!!!"
// );
// return [validEnd, "after"];
// // Check if the slug is before the valid start date.
// } else if (isBefore(slugDate, validStart)) {
// console.warn(
// "Slug date is before the valid date range for this calendar!!!"
// );
// return [validStart, "before"];
// // router.push(`/${getYear(validStart)}/${getMonth(validStart) + 1}`);
// } else {
// console.info(
// "Slug date is within the valid date range for this calendar."
// );
// return [slugDate, "valid"];
// }
// };
useEffect(() => {
// Checking if the slug exists and is an array.
if (slug && Array.isArray(slug)) {
console.log(slug);
// Grabbing the slug length
const length = slug.length;
// Parsing the slug to convert it from strings to numbers.
const parsedSlug = slug.map((e) => {
return parseInt(e);
});
// Checking if the slug has 2 to 3 numbers within the array. year/month/day.
if (length >= 2 && length <= 3) {
// Validate that the date is valid.
const newDate = validateDateInput(parsedSlug);
// If anything is invalid the year/day/month would be set to 0. This checks for the invalid condition.
if (newDate.year === 0 || newDate.month === 0 || newDate.day === 0) {
setError(true);
// Set the date to the valid date.
} else {
// TODO: Make sure the date is within the valid range using the validateDateRange function.
// const validDate = new Date(
// newDate.year,
// newDate.month - 1,
// newDate.day
// );
// const validDateWithinRange = validateDateRange(validDate)[0];
// setDate({
// ...{
// year: getYear(validDateWithinRange),
// month: getMonth(validDateWithinRange) + 1,
// day: getDate(validDateWithinRange)
// }
// });
setDate({
...newDate
});
}
} else if (length === 1) {
// Checking if the slug is not "now".
// ! Update this to include a check for "today".
if (slug[0] !== "now") {
setError(true);
return console.warn("improper date input:", slug);
}
}
}
}, [slug]);
/**
* ? Pushing into the router within the use effect does not create the infinite loop.
* ? The way the validate date range or the way it is being used within a useEffect is what is creating the infinite loop.
*/
// useEffect(() => {
// // Check is slug and date are valid.
// if (slug && date && date !== null) {
// // Check if the slug is an array and has a length of 2.
// if (Array.isArray(slug) && slug.length === 2) {
// const dateState = new Date(date.year, date.month - 1, date.day);
// const parsedSlug = slug.map((e) => {
// return parseInt(e);
// });
// const slugDate = new Date(parsedSlug[0], parsedSlug[1] - 1, 1);
// if (!isSameMonth(dateState, slugDate)) {
// const validDateWithinRange = validateDateRange(dateState);
// if (validDateRange[1] === "after") {
// router.push("/now");
// } else {
// router.push(
// `/${getYear(validDateWithinRange[0])}/${getMonth(
// validDateWithinRange[0]
// )}`
// );
// }
// }
// }
// }
// }, [date]);
if (router.isFallback) {
return <ErrorPage statusCode={404} />;
}
/**
* TODO: Update to disallow navigation in the future and too far in the past.
* Update so that a date given in the future take the user to /now to today's date.
* Update so that a date given beyond the last valid date will bring the user to the
* last month that has stickers within it (When filter is enabled) or to the creation date of the chart..
*/
return error ? (
<ErrorPage statusCode={404} />
) : (
<Box textAlign="center" w="100%" h="auto" pt="50px" pb="10vh">
<Provider store={store}>
<Calender date={date} isLoading={false} />
</Provider>
</Box>
);
};
export default DateRoute;

View File

@@ -0,0 +1,23 @@
import React, { useEffect } from "react";
import { useRouter } from "next/router";
import { Box, Heading } from "@chakra-ui/react";
const DateIndex = () => {
const router = useRouter();
useEffect(() => {
if (router) {
router.push("calendar/now");
}
}, [router]);
return (
<Box textAlign="center" w="100%" h="auto" pt="50px" pb="10vh">
<Heading as="h2" size="xl">
Loading
</Heading>
</Box>
);
};
export default DateIndex;

133
src/pages/index.tsx Normal file
View File

@@ -0,0 +1,133 @@
import React, { Fragment, useEffect, useRef } from "react";
import { Provider } from "react-redux";
import { store } from "../redux/store";
import { useAppDispatch, useAppSelector } from "../redux/hooks";
import { updateLoading } from "../features/calender";
import {
clearTutorialCompleted,
getAndSetTutorial,
StorageState
} from "../features/tutorial";
import { Box } from "@chakra-ui/react";
import { format, isAfter, isBefore, startOfDay } from "date-fns";
import Calender from "../components/calender";
import Tutorial from "../components/tutorial";
import LoadingOverlay from "../components/loading/LoadingOverlay";
import versionStringToNumber from "../../lib/versionStringToNumber";
const IndexPage = (): JSX.Element => {
const currDateStr: string = useAppSelector(
(state) => state.calender.currDate
);
const isLoading: boolean = useAppSelector(
(state) => state.calender.isLoading
);
const currDateObj: Date = new Date(currDateStr);
// * Tutorial * //
const completedTutorial: boolean = useAppSelector(
(state) => state.tutorial.completedTutorial
);
const tutorialCompletionInfo: StorageState = useAppSelector(
(state) => state.tutorial.storageState
);
const dispatch = useAppDispatch();
// Get the completed tutorial cookie or have it set to false.
useEffect(() => {
if (completedTutorial === null && tutorialCompletionInfo === null) {
dispatch(getAndSetTutorial());
}
if (completedTutorial !== null) {
dispatch(updateLoading(false));
}
}, [completedTutorial, dispatch, tutorialCompletionInfo]);
// Checking the exp date of completed tutorial cookie and if the version completed is out of date.
useEffect(() => {
if (tutorialCompletionInfo !== null) {
const { exp, version } = tutorialCompletionInfo;
const currDateObj: Date = new Date(currDateStr);
/**
* Checks if the completed tutorial cookie is expired.
* @param {Date} expDate the date when the completed tutorital cookie expires.
* @returns {boolean} true if the cookie is expired, false is otherwise.
*/
const expDateValidator = (expDate: Date): boolean => {
let flag = false;
const startOfToday = startOfDay(currDateObj);
if (isAfter(startOfToday, expDate)) {
flag = true;
}
return flag;
};
/**
* Checks if the last time the completed tutorial is before an update to the tutorial.
* @param {number} lastVersionCompleted the version number the tutorial was last completed.
* @returns {boolean} true if the version given is before the changes to the tutorial, false otherwise.
*/
const versionValidator = (lastVersionCompleted: number): boolean => {
const lastVersionWithChangeStr: string =
process.env.NEXT_PUBLIC_NEW_TUTORIAL_VERSION;
const lastVersionWithChange: number = versionStringToNumber(
lastVersionWithChangeStr
);
const lastUpdatedDateStr: string =
process.env.NEXT_PUBLIC_LAST_UPDATE_DATE;
const lastUpdatedDate: Date = new Date(lastUpdatedDateStr);
let flag = false;
if (
lastVersionCompleted < lastVersionWithChange ||
(lastVersionCompleted === lastVersionWithChange &&
isBefore(currDateObj, lastUpdatedDate))
) {
flag = true;
console.error("Completed cookie version is out of date.");
}
return flag;
};
if (expDateValidator(new Date(exp)) || versionValidator(version)) {
console.warn("Version outdated or cookie expired.");
dispatch(clearTutorialCompleted());
}
}
}, [currDateStr, dispatch, tutorialCompletionInfo]);
// Current date
const currDate = useRef<UpdateCalenderPropsDateLayout>({
year: parseInt(format(currDateObj, "y")),
month: parseInt(format(currDateObj, "M")),
day: parseInt(format(currDateObj, "d"))
});
return (
<Box textAlign="center" w="100%" h="auto" pt="50px" minWidth="min-content">
<Provider store={store}>
{isLoading === true ? (
<Fragment>
<LoadingOverlay />
<Calender date={currDate.current} isLoading={isLoading} />
</Fragment>
) : completedTutorial ? (
<Calender date={currDate.current} isLoading={isLoading} />
) : (
<Tutorial isLoading={isLoading} />
)}
</Provider>
</Box>
);
};
export default IndexPage;

5
src/redux/hooks.ts Normal file
View File

@@ -0,0 +1,5 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { RootState, AppDispatch } from "./store";
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

15
src/redux/store.ts Normal file
View File

@@ -0,0 +1,15 @@
import { configureStore } from "@reduxjs/toolkit";
import calenderReducer from "../features/calender";
import stickersReducer from "../features/calender/stickers";
import tutorialReducer from "../features/tutorial";
export const store = configureStore({
reducer: {
calender: calenderReducer,
stickers: stickersReducer,
tutorial: tutorialReducer
}
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;

54
src/theme/AppTheme.ts Normal file
View File

@@ -0,0 +1,54 @@
import { extendTheme, ThemeConfig } from "@chakra-ui/react";
// import { createBreakpoints } from "@chakra-ui/theme-tools";
import buttons from "./components/buttonStyles";
const config: ThemeConfig = {
initialColorMode: "dark",
useSystemColorMode: false
};
// const breakpoints = createBreakpoints({
// sm: "30em",
// md: "48em",
// lg: "75em",
// xl: "85em",
// "2xl": "100em",
// });
const AppTheme = extendTheme({
config,
colors: {
brand: {
main: "#3138dc",
primary: "#0068ff",
secondary: "#0086ff",
hover: "#00aec1",
warning: "#ffbd48",
danger: "#FC8181",
valid: "#00c17c",
footer: "#0097a7",
footerText: "black",
content: "#2d3748",
kofi: "#FF5E5B",
twitter: "#1da1f2"
},
loading: {
overlayBg: "#171923cb",
spinnerColor: "#0088ff",
spinnerEmptySpace: "#2D374860"
}
},
styles: {
global: {
body: {
bg: "gray.900"
}
}
},
components: {
Button: buttons
}
// breakpoints,
});
export default AppTheme;

View File

@@ -0,0 +1,177 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { darken, mode, whiten } from "@chakra-ui/theme-tools";
import { Dict } from "@chakra-ui/utils";
const buttonStyles = {
// style object for base or default style
baseStyle: {},
// styles for different sizes ("sm", "md", "lg")
sizes: {},
// styles for different visual variants ("outline", "solid")
variants: {
primary: (props: Dict<never>) => ({
bg: "brand.primary",
fontSize: "xl",
py: 3,
px: 4,
color: "whiteAlpha",
_hover: {
bg: mode(
whiten("brand.primary", 20),
darken("brand.primary", 20)
)(props)
}
}),
secondary: (props: Dict<never>) => ({
bg: "brand.secondary",
fontSize: "xl",
py: 3,
px: 4,
color: "whiteAlpha",
_hover: {
bg: mode(
whiten("brand.secondary", 20),
darken("brand.secondary", 20)
)(props)
}
}),
skip: (props: Dict<never>) => ({
bg: "transparent",
fontSize: "xl",
py: 3,
px: 4,
color: "whiteAlpha.800",
_hover: {
bg: mode(whiten("brand.danger", 20), darken("brand.danger", 20))(props),
color: "whiteAlpha.900"
}
}),
stickerButton: (props: Dict<never>) => ({
bg: "transparent",
fontSize: "4rem",
px: 2,
py: 14,
_hover: {
bg: mode(
whiten("brand.secondary", 20),
darken("brand.secondary", 20)
)(props)
}
}),
nav: (props: Dict<never>) => ({
bg: "transparent",
fontSize: "md",
px: 2,
_hover: {
bg: mode(
whiten("brand.secondary", 20),
darken("brand.secondary", 20)
)(props)
}
}),
stickyNav: (/* props: Dict<never> | StyleFunctionProps */) => ({
bg: "transparent",
fontSize: "md",
px: 2,
_hover: {
textDecoration: "underline"
}
}),
footer: (props: Dict<never>) => ({
bg: "brand.main",
fontSize: "lg",
py: 3,
px: 4,
color: "whiteAlpha",
_hover: {
bg: mode(whiten("brand.main", 20), darken("brand.main", 20))(props)
}
}),
backToTop: (props: Dict<never>) => ({
bg: "rgba(23, 25, 35, 0.5)",
fontSize: "lg",
py: 2,
px: 4,
color: "rgba(0, 134, 255, 0.6)",
boxShadow:
"rgba(0, 134, 255, 0.05) 0px 0px 15px, rgba(0, 134, 255, 0.1) 0px 0px 3px 1px",
border: "1px solid rgba(0, 134, 255, 0.15)",
_hover: {
bg: mode(
whiten("brand.secondary", 20),
darken("brand.secondary", 20)
)(props),
boxShadow:
"rgba(0, 104, 255, 0.5) 0px 0px 15px, rgba(0, 104, 255, 0.3) 0px 0px 3px 1px",
color: "whiteAlpha.900",
border: "1px solid rgba(0, 134, 255, 1)"
}
}),
submit: (props: Dict<never>) => ({
fontSize: "lg",
py: 2,
px: 4,
type: "submit",
_hover: {
color: "whiteAlpha.900",
bg: mode(whiten("brand.valid", 20), darken("brand.valid", 20))(props),
_disabled: {
color: mode(
whiten("brand.danger", 20),
darken("brand.danger", 20)
)(props),
boxShadow:
"rgba(252, 129, 129, .95) 0px 0px 15px, rgba(252, 129, 129, 0.75) 0px 0px 3px 1px",
border: "1px solid #FC8181"
}
}
}),
mobileNav: (props: Dict<never>) => ({
// bg: "transparent",
fontSize: "md",
px: 2,
boxShadow:
"rgba(0, 134, 255, 0.30) 0px 0px 15px, rgba(0, 134, 255, 0.15) 0px 0px 3px 1px",
_hover: {
bg: mode(
whiten("brand.secondary", 20),
darken("brand.secondary", 20)
)(props),
boxShadow:
"rgba(0, 134, 255, 0.5) 0px 0px 15px, rgba(0, 134, 255, 0.3) 0px 0px 3px 1px"
},
_expanded: {
bg: "brand.primary",
boxShadow:
"rgba(0, 134, 255, 0.5) 0px 0px 15px, rgba(0, 134, 255, 0.3) 0px 0px 3px 1px",
border: "1px solid #0068ff"
}
}),
kofi: (props: Dict<never>) => ({
bg: "brand.kofi",
fontSize: "lg",
p: 3,
color: "whiteAlpha",
_hover: {
bg: mode(whiten("brand.kofi", 20), darken("brand.kofi", 20))(props)
}
}),
twitter: (props: Dict<never>) => ({
bg: "brand.twitter",
fontSize: "lg",
py: 3,
px: 4,
color: "whiteAlpha",
_hover: {
bg: mode(
whiten("brand.twitter", 20),
darken("brand.twitter", 20)
)(props)
}
})
},
// default values for `size` and `variant`
defaultProps: {}
};
export default buttonStyles;

View File

@@ -0,0 +1,32 @@
import React, { FC } from "react";
import { Button, Flex, Link } from "@chakra-ui/react";
import { Icon } from "@iconify/react";
interface BackToTopButtonProps {
show: boolean;
}
const BackToTopButton: FC<BackToTopButtonProps> = ({
show
}: BackToTopButtonProps) => {
return (
<Flex
display={show ? "flex" : "none"}
pos="fixed"
top="85vh"
right={{
base: "1.25rem",
sm: "2rem",
md: "3rem"
}}
>
<Link href="/#top">
<Button variant="backToTop">
<Icon icon="akar-icons:chevron-up" />
</Button>
</Link>
</Flex>
);
};
export default BackToTopButton;

View File

@@ -0,0 +1,29 @@
import React from "react";
import { Button, HStack, Link } from "@chakra-ui/react";
import navItems, { NavItem } from "./navItems";
const DesktopNav = (): JSX.Element => {
return (
<HStack
as="nav"
display={{ base: "none", lg: "flex" }}
h="auto"
w="auto"
spacing={4}
// m="auto"
justifyContent="center"
alignContent="center"
alignItems="center"
>
{navItems.map((navItem: NavItem) => {
return (
<Link id={"dekstop-" + navItem[0]} key={navItem[0]} href={navItem[1]}>
<Button variant="nav">{navItem[0]}</Button>
</Link>
);
})}
</HStack>
);
};
export default DesktopNav;

View File

@@ -0,0 +1,37 @@
import React /*, { useEffect, useRef, useState }*/ from "react";
import { Box, Text, VStack, Link } from "@chakra-ui/react";
// import BackToTopButton from "./BackToTopButton";
import Buttons from "../../components/buttons";
const Footer = (): JSX.Element => {
return (
<Box bg="brand.footer" as="footer" w="100%" h="auto">
{/* <BackToTopButton show={showBackToTop} /> */}
<VStack
h="auto"
w="auto"
py={12}
spacing={5}
justifyItems="center"
justifyContent="center"
>
<VStack spacing={4}>
<Buttons />
<Text color="brand.footerText" fontSize="xs">
&copy;
{` 2021 - ${new Date().getFullYear()} `}
<Link
href="https://lucidcreations.media"
rel="noopener"
target="_blank"
>
{"Lucid Creations Media"}
</Link>
</Text>
</VStack>
</VStack>
</Box>
);
};
export default Footer;

189
src/theme/layout/Header.tsx Normal file
View File

@@ -0,0 +1,189 @@
import React, { useEffect, useRef, useState } from "react";
import Image from "next/image";
import {
Heading,
HStack,
Box,
IconButton,
Menu,
MenuButton
} from "@chakra-ui/react";
import { Icon } from "@iconify/react";
import DesktopNav from "./DesktopNav";
import MobileNav from "./MobileNav";
import appLogo from "../../../public/images/logo.svg";
const Header = (): JSX.Element => {
const appName = "LCM Potty Chart";
const appVersion = process.env.NEXT_PUBLIC_APP_VERSION_HEADER || "";
// Add transparency while not at the top of the page.
const [transparentNavbar, setTransparentNavbar] = useState<boolean>(false);
const lastScroll = useRef<number>(0);
const handleScroll = (): void => {
// Sticky Nav
if (window.scrollY >= 20) {
setTransparentNavbar(true);
} else {
setTransparentNavbar(false);
}
// Scroll Position.
const currentScroll =
window.scrollY || window.pageYOffset || document.body.scrollTop;
// Update Scroll Position Reference
lastScroll.current = currentScroll <= 0 ? 0 : currentScroll;
// setScroll(lastScroll.current = currentScroll <= 0 ? 0 : currentScroll)
};
useEffect(() => {
if (!window) {
console.log("waiting for mount");
} else if (window) {
window.addEventListener("scroll", handleScroll);
}
return () => window.removeEventListener("scroll", handleScroll);
}, []);
// Mobile Menu Icon && Open/Close
const [open, setOpen] = useState<boolean>(false);
const [hover, setHover] = useState<boolean>(false);
const menuIcon = (): JSX.Element => {
const iconType = {
default: <Icon icon="bx:bx-menu-alt-right" />,
hover: <Icon icon="bx:bx-menu" />,
open: <Icon icon="bx:bx-x" />
};
if (open) {
return iconType.open;
} else if (hover) {
return iconType.hover;
} else {
return iconType.default;
}
};
return (
<Box
zIndex={1}
w="100%"
pos="fixed"
top={0}
alignItems="center"
boxShadow={
open
? "none"
: "rgba(0, 134, 255, 0.75) 0px 0px 15px, rgba(0, 134, 255, 0.5) 0px 0px 3px 1px"
}
bg={
open
? "brand.main"
: transparentNavbar
? "rgba(49, 56, 220, 0.9)"
: "brand.main"
}
transition=".5s ease"
borderRadius="0px 0px 10px 10px"
_hover={{
bg: "brand.main",
boxShadow: open
? "none"
: "rgba(0, 134, 255, 0.9) 0px 0px 15px, rgba(0, 134, 255, 0.7) 0px 0px 3px 1px"
}}
h={open ? "125px" : "auto"}
>
{/* Logo | Site Name */}
<HStack
display={{ base: "flex", lg: "none" }}
position="absolute"
width="100%"
height={12}
top={0}
ml={4}
spacing="5px"
justifyContent={{
base: "flex-start",
sm: "center"
}}
alignItems="center"
_hover={{
cursor: "default"
}}
>
<Image height="30" width="30" src={appLogo} alt="App Logo" />
<Heading as="h1" size="md">
{appName}
</Heading>
<Heading color="whiteAlpha.500" as="h2" size="sm">
{appVersion}
</Heading>
</HStack>
{/* Desktop Nav Items and Mobile Menu Button */}
<HStack
w="100%"
px={4}
h={12}
alignItems="center"
justifyContent="space-between"
>
<HStack
w="100%"
h="auto"
alignItems="center"
justifyContent="space-between"
>
<Box w="auto" display={{ base: "flex", lg: "none " }}></Box>
<Box w="100%" display={{ base: "none", lg: "flex" }} m="auto">
<HStack
width="100%"
alignItems="center"
height="auto"
spacing="5px"
_hover={{
cursor: "default"
}}
>
<Image height="30" width="30" src={appLogo} alt="App Logo" />
<Heading as="h1" size="md">
{appName}
</Heading>
<Heading color="whiteAlpha.500" as="h2" size="sm">
{appVersion}
</Heading>
</HStack>
</Box>
<DesktopNav />
</HStack>
<Menu isLazy lazyBehavior="unmount" isOpen={open}>
<MenuButton
id="mobile-menu-button"
as={IconButton}
aria-label="Mobile Menu"
icon={menuIcon()}
display={{
base: "inline-flex",
lg: "none"
}}
bg={transparentNavbar ? "transparent" : "rgba(255, 255, 255, .15)"}
border={transparentNavbar ? "1px solid #0068ff" : "none"}
variant="mobileNav"
type="button"
onClick={() => setOpen(!open)}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
/>
<MobileNav updateOpen={setOpen} />
</Menu>
</HStack>
</Box>
);
};
export default Header;

View File

@@ -0,0 +1,24 @@
import React, { FC, ReactNode } from "react";
import type { AppProps } from "next/app";
import Header from "../layout/Header";
import { Box } from "@chakra-ui/layout";
import Footer from "./Footer";
interface LayoutProps {
children: ReactNode;
elementType?: string;
}
const Layout: FC<LayoutProps> = (
{ children }: LayoutProps,
{ pageProps }: AppProps
) => {
return (
<Box w="100%">
<Header {...pageProps} />
<main>{children}</main>
<Footer />
</Box>
);
};
export default Layout;

View File

@@ -0,0 +1,56 @@
import React, { FC, Fragment } from "react";
import {
Button,
Link,
MenuDivider,
MenuItem,
MenuList
} from "@chakra-ui/react";
import navItems, { NavItem } from "./navItems";
interface MobileNavProps {
updateOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const MobileNav: FC<MobileNavProps> = ({ updateOpen }: MobileNavProps) => {
return (
<MenuList
as="nav"
display={{ base: "block", lg: "none" }}
h="auto"
w="100%"
p={0}
border="none"
boxShadow="none"
bg="brand.main"
>
{navItems.map((navItem: NavItem, index: number) => {
return (
<MenuItem
id={"mobile-" + navItem[0]}
key={navItem[0]}
w="auto"
h="auto"
p={0}
_hover={{
backgroundColor: "none"
}}
_focus={{
backgroundColor: "none"
}}
>
<Link onClick={() => updateOpen(false)} href={navItem[1]}>
{index === 0 ? <MenuDivider /> : <Fragment></Fragment>}
<Button w="100vw" variant={"nav"} p={0} m="auto">
{navItem[0]}
</Button>
<MenuDivider />
</Link>
</MenuItem>
);
})}
</MenuList>
);
};
export default MobileNav;

View File

@@ -0,0 +1,6 @@
export type NavItem = [string, string];
export type NavItems = NavItem[];
const navItems: NavItems = [["Home", "/"]];
export default navItems;

56
types/Calender.d.ts vendored Normal file
View File

@@ -0,0 +1,56 @@
type Days =
| "Sunday"
| "Monday"
| "Tuesday"
| "Wednesday"
| "Thursday"
| "Friday"
| "Saturday";
type DaysOfWeek = Days[];
interface WeekDays {
sunday: DaysOfWeek;
monday: DaysOfWeek;
}
interface MonthDay {
date: string;
isOverflow: boolean;
overflowDirection: "prev" | "next" | null;
}
interface Month {
week1: MonthDay[];
week2: MonthDay[];
week3: MonthDay[];
week4: MonthDay[];
week5: MonthDay[];
week6: MonthDay[];
}
interface WeekLayout {
weekdays: DaysOfWeek;
month: Month;
}
interface MonthLayout {
sunday: WeekLayout;
monday: WeekLayout;
}
interface UpdateCalenderPropsDateLayout {
year: number;
month: number;
day: number;
}
interface UpdateCalendarProps {
date: UpdateCalenderPropsDateLayout;
isLoading: boolean;
}
interface SelectedDateInfo {
date: string;
title: string;
layout: MonthLayout;
}

24
types/Stickers.d.ts vendored Normal file
View File

@@ -0,0 +1,24 @@
type StickerVal = -2 | -1 | 0 | 1 | 2 | null;
type ValidStickerVal = -2 | -1 | 0 | 1 | 2;
interface AddEditStickerProps {
date: Date;
sticker: ValidStickerVal;
}
interface Sticker {
id: string;
date: string;
sticker: StickerVal;
edited: boolean;
manual: boolean;
}
type StickerDays = Sticker[];
interface StickerModal {
isOpen: boolean;
selectedSticker: StickerVal;
step: number;
}

6502
yarn.lock

File diff suppressed because it is too large Load Diff