├── public
├── _redirects
└── favicon.png
├── .prettierrc
├── src
├── vite-env.d.ts
├── assets
│ ├── fonts
│ │ ├── Akira.ttf
│ │ └── Redro.ttf
│ ├── images
│ │ ├── eyes.gif
│ │ ├── moth.png
│ │ ├── mask.webp
│ │ ├── shapes.gif
│ │ ├── snake.png
│ │ ├── butterfly.png
│ │ ├── owl-head.webp
│ │ ├── painting.png
│ │ ├── butterfly-2.png
│ │ ├── carousel-1.gif
│ │ ├── carousel-2.gif
│ │ ├── carousel-3.gif
│ │ ├── moon-phases.png
│ │ ├── thinking-man.webp
│ │ ├── zodiac-wheel.png
│ │ ├── download-android.svg
│ │ └── download-apple.svg
│ └── printscreens
│ │ ├── laptop.png
│ │ ├── mobile.png
│ │ ├── tablet.png
│ │ ├── dark-mode.png
│ │ └── light-mode.png
├── components
│ ├── Pagination
│ │ ├── Pagination.css
│ │ ├── Pagination.types.ts
│ │ └── Pagination.tsx
│ ├── Accordion
│ │ ├── Accordion.types.ts
│ │ ├── Accordion.tsx
│ │ └── Accordion.css
│ ├── Button
│ │ ├── Button.types.ts
│ │ ├── Button.tsx
│ │ └── Button.css
│ ├── Loading
│ │ ├── Loading.tsx
│ │ └── Loading.css
│ ├── MagicCard
│ │ ├── MagicCard.css
│ │ └── MagicCard.tsx
│ ├── ZodiacCard
│ │ ├── ZodiacCard.tsx
│ │ └── ZodiacCard.css
│ ├── CardReading
│ │ ├── CardReading.css
│ │ └── CardReading.tsx
│ ├── ThemeToggle
│ │ ├── ThemeToggle.tsx
│ │ └── ThemeToggle.css
│ ├── SubscribeForm
│ │ ├── SubscribeForm.css
│ │ └── SubscribeForm.tsx
│ ├── FlipCard
│ │ ├── FlipCard.tsx
│ │ └── FlipCard.css
│ ├── ImageCarousel
│ │ ├── ImageCarousel.css
│ │ └── ImageCarousel.tsx
│ ├── Footer
│ │ ├── Footer.css
│ │ └── Footer.tsx
│ └── Header
│ │ ├── Header.tsx
│ │ └── Header.css
├── pages
│ ├── Numerology
│ │ ├── Numerology.types.ts
│ │ ├── Numerology.css
│ │ └── Numerology.tsx
│ ├── NotFound
│ │ ├── NotFound.css
│ │ └── NotFound.tsx
│ ├── About
│ │ ├── About.css
│ │ └── About.tsx
│ ├── ZodiacSignDetails
│ │ ├── ZodiacSignDetails.css
│ │ └── ZodiacSignDetails.tsx
│ ├── Zodiac
│ │ ├── Zodiac.css
│ │ └── Zodiac.tsx
│ ├── Tarot
│ │ ├── Tarot.css
│ │ └── Tarot.tsx
│ └── Home
│ │ ├── Home.tsx
│ │ └── Home.css
├── App.css
├── styles
│ ├── global
│ │ ├── animation-variables.css
│ │ ├── space-variables.css
│ │ ├── base.css
│ │ ├── color-variables.css
│ │ └── typography-variables.css
│ ├── utilities
│ │ ├── class-utilities.css
│ │ └── animation-utilities.css
│ ├── index.css
│ └── resets
│ │ └── normalize.css
├── types
│ ├── Tarot.types.ts
│ └── Zodiac.types.ts
├── main.tsx
├── hooks
│ └── useFetch.ts
├── data
│ ├── lettersData.ts
│ ├── accordionData.ts
│ └── lifePathData.ts
└── App.tsx
├── vite.config.ts
├── tsconfig.node.json
├── .gitignore
├── .eslintrc.cjs
├── index.html
├── tsconfig.json
├── package.json
└── README.md
/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/public/favicon.png
--------------------------------------------------------------------------------
/src/assets/fonts/Akira.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/fonts/Akira.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Redro.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/fonts/Redro.ttf
--------------------------------------------------------------------------------
/src/assets/images/eyes.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/images/eyes.gif
--------------------------------------------------------------------------------
/src/assets/images/moth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/images/moth.png
--------------------------------------------------------------------------------
/src/assets/images/mask.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/images/mask.webp
--------------------------------------------------------------------------------
/src/assets/images/shapes.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/images/shapes.gif
--------------------------------------------------------------------------------
/src/assets/images/snake.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/images/snake.png
--------------------------------------------------------------------------------
/src/assets/images/butterfly.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/images/butterfly.png
--------------------------------------------------------------------------------
/src/assets/images/owl-head.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/images/owl-head.webp
--------------------------------------------------------------------------------
/src/assets/images/painting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/images/painting.png
--------------------------------------------------------------------------------
/src/assets/images/butterfly-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/images/butterfly-2.png
--------------------------------------------------------------------------------
/src/assets/images/carousel-1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/images/carousel-1.gif
--------------------------------------------------------------------------------
/src/assets/images/carousel-2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/images/carousel-2.gif
--------------------------------------------------------------------------------
/src/assets/images/carousel-3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/images/carousel-3.gif
--------------------------------------------------------------------------------
/src/assets/images/moon-phases.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/images/moon-phases.png
--------------------------------------------------------------------------------
/src/assets/images/thinking-man.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/images/thinking-man.webp
--------------------------------------------------------------------------------
/src/assets/images/zodiac-wheel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/images/zodiac-wheel.png
--------------------------------------------------------------------------------
/src/assets/printscreens/laptop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/printscreens/laptop.png
--------------------------------------------------------------------------------
/src/assets/printscreens/mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/printscreens/mobile.png
--------------------------------------------------------------------------------
/src/assets/printscreens/tablet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/printscreens/tablet.png
--------------------------------------------------------------------------------
/src/assets/printscreens/dark-mode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/printscreens/dark-mode.png
--------------------------------------------------------------------------------
/src/assets/printscreens/light-mode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxenia/astrology-app/HEAD/src/assets/printscreens/light-mode.png
--------------------------------------------------------------------------------
/src/components/Pagination/Pagination.css:
--------------------------------------------------------------------------------
1 | .pagination {
2 | display: flex;
3 | flex-direction: row;
4 | justify-content: center;
5 | }
6 |
--------------------------------------------------------------------------------
/src/pages/Numerology/Numerology.types.ts:
--------------------------------------------------------------------------------
1 | export interface LifePathProps {
2 | number: number;
3 | title: string;
4 | description: string;
5 | url: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/Accordion/Accordion.types.ts:
--------------------------------------------------------------------------------
1 | export interface AccordionProps {
2 | title: string;
3 | content: string;
4 | isActive: boolean;
5 | onToggle: () => void;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/Pagination/Pagination.types.ts:
--------------------------------------------------------------------------------
1 | export interface PaginationProps {
2 | page: number;
3 | totalPages: number;
4 | handlePagination: (page: number) => void;
5 | }
6 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | #dark {
2 | background-color: #000000;
3 | color: rgba(255, 255, 255, 0.834);
4 | transition: 0.3s all;
5 | }
6 |
7 | #light {
8 | transition: 0.3s all;
9 | }
10 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/src/pages/NotFound/NotFound.css:
--------------------------------------------------------------------------------
1 | .not-found {
2 | height: 100vh;
3 | display: flex;
4 | justify-content: center;
5 | flex-direction: column;
6 | align-items: center;
7 | margin: auto;
8 | width: 50%;
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/src/styles/global/animation-variables.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --duration-instant: 0ms;
3 | --duration-fast: 50ms;
4 | --duration-quick: 100ms;
5 | --duration-standard: 150ms;
6 | --duration-slow: 250ms;
7 | --duration-slower: 300ms;
8 | --duration-slowest: 600ms;
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Button/Button.types.ts:
--------------------------------------------------------------------------------
1 | export interface ButtonProps {
2 | children: string;
3 | url?: string;
4 | target?: '_blank' | '_self';
5 | className?: string;
6 | onClick?: () => void;
7 | as?: 'button' | 'link';
8 | type?: 'button' | 'submit' | 'reset';
9 | }
10 |
--------------------------------------------------------------------------------
/src/types/Tarot.types.ts:
--------------------------------------------------------------------------------
1 | export interface TarotProps {
2 | id: number;
3 | image: string;
4 | link: string;
5 | name: string;
6 | reversed: string[];
7 | upright: string[];
8 | type: 'Major' | 'Minor';
9 | }
10 |
11 | export interface TarotCardProps {
12 | card: TarotProps;
13 | }
14 |
--------------------------------------------------------------------------------
/src/styles/global/space-variables.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --space-005: 0.05rem;
3 | --space-02: 0.2rem;
4 | --space-03: 0.3rem;
5 | --space-04: 0.4rem;
6 | --space-05: 0.5rem;
7 | --space-10: 1rem;
8 | --space-15: 1.5rem;
9 | --space-20: 2rem;
10 | --space-25: 25rem;
11 | --space-30: 3rem;
12 | --space-40: 4rem;
13 | }
14 |
--------------------------------------------------------------------------------
/src/styles/utilities/class-utilities.css:
--------------------------------------------------------------------------------
1 | [data-theme='dark'] .invert {
2 | -webkit-filter: invert(80%);
3 | filter: invert(80%);
4 | }
5 |
6 | .description {
7 | width: 90%;
8 | margin: auto;
9 | text-align: start;
10 | }
11 |
12 | @media (min-width: 1200px) {
13 | .description {
14 | width: 60%;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/styles/index.css:
--------------------------------------------------------------------------------
1 | @import url('./resets/normalize.css');
2 |
3 | @import './global/animation-variables.css';
4 | @import './global/base.css';
5 | @import './global/color-variables.css';
6 | @import './global/space-variables.css';
7 | @import './global/typography-variables.css';
8 |
9 | @import './utilities/animation-utilities.css';
10 | @import './utilities/class-utilities.css';
11 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { BrowserRouter } from 'react-router-dom';
4 |
5 | import App from './App.tsx';
6 |
7 | import './styles/index.css';
8 |
9 | ReactDOM.createRoot(document.getElementById('root')!).render(
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | # Extra
27 | src/data-backup/
28 | .prettierrc
29 |
30 |
--------------------------------------------------------------------------------
/src/pages/NotFound/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import mask from '../../assets/images/mask.webp';
2 | import './NotFound.css';
3 |
4 | function NotFound() {
5 | return (
6 |
7 |
OOPS!
8 |
9 | Something went wrong. The page you are looking for seems to not be here.
10 |
11 |
12 |
13 | );
14 | }
15 |
16 | export default NotFound;
17 |
--------------------------------------------------------------------------------
/src/components/Loading/Loading.tsx:
--------------------------------------------------------------------------------
1 | import './Loading.css';
2 |
3 | function Loading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
20 | export default Loading;
21 |
--------------------------------------------------------------------------------
/src/pages/About/About.css:
--------------------------------------------------------------------------------
1 | .about {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | min-height: 100vh;
7 | }
8 |
9 | .about__info {
10 | width: 90%;
11 | text-align: start;
12 | }
13 |
14 | .about img {
15 | width: 15rem;
16 | }
17 |
18 | .about {
19 | padding-bottom: var(--space-40);
20 | }
21 |
22 | @media (min-width: 768px) {
23 | .about__info {
24 | text-align: center;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | 'react-refresh/only-export-components': [
14 | 'warn',
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 | Lunar | Astrology in your pocket
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/components/MagicCard/MagicCard.css:
--------------------------------------------------------------------------------
1 | .glow {
2 | background: linear-gradient(-45deg, #ffe550, #e7a53c, #d55e23, #f59928);
3 | background-size: 400% 400%;
4 | animation: gradient 5s ease infinite;
5 | }
6 |
7 | html[data-theme='dark'] .glow {
8 | background: linear-gradient(-45deg, #8952ee, #6c00b5, #3823d5, #be28f5);
9 | background-size: 400% 400%;
10 | animation: gradient 5s ease infinite;
11 | }
12 |
13 | .magic-card {
14 | display: flex;
15 | justify-content: center;
16 | align-items: center;
17 | font-size: 200px;
18 | color: var(--white-sheer);
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/ZodiacCard/ZodiacCard.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 | import type { ZodiacCardProps } from '../../types/Zodiac.types';
4 |
5 | import './ZodiacCard.css';
6 |
7 | function ZodiacCard({ sign }: ZodiacCardProps) {
8 | return (
9 |
10 |
11 |
{sign.name}
12 |
13 |
Dates: {sign.dates}
14 |
15 |
16 | );
17 | }
18 |
19 | export default ZodiacCard;
20 |
--------------------------------------------------------------------------------
/src/types/Zodiac.types.ts:
--------------------------------------------------------------------------------
1 | export interface ZodiacProps {
2 | id: number;
3 | elementId: number;
4 | modalityId: number;
5 | rulerId: number;
6 | name: string;
7 | dates: string;
8 | positiveTraits: string[];
9 | negativeTraits: string[];
10 | image: string;
11 | element: { image: string; keywords: string[]; name: string };
12 | ruler: {
13 | image: string;
14 | keywords: string[];
15 | name: string;
16 | transition: string;
17 | type: string;
18 | };
19 | modality: { image: string; keywords: string[]; name: string };
20 | }
21 |
22 | export interface ZodiacCardProps {
23 | sign: ZodiacProps;
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/Accordion/Accordion.tsx:
--------------------------------------------------------------------------------
1 | import { AccordionProps } from './Accordion.types';
2 |
3 | import './Accordion.css';
4 |
5 | const Accordion = ({ title, content, isActive, onToggle }: AccordionProps) => {
6 | const handleClick = () => {
7 | onToggle();
8 | };
9 |
10 | return (
11 |
12 |
13 |
{title}
14 |
{isActive ? '-' : '+'}
15 |
16 |
17 | {content}
18 |
19 |
20 | );
21 | };
22 |
23 | export default Accordion;
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/ZodiacCard/ZodiacCard.css:
--------------------------------------------------------------------------------
1 | .card__link {
2 | letter-spacing: 0;
3 | }
4 |
5 | .card__item {
6 | display: flex;
7 | flex-direction: column;
8 | justify-content: center;
9 | align-items: center;
10 | font-weight: var(--medium-weight);
11 | gap: var(--space-10);
12 | }
13 |
14 | .card__item span {
15 | background-color: var(--quaternary-foreground);
16 | padding: var(--space-02);
17 | }
18 |
19 | .card__img {
20 | height: 8rem;
21 | }
22 |
23 | [data-theme='dark'] .card__img {
24 | -webkit-filter: invert(80%);
25 | filter: invert(80%);
26 | }
27 |
28 | @media (min-width: 768px) {
29 | .card__img {
30 | height: 6rem;
31 | }
32 | }
33 |
34 | @media (min-width: 992px) {
35 | .card__img {
36 | height: 8rem;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/CardReading/CardReading.css:
--------------------------------------------------------------------------------
1 | .reading--description {
2 | width: 70%;
3 | text-align: start;
4 | margin: auto;
5 | padding-bottom: var(--space-20);
6 | }
7 |
8 | .card__list--reading {
9 | display: grid;
10 | grid-template-columns: repeat(1, 1fr);
11 | gap: var(--space-10);
12 | margin: auto;
13 | padding: var(--space-20);
14 | }
15 |
16 | .card__btn--reading {
17 | display: block;
18 | margin: var(--space-20) auto;
19 | }
20 |
21 | @media (min-width: 920px) {
22 | .card__list--reading {
23 | grid-template-columns: repeat(3, 1fr);
24 | width: 90%;
25 | }
26 | }
27 |
28 | @media (min-width: 1200px) {
29 | .card__list--reading {
30 | width: 70%;
31 | }
32 | }
33 |
34 | @media (min-width: 1600px) {
35 | .card__list--reading {
36 | width: 50%;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonProps } from './Button.types';
2 |
3 | import './Button.css';
4 |
5 | const Button = ({
6 | children,
7 | url,
8 | target,
9 | className,
10 | onClick,
11 | type,
12 | as = 'button',
13 | }: ButtonProps) => {
14 | if (as === 'link' && url) {
15 | return (
16 |
22 | {children}
23 |
24 | );
25 | } else {
26 | return (
27 |
32 | {children}
33 |
34 | );
35 | }
36 | };
37 |
38 | export default Button;
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lunar",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0",
15 | "react-router-dom": "^6.15.0"
16 | },
17 | "devDependencies": {
18 | "@types/react": "^18.2.15",
19 | "@types/react-dom": "^18.2.7",
20 | "@typescript-eslint/eslint-plugin": "^6.0.0",
21 | "@typescript-eslint/parser": "^6.0.0",
22 | "@vitejs/plugin-react": "^4.0.3",
23 | "eslint": "^8.45.0",
24 | "eslint-plugin-react-hooks": "^4.6.0",
25 | "eslint-plugin-react-refresh": "^0.4.3",
26 | "typescript": "^5.0.2",
27 | "vite": "^4.5.2"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/pages/ZodiacSignDetails/ZodiacSignDetails.css:
--------------------------------------------------------------------------------
1 | .card-details {
2 | width: 80%;
3 | margin: auto;
4 | margin-bottom: var(--space-20);
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: flex-start;
8 | align-items: center;
9 | text-align: left;
10 | }
11 |
12 | .card-details__heading {
13 | display: flex;
14 | flex-direction: column;
15 | align-items: center;
16 | margin-bottom: var(--space-20);
17 | }
18 |
19 | .card-details__heading img {
20 | width: 5rem;
21 | margin-top: -2rem;
22 | margin-bottom: var(--space-10);
23 | }
24 |
25 | .card-details h3 {
26 | margin-top: var(--space-20);
27 | }
28 |
29 | .card-details ul {
30 | margin-bottom: var(--space-20);
31 | border: 2px solid var(--primary-foreground);
32 | padding: var(--space-20);
33 | box-shadow: var(--quaternary-foreground) 5px 5px;
34 | }
35 |
36 | .snake-img {
37 | width: 10rem;
38 | margin: var(--space-20);
39 | }
40 |
--------------------------------------------------------------------------------
/src/styles/utilities/animation-utilities.css:
--------------------------------------------------------------------------------
1 | @keyframes skew-y-shaking {
2 | 0% {
3 | transform: translate(0, 0) rotate(0deg);
4 | }
5 | 25% {
6 | transform: translate(5px, 5px) rotate(5deg);
7 | }
8 | 50% {
9 | transform: translate(0, 0) rotate(0eg);
10 | }
11 | 75% {
12 | transform: translate(-5px, 5px) rotate(-5deg);
13 | }
14 | 100% {
15 | transform: translate(0, 0) rotate(0deg);
16 | }
17 | }
18 |
19 | @keyframes rotation {
20 | from {
21 | transform: rotate(0deg);
22 | }
23 | to {
24 | transform: rotate(359deg);
25 | }
26 | }
27 |
28 | @keyframes lds-roller {
29 | 0% {
30 | transform: rotate(0deg);
31 | }
32 | 100% {
33 | transform: rotate(360deg);
34 | }
35 | }
36 |
37 | @keyframes gradient {
38 | 0% {
39 | background-position: 0% 50%;
40 | }
41 | 50% {
42 | background-position: 100% 50%;
43 | }
44 | 100% {
45 | background-position: 0% 50%;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/ThemeToggle/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | import { ThemeContext } from '../../App';
4 |
5 | import './ThemeToggle.css';
6 |
7 | export default function ThemeToggle() {
8 | const { theme, toggleTheme } = useContext(ThemeContext);
9 |
10 | function onChangeHandler() {
11 | if (theme === 'light') {
12 | document.documentElement.setAttribute('data-theme', 'dark');
13 | toggleTheme();
14 | } else {
15 | document.documentElement.setAttribute('data-theme', 'light');
16 | toggleTheme();
17 | }
18 | }
19 |
20 | return (
21 |
22 |
23 |
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/SubscribeForm/SubscribeForm.css:
--------------------------------------------------------------------------------
1 | .subscribe-form {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | padding: 1rem;
7 | color: var(--tertiary-foreground);
8 | gap: 1rem;
9 | }
10 |
11 | .subscribe-form label {
12 | color: var(--tertiary-foreground);
13 | }
14 |
15 | .subscribe-form input {
16 | border: 2px solid var(--tertiary-foreground);
17 | background-color: var(--tertiary-background);
18 | color: var(--tertiary-foreground);
19 | padding: 0.5rem;
20 | margin: 0.5rem;
21 | width: 15rem;
22 | }
23 |
24 | .subscribe-form__button {
25 | color: var(--tertiary-foreground);
26 | background-color: var(--tertiary-background);
27 | border: 2px solid var(--tertiary-foreground);
28 | }
29 |
30 | @media (min-width: 768px) {
31 | .subscribe-form input {
32 | width: 20rem;
33 | }
34 | }
35 |
36 | @media (min-width: 1200px) {
37 | .subscribe-form input {
38 | width: 25rem;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/hooks/useFetch.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | const useFetch = (url: string) => {
4 | const [data, setData] = useState(null);
5 | const [error, setError] = useState(null);
6 | const [loading, setLoading] = useState(false);
7 |
8 | useEffect(() => {
9 | (async function () {
10 | const controller = new AbortController();
11 | const signal = controller.signal;
12 |
13 | try {
14 | setLoading(true);
15 |
16 | const response = await fetch(url, { signal });
17 | const data = await response.json();
18 |
19 | setData(data);
20 | } catch (error) {
21 | setError(error as Error);
22 | } finally {
23 | setTimeout(() => {
24 | setLoading(false);
25 | }, 700);
26 | }
27 |
28 | return () => controller?.abort();
29 | })();
30 | }, [url]);
31 |
32 | return { data, error, loading };
33 | };
34 |
35 | export default useFetch;
36 |
--------------------------------------------------------------------------------
/src/styles/global/base.css:
--------------------------------------------------------------------------------
1 | html {
2 | background-color: var(--primary-background);
3 | }
4 |
5 | body {
6 | font-family: var(--body-font);
7 | font-weight: var(--regular-weight);
8 | font-size: var(--regular-size);
9 | color: var(--primary-foreground);
10 | letter-spacing: var(--space-005);
11 | font-size: var(--regular-size);
12 | }
13 |
14 | button {
15 | padding: var(--space-05);
16 | }
17 |
18 | a {
19 | text-decoration: none;
20 | }
21 |
22 | a:hover,
23 | button:hover {
24 | cursor: pointer;
25 | transition: ease-in-out var(--duration-slow);
26 | }
27 |
28 | ul {
29 | list-style-type: none;
30 | }
31 |
32 | h1 {
33 | font-family: var(--title-font);
34 | font-size: var(--large-size);
35 | text-align: center;
36 | }
37 |
38 | h2 {
39 | font-family: var(--highlight-font);
40 | font-size: var(--medium-size);
41 | }
42 |
43 | h3 {
44 | font-family: var(--body-font);
45 | font-size: var(--medium-size);
46 | font-weight: var(--light-weight);
47 | }
48 |
49 | .page {
50 | min-height: 100vh;
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/Pagination/Pagination.tsx:
--------------------------------------------------------------------------------
1 | import Button from '../Button/Button';
2 | import { PaginationProps } from './Pagination.types';
3 |
4 | import './Pagination.css';
5 |
6 | export const Pagination = ({
7 | page,
8 | totalPages,
9 | handlePagination,
10 | }: PaginationProps) => {
11 | const scrollToTop = () => {
12 | window.scrollTo({ top: 0, behavior: 'smooth' });
13 | };
14 |
15 | return (
16 |
17 |
18 | {page > 1 && (
19 | {
21 | handlePagination(page - 1);
22 | scrollToTop();
23 | }}
24 | >
25 | 👈🏼 Back
26 |
27 | )}
28 | {page < totalPages && (
29 | {
31 | handlePagination(page + 1);
32 | scrollToTop();
33 | }}
34 | >
35 | Forward 👉🏼
36 |
37 | )}
38 |
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/src/pages/Zodiac/Zodiac.css:
--------------------------------------------------------------------------------
1 | .zodiac {
2 | min-height: 100vh;
3 | }
4 |
5 | .astrology__image {
6 | display: block;
7 | margin: auto;
8 | width: 15rem;
9 | margin-bottom: -5rem;
10 | margin-top: var(--space-20);
11 | border-radius: 0 0 50% 50%;
12 | }
13 |
14 | .astrology__list {
15 | display: grid;
16 | grid-template-columns: repeat(1, 1fr);
17 | padding: var(--space-20) 0;
18 | }
19 |
20 | @media (min-width: 768px) {
21 | .astrology__image {
22 | margin-bottom: 0;
23 | }
24 |
25 | .astrology__list {
26 | grid-template-columns: repeat(2, 1fr);
27 | padding: var(--space-30);
28 | }
29 | }
30 |
31 | @media (min-width: 992px) {
32 | .astrology__image {
33 | margin-bottom: -5.5rem;
34 | border-radius: 0;
35 | }
36 |
37 | .astrology__list {
38 | display: grid;
39 | grid-template-columns: repeat(3, 1fr);
40 | margin: auto;
41 | }
42 | }
43 |
44 | @media (min-width: 1400px) {
45 | .astrology__list {
46 | width: 80%;
47 | }
48 | }
49 |
50 | @media (min-width: 2360px) {
51 | .astrology__list {
52 | width: 70%;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/Accordion/Accordion.css:
--------------------------------------------------------------------------------
1 | .accordion__item {
2 | width: 90%;
3 | margin: auto;
4 | }
5 |
6 | .accordion__title {
7 | display: flex;
8 | flex-direction: row;
9 | align-items: center;
10 | justify-content: space-between;
11 | background-color: var(--quaternary-foreground);
12 | padding: var(--space-05);
13 | border: 2px solid var(--primary-foreground);
14 | }
15 |
16 | .accordion__title:hover {
17 | transition: ease-in-out var(--duration-standard);
18 | background-color: var(--primary-background);
19 | cursor: pointer;
20 | }
21 |
22 | .accordion__content {
23 | transition: ease-in-out var(--duration-quick);
24 | max-height: 0;
25 | overflow: hidden;
26 | text-align: justify;
27 | margin: 0.5rem;
28 | opacity: 0;
29 | }
30 |
31 | .accordion__content.dropped {
32 | transition: ease-in-out var(--duration-slow);
33 | max-height: 600px;
34 | margin: 1.5rem;
35 | opacity: 1;
36 | }
37 |
38 | @media (min-width: 768px) {
39 | .accordion__item {
40 | width: 60%;
41 | }
42 | }
43 |
44 | @media (min-width: 1200px) {
45 | .accordion__item {
46 | width: 50%;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/FlipCard/FlipCard.tsx:
--------------------------------------------------------------------------------
1 | import Button from '../Button/Button.tsx';
2 | import { TarotCardProps } from '../../types/Tarot.types.ts';
3 |
4 | import './FlipCard.css';
5 |
6 | function TarotCard({ card }: TarotCardProps) {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
{card.name}
15 |
16 |
Type: {card.type}
17 |
Upright: {card.upright.join(', ')}
18 |
Reversed: {card.reversed.join(', ')}
19 |
20 |
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | export default TarotCard;
34 |
--------------------------------------------------------------------------------
/src/styles/global/color-variables.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --black: rgb(0, 0, 0);
3 | --black-sheer: rgba(0, 0, 0, 0.7);
4 | --dark-grey: rgb(83, 83, 83);
5 | --light-grey: rgb(206, 206, 206);
6 | --white: rgb(255, 255, 255);
7 | --white-sheer: rgba(255, 255, 255, 0.8);
8 | --orange: rgb(255, 193, 77);
9 | --purple: rgb(90, 0, 169);
10 | }
11 |
12 | html[data-theme='dark'] {
13 | --primary-foreground: var(--white-sheer);
14 | --secondary-foreground: var(--white);
15 | --tertiary-foreground: var(--white-sheer);
16 | --quaternary-foreground: var(--purple);
17 | --primary-background: var(--black);
18 | --secondary-background: var(--black-sheer);
19 | --tertiary-background: var(--black);
20 | --quaternary-background: var(--purple);
21 | }
22 |
23 | html[data-theme='light'] {
24 | --primary-foreground: var(--black);
25 | --secondary-foreground: var(--dark-grey);
26 | --tertiary-foreground: var(--white);
27 | --quaternary-foreground: var(--orange);
28 | --primary-background: var(--white);
29 | --secondary-background: var(--white-sheer);
30 | --tertiary-background: var(--black);
31 | --quaternary-background: var(--orange);
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/Button/Button.css:
--------------------------------------------------------------------------------
1 | .button--shadow {
2 | color: var(--primary-foreground);
3 | text-decoration: none;
4 | padding: 0.5rem;
5 | margin: 20px;
6 | border: 2px solid var(--primary-foreground);
7 | &:after {
8 | background-color: var(--primary-foreground);
9 | }
10 | &:hover {
11 | top: 5px;
12 | box-shadow: none;
13 | &:after {
14 | position: absolute;
15 | top: 0em;
16 | right: 0em;
17 | left: 0em;
18 | bottom: 0em;
19 | }
20 | }
21 | &:active {
22 | background-color: var(--quaternary-foreground);
23 | color: var(--primary-foreground);
24 | transition: color 200ms, background-color 200ms;
25 | }
26 | }
27 |
28 | .shadow {
29 | transition: all 100ms;
30 | background-color: var(--primary-background);
31 | position: relative;
32 | box-shadow: 4px 4px 4px 0px rgba(50, 50, 50, 0.05);
33 | &:after {
34 | transition: all 100ms;
35 | content: '';
36 | display: block;
37 | z-index: -1;
38 | background-color: var(--primary-foreground);
39 | position: absolute;
40 | top: 0.5em;
41 | right: -0.4em;
42 | left: 0.4em;
43 | bottom: -0.4em;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/data/lettersData.ts:
--------------------------------------------------------------------------------
1 | // Credits to @themuuln on Github for the API data and formulas
2 |
3 | export const soulUrgeLetters: { [key: string]: number } = {
4 | А: 1,
5 | И: 1,
6 | О: 7,
7 | Ө: 8,
8 | У: 4,
9 | Ү: 5,
10 | Э: 6,
11 | A: 1,
12 | E: 5,
13 | I: 9,
14 | O: 6,
15 | U: 3,
16 | };
17 |
18 | export const expressionLetters: { [key: string]: number } = {
19 | А: 1,
20 | Б: 2,
21 | В: 3,
22 | Г: 4,
23 | Д: 5,
24 | Е: 6,
25 | Ё: 7,
26 | Ж: 8,
27 | З: 9,
28 | И: 1,
29 | Й: 2,
30 | К: 3,
31 | Л: 4,
32 | М: 5,
33 | Н: 6,
34 | О: 7,
35 | Ө: 8,
36 | П: 9,
37 | Р: 1,
38 | С: 2,
39 | Т: 3,
40 | У: 4,
41 | Ү: 5,
42 | Ф: 6,
43 | Х: 7,
44 | Ц: 8,
45 | Ч: 9,
46 | Ш: 1,
47 | Щ: 2,
48 | Ъ: 3,
49 | Ь: 4,
50 | Ы: 5,
51 | Э: 6,
52 | Ю: 7,
53 | Я: 8,
54 | // latin
55 | A: 1,
56 | B: 2,
57 | C: 3,
58 | D: 4,
59 | E: 5,
60 | F: 6,
61 | G: 7,
62 | H: 8,
63 | I: 9,
64 | J: 1,
65 | K: 2,
66 | L: 3,
67 | M: 4,
68 | N: 5,
69 | O: 6,
70 | P: 7,
71 | Q: 8,
72 | R: 9,
73 | S: 1,
74 | T: 2,
75 | U: 3,
76 | V: 4,
77 | W: 5,
78 | X: 6,
79 | Y: 7,
80 | Z: 9,
81 | };
82 |
--------------------------------------------------------------------------------
/src/pages/About/About.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import shapes from '../../assets/images/shapes.gif';
3 | import Accordion from '../../components/Accordion/Accordion';
4 | import { accordionData } from '../../data/accordionData';
5 | import './About.css';
6 |
7 | export default function About() {
8 | const [activeIndex, setActiveIndex] = useState(-1);
9 |
10 | const toggleAccordion = (index: number) => {
11 | setActiveIndex(index === activeIndex ? -1 : index);
12 | };
13 |
14 | return (
15 |
16 | About
17 | So what is Lunar, really?
18 |
19 | In this section I will walk you through all you need to know about my
20 | website.
21 |
22 |
23 |
24 | {accordionData.map(({ title, content }, index) => (
25 |
toggleAccordion(index)}
31 | />
32 | ))}
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/ImageCarousel/ImageCarousel.css:
--------------------------------------------------------------------------------
1 | .carousel {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | flex-direction: column;
6 | margin-top: var(--space-30);
7 | }
8 |
9 | .carousel span {
10 | display: block;
11 | text-align: center;
12 | font-weight: var(--bold-weight);
13 | padding: var(--space-10);
14 | }
15 |
16 | .results {
17 | font-size: var(--small-size);
18 | }
19 |
20 | .box__buttons {
21 | display: flex;
22 | justify-content: flex-start;
23 | margin: 1rem;
24 | }
25 |
26 | .box__buttons button {
27 | margin: var(--space-03);
28 | font-size: var(--small-size);
29 | border: 2px solid var(--primary-foreground);
30 | color: var(--primary-foreground);
31 | background-color: var(--primary-background);
32 | }
33 |
34 | .box__buttons button:hover {
35 | background: var(--primary-background);
36 | background: radial-gradient(
37 | circle,
38 | var(--quaternary-background) 0%,
39 | var(--primary-background) 70%
40 | );
41 | }
42 |
43 | .box__ul {
44 | width: 20rem;
45 | display: flex;
46 | justify-items: center;
47 | flex-direction: row;
48 | overflow-x: hidden;
49 | gap: 1rem;
50 | }
51 |
52 | .box__ul li img {
53 | width: 20rem;
54 | height: 20rem;
55 | }
56 |
57 | .box-ul li span {
58 | text-align: center;
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/ThemeToggle/ThemeToggle.css:
--------------------------------------------------------------------------------
1 | .toggle__switch {
2 | position: relative;
3 | display: inline-block;
4 | width: 40px;
5 | height: 20px;
6 | margin-top: 3px;
7 | }
8 |
9 | .toggle__switch input {
10 | opacity: 0;
11 | width: 0;
12 | height: 0;
13 | }
14 |
15 | .toggle__slider {
16 | position: absolute;
17 | cursor: pointer;
18 | top: 0;
19 | left: 0;
20 | right: 0;
21 | bottom: 0;
22 | background-color: var(--light-grey);
23 | transition: 0.4s;
24 | border-radius: 20px;
25 | }
26 |
27 | .toggle__slider:before {
28 | position: absolute;
29 | content: '';
30 | height: 16px;
31 | width: 16px;
32 | left: 2px;
33 | bottom: 2px;
34 | background-color: var(--black);
35 | transition: 0.4s;
36 | border-radius: 50%;
37 | }
38 |
39 | input:checked + .toggle__slider {
40 | background-color: var(--dark-grey);
41 | }
42 |
43 | input:checked + .toggle__slider:before {
44 | transform: translateX(20px);
45 | }
46 |
47 | .toggle__slider.round {
48 | border-radius: 20px;
49 | }
50 |
51 | .toggle__slider.round:before {
52 | border-radius: 50%;
53 | }
54 |
55 | @media (max-width: 767px) {
56 | .toggle__switch {
57 | right: 23px;
58 | }
59 | }
60 |
61 | @media (min-width: 1400px) {
62 | .toggle__switch {
63 | top: 2px;
64 | }
65 | }
66 |
67 | @media (min-width: 2360px) {
68 | .toggle__switch {
69 | top: 10px;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/components/Footer/Footer.css:
--------------------------------------------------------------------------------
1 | footer {
2 | position: relative;
3 | bottom: 0;
4 | left: 0;
5 | right: 0;
6 | padding-top: var(--space-20);
7 | border-top: 2px solid var(--light-grey);
8 | }
9 |
10 | .footer__contact {
11 | display: flex;
12 | justify-content: center;
13 | text-align: start;
14 | width: 90%;
15 | margin: auto;
16 | font-size: var(--small-size);
17 | font-weight: var(--regular-weight);
18 | }
19 |
20 | .footer__download,
21 | .footer__socials {
22 | display: flex;
23 | flex-direction: row;
24 | justify-content: center;
25 | align-items: center;
26 | gap: var(--space-10);
27 | font-size: var(--space-15);
28 | padding: var(--space-10);
29 | }
30 |
31 | .footer__download img:hover {
32 | cursor: pointer;
33 | }
34 |
35 | .footer__socials li a {
36 | color: var(--primary-foreground);
37 | }
38 |
39 | .footer__socials li a i:hover {
40 | color: var(--secondary-foreground);
41 | transition: ease-in-out var(--duration-standard);
42 | }
43 |
44 | .footer__copyright {
45 | background-color: var(--tertiary-background);
46 | color: var(--tertiary-foreground);
47 | font-size: var(--small-size);
48 | text-align: center;
49 | padding: var(--space-05);
50 | }
51 |
52 | @media (min-width: 768px) {
53 | .footer__contact {
54 | width: 40%;
55 | }
56 | }
57 |
58 | @media (min-width: 992px) {
59 | .footer__contact {
60 | width: 100%;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/MagicCard/MagicCard.tsx:
--------------------------------------------------------------------------------
1 | import Button from '../Button/Button.tsx';
2 | import { TarotCardProps } from '../../types/Tarot.types.ts';
3 |
4 | import './MagicCard.css';
5 |
6 | function getRandomBoolean() {
7 | return Math.random() < 0.5;
8 | }
9 |
10 | function TarotCard({ card }: TarotCardProps) {
11 | const uprightReversed = getRandomBoolean();
12 |
13 | return (
14 |
15 |
16 |
17 | ?
18 |
19 |
20 |
{card.name}
21 |
22 |
23 | This card is
24 | {uprightReversed ? ' reversed' : ' upright'}.
25 |
26 |
27 | Reflect on the following keywords: {''}
28 | {uprightReversed
29 | ? card.reversed.join(', ')
30 | : card.upright.join(', ')}
31 |
32 |
33 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | export default TarotCard;
47 |
--------------------------------------------------------------------------------
/src/components/Footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import downloadApple from '../../assets/images/download-apple.svg';
2 | import downloadAndroid from '../../assets/images/download-android.svg';
3 |
4 | import './Footer.css';
5 |
6 | export default function Footer() {
7 | return (
8 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/pages/Zodiac/Zodiac.tsx:
--------------------------------------------------------------------------------
1 | import owlHead from '../../assets/images/owl-head.webp';
2 | import useFetch from '../../hooks/useFetch';
3 | import ZodiacCard from '../../components/ZodiacCard/ZodiacCard.tsx';
4 | import Loading from '../../components/Loading/Loading.tsx';
5 | import { ZodiacProps } from '../../types/Zodiac.types.ts';
6 |
7 | import './Zodiac.css';
8 |
9 | function Zodiac() {
10 | const url = 'https://jps-tarot-api.azurewebsites.net/api/Zodiac/Get';
11 | const { data: signs, error, loading } = useFetch(url);
12 |
13 | if (error) {
14 | console.log(`Error: ${error.message}`);
15 | }
16 |
17 | return (
18 |
19 | Astrology
20 | {error && {error?.message}
}
21 | {loading && }
22 |
23 | Astrology is an ancient and complex system of divination that has been
24 | practiced for thousands of years. Its origins can be traced back to
25 | various civilizations throughout history, including Mesopotamia, Egypt,
26 | and Greece. The Western zodiac, also known as the tropical zodiac, is
27 | based on the position of the Sun relative to the twelve zodiac signs at
28 | the time of a person's birth. Each sign is associated with specific
29 | personality traits and characteristics.
30 |
31 |
32 |
33 | {signs?.map((sign) => (
34 |
35 | ))}
36 |
37 |
38 | );
39 | }
40 |
41 | export default Zodiac;
42 |
--------------------------------------------------------------------------------
/src/pages/Tarot/Tarot.css:
--------------------------------------------------------------------------------
1 | .tarot {
2 | min-height: 100vh;
3 | }
4 |
5 | .card__list {
6 | display: grid;
7 | grid-template-columns: repeat(1, 1fr);
8 | padding: 2rem;
9 | gap: 1rem;
10 | }
11 |
12 | .filter-buttons {
13 | display: flex;
14 | flex-direction: column;
15 | align-items: center;
16 | justify-content: center;
17 | margin-bottom: var(--space-10);
18 | }
19 |
20 | .filter-input {
21 | width: 90%;
22 | margin: auto;
23 | display: flex;
24 | flex-direction: column;
25 | align-items: center;
26 | justify-content: center;
27 | padding: var(--space-10);
28 | }
29 |
30 | .search-input {
31 | display: block;
32 | margin: 1rem auto;
33 | padding: 0.5rem;
34 | border: 2px solid var(--primary-foreground);
35 | background-color: var(--primary-background);
36 | color: var(--primary-foreground);
37 | }
38 |
39 | /* .card__item {
40 | border: 1px solid white;
41 | background-color: rgb(23, 23, 23);
42 | width: 90%;
43 | } */
44 |
45 | @media (min-width: 640px) {
46 | .filter-buttons {
47 | flex-direction: row;
48 | }
49 |
50 | .card__list {
51 | display: grid;
52 | grid-template-columns: repeat(2, 1fr);
53 | margin: auto;
54 | width: 90%;
55 | }
56 | }
57 |
58 | @media (min-width: 920px) {
59 | .card__list {
60 | display: flex;
61 | flex-wrap: wrap;
62 | justify-content: center;
63 | margin: auto;
64 | }
65 | }
66 |
67 | @media (min-width: 1300px) {
68 | .card__list {
69 | display: grid;
70 | grid-template-columns: repeat(4, 1fr);
71 | width: 70%;
72 | }
73 | }
74 |
75 | @media (min-width: 2360px) {
76 | .card__list {
77 | display: grid;
78 | grid-template-columns: repeat(6, 1fr);
79 | width: 50%;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Route, Routes } from 'react-router-dom';
2 | import { createContext, useState } from 'react';
3 |
4 | import Header from './components/Header/Header';
5 | import Footer from './components/Footer/Footer';
6 |
7 | import Home from '../src/pages/Home/Home';
8 | import About from './pages/About/About';
9 | import Tarot from './pages/Tarot/Tarot';
10 | import Zodiac from './pages/Zodiac/Zodiac';
11 | import ZodiacSignDetails from './pages/ZodiacSignDetails/ZodiacSignDetails';
12 | import NotFound from './pages/NotFound/NotFound';
13 | import Numerology from './pages/Numerology/Numerology';
14 |
15 | import './App.css';
16 |
17 | type Theme = 'light' | 'dark';
18 | type ThemeContext = { theme: Theme; toggleTheme: () => void };
19 |
20 | export const ThemeContext = createContext({} as ThemeContext);
21 |
22 | function App() {
23 | const [theme, setTheme] = useState('light');
24 |
25 | const toggleTheme = () => {
26 | setTheme(theme === 'light' ? 'dark' : 'light');
27 | };
28 |
29 | document.documentElement.setAttribute('data-theme', theme);
30 |
31 | return (
32 |
33 |
34 |
35 | } />
36 | } />
37 | } />
38 | } />
39 | } />
40 | } />
41 | } />
42 |
43 |
44 |
45 | );
46 | }
47 |
48 | export default App;
49 |
--------------------------------------------------------------------------------
/src/pages/Numerology/Numerology.css:
--------------------------------------------------------------------------------
1 | .numerology {
2 | min-height: 100vh;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: flex-start;
6 | align-items: center;
7 | gap: 1rem;
8 | margin-bottom: 20%;
9 | min-height: 100vh;
10 | }
11 |
12 | /* .glow {
13 | color: #000000;
14 | text-align: center;
15 | text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #ffffff, 0 0 40px #ffffff,
16 | 0 0 50px #ffffff, 0 0 60px #ffffff, 0 0 70px #ffffff;
17 | } */
18 |
19 | .numerology__paragraph {
20 | text-align: start;
21 | width: 90%;
22 | }
23 |
24 | .numerology__form {
25 | display: flex;
26 | flex-direction: column;
27 | justify-items: center;
28 | margin: auto;
29 | border: 2px solid var(--primary-foreground);
30 | padding: var(--space-10);
31 | gap: var(--space-05);
32 | margin-top: var(--space-20);
33 | }
34 |
35 | .numerology__form label {
36 | font-weight: var(--medium-weight);
37 | }
38 |
39 | .numerology__form input {
40 | border: 2px solid var(--primary-foreground);
41 | padding: var(--space-05);
42 | background-color: var(--primary-background);
43 | color: var(--primary-foreground);
44 | }
45 |
46 | .info--numerology {
47 | padding-top: var(--space-20);
48 | width: 80%;
49 | margin: auto;
50 | display: flex;
51 | flex-direction: column;
52 | justify-content: center;
53 | text-align: start;
54 | }
55 |
56 | .info--numerology h3 {
57 | padding-bottom: var(--space-10);
58 | text-align: left;
59 | }
60 |
61 | @media (min-width: 768px) {
62 | .numerology__paragraph {
63 | width: 50%;
64 | }
65 |
66 | .info--numerology {
67 | width: 50%;
68 | }
69 | }
70 |
71 | @media (min-width: 992px) {
72 | }
73 |
74 | @media (min-width: 1200px) {
75 | }
76 |
77 | @media (min-width: 1400px) {
78 | }
79 |
--------------------------------------------------------------------------------
/src/data/accordionData.ts:
--------------------------------------------------------------------------------
1 | export const accordionData = [
2 | {
3 | title: 'Project',
4 | content: `This is my final project for the FooCoding program held in Malmö between February and October 2023.
5 | The application I developed focuses on astrology, tarot and numerology with the help of external and internal data
6 | simultaneously.`,
7 | },
8 | {
9 | title: 'Author',
10 | content: `My name is Dana Xenia Marasca and I am an Italian full-stack developer based in Malmö.`,
11 | },
12 | {
13 | title: 'Technologies',
14 | content: `The technologies used in this project are React, Typescript and CSS.`,
15 | },
16 | {
17 | title: 'Requirements',
18 | content: `The application needs to meet several requirements. Each week, new topics must be implemented.
19 | Applications must be deployed at the end. No external package is allowed other than React and React Router
20 | Dom. The application must fetch data through an API. The application needs to be fully
21 | functional on the deployed version.`,
22 | },
23 | {
24 | title: 'Checkpoints',
25 | content: `The weekly checkpoints help to build the application on a consistent basis. The following
26 | checkpoints need to be met: inclusion of type-safe components, hooks(useState, useEffect, useRef, useCallback, useMemo),
27 | DOM events, conditional rendering and list rendering, react-router-dom(and navigation between at least two pages), deployment.`,
28 | },
29 | {
30 | title: 'Inspiration and Credits',
31 | content: `A few websites inspired the creation of this application. To name the most prevalent ones, Co-star app,
32 | Horoscope.com and Astrology.com. Credits to JPS Tarot API and @themuuln on Github for the data used in this website.`,
33 | },
34 | ];
35 |
--------------------------------------------------------------------------------
/src/styles/global/typography-variables.css:
--------------------------------------------------------------------------------
1 | /*------------ FONT IMPORTS ------------*/
2 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap');
3 |
4 | @font-face {
5 | font-family: 'Redro';
6 | src: url('../../assets/fonts/Redro.ttf') format('truetype');
7 | }
8 |
9 | @font-face {
10 | font-family: 'Akira';
11 | src: url('../../assets/fonts/Akira.ttf') format('truetype');
12 | }
13 |
14 | /*------------ VARIABLES ------------*/
15 |
16 | :root {
17 | /* font-weight */
18 | --light-weight: 300;
19 | --regular-weight: 400;
20 | --medium-weight: 500;
21 | --bold-weight: 600;
22 |
23 | /* font-family */
24 | --body-font: 'Inter', sans-serif;
25 | --title-font: 'Redro';
26 | --highlight-font: 'Akira';
27 |
28 | /* font-size */
29 | --small-size: 1rem;
30 | --regular-size: 1.2rem;
31 | --medium-size: 1.7rem;
32 | --large-size: 3.5rem;
33 | --x-large-size: 6rem;
34 | }
35 |
36 | /* MEDIA QUERIES */
37 |
38 | @media (min-width: 768px) {
39 | :root {
40 | --small-size: 0.6rem;
41 | --regular-size: 1rem;
42 | --medium-size: 1.4rem;
43 | --large-size: 5rem;
44 | --x-large-size: 8rem;
45 | }
46 | }
47 |
48 | @media (min-width: 992px) {
49 | :root {
50 | --small-size: 0.8rem;
51 | --regular-size: 1rem;
52 | --medium-size: 1.5rem;
53 | --large-size: 5rem;
54 | --x-large-size: 7rem;
55 | }
56 | }
57 |
58 | @media (min-width: 1400px) {
59 | :root {
60 | --small-size: 0.8rem;
61 | --regular-size: 1.2rem;
62 | --medium-size: 1.6rem;
63 | --large-size: 5rem;
64 | --x-large-size: 9rem;
65 | }
66 | }
67 |
68 | @media (min-width: 2560px) {
69 | :root {
70 | --small-size: 1.3rem;
71 | --regular-size: 2rem;
72 | --medium-size: 2.5rem;
73 | --large-size: 6rem;
74 | --x-large-size: 10rem;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/Loading/Loading.css:
--------------------------------------------------------------------------------
1 | .loading__wrapper {
2 | height: 100vh;
3 | display: flex;
4 | justify-content: center;
5 | }
6 |
7 | .lds-roller {
8 | display: inline-block;
9 | width: 80px;
10 | height: 80px;
11 | }
12 | .lds-roller div {
13 | animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
14 | transform-origin: 40px 40px;
15 | }
16 | .lds-roller div:after {
17 | content: ' ';
18 | display: block;
19 | position: absolute;
20 | width: 7px;
21 | height: 7px;
22 | border-radius: 50%;
23 | background: var(--primary-foreground);
24 | margin: -4px 0 0 -4px;
25 | }
26 | .lds-roller div:nth-child(1) {
27 | animation-delay: -0.036s;
28 | }
29 | .lds-roller div:nth-child(1):after {
30 | top: 63px;
31 | left: 63px;
32 | }
33 | .lds-roller div:nth-child(2) {
34 | animation-delay: -0.072s;
35 | }
36 | .lds-roller div:nth-child(2):after {
37 | top: 68px;
38 | left: 56px;
39 | }
40 | .lds-roller div:nth-child(3) {
41 | animation-delay: -0.108s;
42 | }
43 | .lds-roller div:nth-child(3):after {
44 | top: 71px;
45 | left: 48px;
46 | }
47 | .lds-roller div:nth-child(4) {
48 | animation-delay: -0.144s;
49 | }
50 | .lds-roller div:nth-child(4):after {
51 | top: 72px;
52 | left: 40px;
53 | }
54 | .lds-roller div:nth-child(5) {
55 | animation-delay: -0.18s;
56 | }
57 | .lds-roller div:nth-child(5):after {
58 | top: 71px;
59 | left: 32px;
60 | }
61 | .lds-roller div:nth-child(6) {
62 | animation-delay: -0.216s;
63 | }
64 | .lds-roller div:nth-child(6):after {
65 | top: 68px;
66 | left: 24px;
67 | }
68 | .lds-roller div:nth-child(7) {
69 | animation-delay: -0.252s;
70 | }
71 | .lds-roller div:nth-child(7):after {
72 | top: 63px;
73 | left: 17px;
74 | }
75 | .lds-roller div:nth-child(8) {
76 | animation-delay: -0.288s;
77 | }
78 | .lds-roller div:nth-child(8):after {
79 | top: 56px;
80 | left: 12px;
81 | }
82 |
83 |
--------------------------------------------------------------------------------
/src/components/FlipCard/FlipCard.css:
--------------------------------------------------------------------------------
1 | .flip-card__img {
2 | height: 25rem;
3 | display: block;
4 | margin: auto;
5 | margin-top: 0.2rem;
6 | }
7 |
8 | .flip-card {
9 | background-color: transparent;
10 | width: 15rem;
11 | height: 25.5rem;
12 | perspective: 1000px;
13 | margin: auto;
14 | }
15 |
16 | .flip-card__inner {
17 | position: relative;
18 | height: 100%;
19 | text-align: center;
20 | transition: transform 0.8s;
21 | transform-style: preserve-3d;
22 | }
23 |
24 | .flip-card:hover .flip-card__inner {
25 | transform: rotateY(180deg);
26 | }
27 |
28 | .flip-card__front,
29 | .flip-card__back {
30 | position: absolute;
31 | width: 100%;
32 | height: 100%;
33 | -webkit-backface-visibility: hidden;
34 | backface-visibility: hidden;
35 | border: 2px solid black;
36 | }
37 |
38 | .flip-card__front {
39 | background-color: white;
40 | color: black;
41 | }
42 |
43 | .flip-card__back {
44 | background-color: var(--primary-background);
45 | color: var(--primary-foreground);
46 | border: 2px solid var(--primary-foreground);
47 | transform: rotateY(180deg);
48 | padding: var(--space-15);
49 | font-size: var(--small-size);
50 | }
51 |
52 | .flip-card__name {
53 | font-size: var(--small-size);
54 | padding-bottom: 1rem;
55 | }
56 |
57 | .flip-card__info {
58 | text-align: left;
59 | padding-bottom: 2rem;
60 | }
61 |
62 | .flip-card__info p {
63 | margin-bottom: 0.5rem;
64 | font-size: 1.1rem;
65 | }
66 |
67 | @media (min-width: 640px) {
68 | .flip-card__name {
69 | font-size: var(--regular-size);
70 | }
71 |
72 | .flip-card__info {
73 | font-size: var(--regular-size);
74 | }
75 |
76 | .flip-card__button {
77 | font-size: var(--regular-size);
78 | }
79 | }
80 |
81 | @media (min-width: 920px) {
82 | .flip-card__name {
83 | font-size: var(--regular-size);
84 | }
85 | }
86 |
87 | @media (min-width: 2560px) {
88 | .flip-card__name,
89 | .flip-card__info,
90 | .flip-card__button {
91 | font-size: 1rem;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/SubscribeForm/SubscribeForm.tsx:
--------------------------------------------------------------------------------
1 | import { ChangeEvent, FormEvent, useState } from 'react';
2 |
3 | import Button from '../Button/Button';
4 |
5 | import './SubscribeForm.css';
6 |
7 | interface FormProps {
8 | onSubmit: (data: FormData) => void;
9 | }
10 |
11 | export interface FormData {
12 | firstName: string;
13 | birthday: Date | string;
14 | email: string;
15 | }
16 |
17 | export const SubscribeForm = ({ onSubmit }: FormProps) => {
18 | const [formData, setFormData] = useState({
19 | firstName: '',
20 | birthday: '',
21 | email: '',
22 | });
23 |
24 | const initialFormData: FormData = {
25 | firstName: '',
26 | birthday: '',
27 | email: '',
28 | };
29 |
30 | const handleInputChange = (event: ChangeEvent) => {
31 | const { name, value } = event.target;
32 |
33 | setFormData((prevFormData) => ({ ...prevFormData, [name]: value }));
34 | };
35 |
36 | const handleSubmit = (event: FormEvent) => {
37 | event.preventDefault();
38 |
39 | if (!formData.firstName || !formData.birthday || !formData.email) {
40 | alert('Please fill in all fields.');
41 | return;
42 | }
43 |
44 | setFormData(initialFormData);
45 |
46 | onSubmit(formData);
47 | };
48 |
49 | return (
50 |
80 | );
81 | };
82 |
--------------------------------------------------------------------------------
/src/pages/ZodiacSignDetails/ZodiacSignDetails.tsx:
--------------------------------------------------------------------------------
1 | import { useParams, useNavigate } from 'react-router-dom';
2 |
3 | import snake from '../../assets/images/snake.png';
4 | import useFetch from '../../hooks/useFetch';
5 | import Button from '../../components/Button/Button.tsx';
6 | import Loading from '../../components/Loading/Loading.tsx';
7 | import { ZodiacProps } from '../../types/Zodiac.types.ts';
8 | import './ZodiacSignDetails.css';
9 |
10 | function ZodiacSignDetails() {
11 | const { id } = useParams();
12 | const url = `https://jps-tarot-api.azurewebsites.net/api/Zodiac/GetById/${id}`;
13 |
14 | const { data: sign, error, loading } = useFetch(url);
15 |
16 |
17 | if (error) {
18 | console.log(`Error: ${error.message}`);
19 | }
20 |
21 | const navigate = useNavigate();
22 |
23 | return (
24 |
25 | {error && {error?.message}
}
26 | {loading && }
27 |
28 |
29 |
30 |
{sign?.name}
31 |
32 |
33 |
{sign?.dates}
34 |
35 |
36 |
37 | Ruler:
38 | {sign?.ruler.name}
39 |
40 |
41 |
42 | Transition consistency: {sign?.ruler.transition}.
43 |
44 |
45 | Relevant keywords of the planet: {' '}
46 | {sign?.ruler.keywords?.join(', ')}.
47 |
48 |
49 |
50 | Sign's Positive Traits {sign?.positiveTraits?.join(', ')}
51 |
52 |
53 | Sign's Negative Traits {sign?.negativeTraits?.join(', ')}
54 |
55 |
56 |
57 |
navigate(-1)}
60 | >
61 | Go back to Zodiac Page
62 |
63 |
64 |
65 | );
66 | }
67 |
68 | export default ZodiacSignDetails;
69 |
--------------------------------------------------------------------------------
/src/components/ImageCarousel/ImageCarousel.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 |
3 | import carouselOne from '../../assets/images/carousel-1.gif';
4 | import carouselTwo from '../../assets/images/carousel-2.gif';
5 | import carouselThree from '../../assets/images/carousel-3.gif';
6 |
7 | import './ImageCarousel.css';
8 |
9 | interface CarouselProps {
10 | lifePathNumber: number;
11 | expressionNumber: number;
12 | soulUrgeNumber: number;
13 | }
14 |
15 | export default function Carousel({
16 | lifePathNumber,
17 | expressionNumber,
18 | soulUrgeNumber,
19 | }: CarouselProps) {
20 | const listRef = useRef(null);
21 |
22 | function scrollToIndex(index: number) {
23 | if (listRef.current) {
24 | const imgNodes = listRef.current.querySelectorAll('li > img');
25 | if (imgNodes.length > index) {
26 | imgNodes[index].scrollIntoView({
27 | behavior: 'smooth',
28 | block: 'nearest',
29 | inline: 'center',
30 | });
31 | }
32 | }
33 | }
34 |
35 | return (
36 |
37 |
38 | Your results are ready. Press the buttons below to discover them:
39 |
40 |
41 | {/* NOTE: OnClickButton component is not used below to improve UX */}
42 | scrollToIndex(0)}>Life Path Number
43 | scrollToIndex(1)}>Expression Number
44 | scrollToIndex(2)}>Soul Urge Number
45 |
46 |
47 |
48 |
49 | {lifePathNumber !== 0 && (
50 |
51 | Your life path number is: {lifePathNumber}
52 |
53 | )}
54 |
55 |
56 |
57 | {expressionNumber !== 0 && (
58 |
59 | Your expression number is: {expressionNumber}
60 |
61 | )}
62 |
63 |
64 |
65 | {soulUrgeNumber !== 0 && (
66 |
67 | Your soul urge number is: {soulUrgeNumber}
68 |
69 | )}
70 |
71 |
72 |
73 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/CardReading/CardReading.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | import useFetch from '../../hooks/useFetch';
4 | import Button from '../Button/Button';
5 | import Loading from '../../components/Loading/Loading';
6 | import MagicCard from '../../components/MagicCard/MagicCard';
7 | import { TarotProps } from '../../types/Tarot.types.ts';
8 |
9 | import './CardReading.css';
10 |
11 | export default function CardReading() {
12 | const url = 'https://jps-tarot-api.azurewebsites.net/api/Tarot/Get';
13 | const { data: allCards, loading, error } = useFetch(url);
14 |
15 | const [selectedCards, setSelectedCards] = useState(null);
16 |
17 | useEffect(() => {
18 | if (allCards) {
19 | const randomIndexes = getRandomIndexes(allCards.length, 3);
20 | const randomCards = randomIndexes.map((index) => allCards[index]);
21 | setSelectedCards(randomCards);
22 | }
23 | }, [allCards]);
24 |
25 | const handleRerenderClick = () => {
26 | if (allCards) {
27 | const remainingCards = allCards.filter(
28 | (card) => !selectedCards?.some((selected) => selected.id === card.id)
29 | );
30 | const randomIndexes = getRandomIndexes(remainingCards.length, 3);
31 | const randomCards = randomIndexes.map((index) => remainingCards[index]);
32 | setSelectedCards(randomCards);
33 | }
34 | };
35 |
36 | if (error) {
37 | console.log(`Error: ${error.message}`);
38 | }
39 |
40 | return (
41 |
42 |
Today's Reading
43 |
44 | The history of tarot card reading is marked by a blend of traditional
45 | playing card origins and the overlay of esoteric and mystical
46 | interpretations. Today, tarot cards are used by individuals for a wide
47 | range of purposes, including personal insight, meditation,
48 | self-discovery, and entertainment. The practice continues to evolve and
49 | adapt in response to changing beliefs and cultural contexts.
50 |
51 | {error &&
{error?.message}
}
52 | {loading ? (
53 |
54 | ) : (
55 |
56 | {selectedCards?.map((card) => (
57 |
58 | ))}
59 |
60 | )}
61 |
62 | Generate a new reading
63 |
64 |
65 | );
66 | }
67 |
68 | function getRandomIndexes(max: number, count: number): number[] {
69 | const indexes: number[] = [];
70 | while (indexes.length < count) {
71 | const randomIndex = Math.floor(Math.random() * max);
72 | if (!indexes.includes(randomIndex)) {
73 | indexes.push(randomIndex);
74 | }
75 | }
76 | return indexes;
77 | }
78 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LUNAR - Astrology, Tarot and Numerology App
2 |
3 | [React.js](https://react.dev/) web app that fetches data from [Tarot API](https://jps-tarot-api.azurewebsites.net/swagger/index.html). Internal data is also employed to a limited extent.
4 |
5 | Deployed version of [LUNAR](https://lunar-app.netlify.app/).
6 |
7 | ## Table of contents
8 |
9 | - [Overview](#overview)
10 | - [Built with](#built-with)
11 | - [Features](#features)
12 | - [Getting started](#getting-started)
13 | - [Print Screens](#print-screens)
14 | - [Conclusion](#conclusion)
15 |
16 | ## Overview
17 |
18 | Lunar is a user-friendly app featuring a responsive design that seamlessly transitions between light and dark modes. It's perfect for individuals who want to explore astrology-related subjects but are new to the field. Lunar offers a comprehensive zodiac sign library, a tarot card collection, personalized tarot card readings, and a numerology calculator based on birth date and name.
19 |
20 | ## Built with
21 |
22 | - React.js
23 | - TypeScript
24 | - CSS
25 | - Swagger UI
26 |
27 | ## Features
28 |
29 | - Data Fetching from Tarot API using Swagger UI allowing users to access a wide range of tarot-related information.
30 | - Integration with the React.js framework for delivering a fast and smooth user experience.
31 | - Intuitive and responsive user interface.
32 | - Light/Dark Mode Switch enhancing the application's accessibility.
33 | - Zodiac signs library with
34 | - Flip tarot cards library including filtering and search bar.
35 | - Individual tarot card readings, which can be regenerated onclick.
36 | - A tool that calculates numerology insights based on user input, such as age and name, expanding the app's offerings.
37 | - Utilization of internal data to perform numerical calculations.
38 |
39 | ## Getting started
40 |
41 | If you want to run the project locally, you need to make sure you have [Node.js](https://nodejs.org/it) installed on your device. Then, simply clone the repository, install dependencies and start the development server using the line commands listed below.
42 |
43 | ```
44 | git clone https://github.com/dxenia/astrology-app.git
45 | cd astrology-app
46 | npm install
47 | npm run dev
48 | ```
49 |
50 | ## Print Screens
51 |
52 | ### Light/Dark Mode Comparison
53 |
54 |
55 |
56 |
57 | ### Responsive design
58 |
59 | Mobile:
60 |
61 |
62 | Tablet:
63 |
64 |
65 | Laptop:
66 |
67 |
68 | ## Conclusion
69 |
70 | This application showcases the synergy of React.js and API integration, exemplifying the creation of a user-friendly and compelling digital experience. Its architecture allows for seamless scalability, making it a solid foundation for future enhancements and feature expansions.
71 |
--------------------------------------------------------------------------------
/src/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { NavLink, useLocation } from 'react-router-dom';
3 |
4 | import ThemeToggle from '../ThemeToggle/ThemeToggle';
5 |
6 | import './Header.css';
7 |
8 | export default function Header() {
9 | const [isOpen, setIsOpen] = useState(false);
10 |
11 | const location = useLocation();
12 |
13 | const closeMenu = () => {
14 | setIsOpen(!isOpen);
15 | };
16 |
17 | const openClose = isOpen ? 'open' : 'close';
18 | const hamburger = `line ${isOpen ? 'line-rotate' : ''}`;
19 |
20 | const scrollToTop = () => {
21 | window.scrollTo(0, 0);
22 | };
23 |
24 | useEffect(() => {
25 | scrollToTop();
26 | }, [location.pathname]);
27 |
28 | return (
29 |
30 |
31 |
32 |
33 | lunar
34 |
35 | {/*
*/}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
49 | Home
50 |
51 |
52 |
53 |
60 | About
61 |
62 |
63 |
64 |
71 | Zodiac Signs
72 |
73 |
74 |
75 |
82 | Tarots
83 |
84 |
85 |
86 |
93 | Numerology
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | );
103 | }
104 |
--------------------------------------------------------------------------------
/src/pages/Tarot/Tarot.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | import Button from '../../components/Button/Button.tsx';
4 | import useFetch from '../../hooks/useFetch';
5 | import FlipCard from '../../components/FlipCard/FlipCard.tsx';
6 | import Loading from '../../components/Loading/Loading.tsx';
7 | import { TarotProps } from '../../types/Tarot.types.ts';
8 | import { Pagination } from '../../components/Pagination/Pagination.tsx';
9 | import CardReading from '../../components/CardReading/CardReading.tsx';
10 |
11 | import '../Tarot/Tarot.css';
12 |
13 | export default function Tarot() {
14 | const [page, setPage] = useState(1);
15 | const [filterCriteria, setFilterCriteria] = useState('All');
16 | const [searchQuery, setSearchQuery] = useState('');
17 | const itemsPerPage = 12;
18 |
19 | const url = 'https://jps-tarot-api.azurewebsites.net/api/Tarot/GetShuffled';
20 | const { data: cards, loading, error } = useFetch(url);
21 |
22 | const filteredCards = cards?.filter((card) => {
23 | if (filterCriteria === 'All') {
24 | return card.name.toLowerCase().includes(searchQuery.toLowerCase());
25 | }
26 | return (
27 | card.type === filterCriteria &&
28 | card.name.toLowerCase().includes(searchQuery.toLowerCase())
29 | );
30 | });
31 |
32 | const totalFilteredPages = Math.ceil(
33 | (filteredCards?.length || 0) / itemsPerPage
34 | );
35 |
36 | const startIndex = (page - 1) * itemsPerPage;
37 | const endIndex = startIndex + itemsPerPage;
38 |
39 | const cardsToDisplay = filteredCards?.slice(startIndex, endIndex) || [];
40 |
41 | if (page < 1) {
42 | setPage(1);
43 | } else if (totalFilteredPages && page > totalFilteredPages) {
44 | setPage(totalFilteredPages);
45 | }
46 |
47 | // if (error) {
48 | // console.log(`Error: ${error.message}`);
49 | // }
50 |
51 | const handleFilterChange = (criteria: string) => {
52 | setFilterCriteria(criteria);
53 | setPage(1);
54 | };
55 |
56 | const handleSearch = (query: string) => {
57 | setSearchQuery(query);
58 | setPage(1);
59 | };
60 |
61 | return (
62 |
63 | Tarot Cards
64 | {error && {error?.message}
}
65 | {loading && }
66 |
67 | handleFilterChange('All')}>All Arcana
68 | handleFilterChange('Major')}>
69 | Major Arcana
70 |
71 | handleFilterChange('Minor')}>
72 | Minor Arcana
73 |
74 |
75 |
76 |
77 |
Are you looking for a specific card?
78 | Insert its name below.
79 | handleSearch(e.target.value)}
84 | className="search-input"
85 | />
86 |
87 |
88 |
89 | {cardsToDisplay.map((card) => (
90 |
91 | ))}
92 |
93 |
98 |
99 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/src/components/Header/Header.css:
--------------------------------------------------------------------------------
1 | header {
2 | background-color: var(--secondary-background);
3 | backdrop-filter: blur(5px);
4 | position: sticky;
5 | position: -webkit-sticky;
6 | top: 0;
7 | left: 0;
8 | right: 0;
9 | bottom: 0;
10 | z-index: 99999;
11 | }
12 |
13 | .nav a {
14 | font-weight: var(--bold-weight);
15 | letter-spacing: var(--space-02);
16 | text-decoration: none;
17 | }
18 |
19 | .nav {
20 | padding: var(--space-05);
21 | }
22 |
23 | .active:not(.logo) {
24 | background-color: var(--primary-foreground);
25 | color: var(--primary-background);
26 | }
27 |
28 | .nav__options {
29 | display: flex;
30 | flex-direction: row;
31 | justify-content: flex-start;
32 | align-items: center;
33 | gap: var(--space-10);
34 | }
35 |
36 | nav,
37 | .nav__list {
38 | display: flex;
39 | flex-direction: row;
40 | justify-content: space-between;
41 | align-items: center;
42 | }
43 |
44 | .logo {
45 | font-family: var(--title-font);
46 | font-size: var(--medium-size);
47 | padding-left: var(--space-10);
48 | color: var(--primary-foreground);
49 | }
50 |
51 | .nav__list {
52 | gap: var(--space-10);
53 | padding-right: var(--space-05);
54 | }
55 |
56 | .nav__item a {
57 | text-transform: uppercase;
58 | font-size: var(--small-size);
59 | color: var(--primary-foreground);
60 | padding: var(--space-02);
61 | padding-left: var(--space-04);
62 | }
63 |
64 | .nav__item a:hover {
65 | transition: ease-in-out var(--duration-standard);
66 | background-color: var(--primary-foreground);
67 | color: var(--primary-background);
68 | }
69 |
70 | /*------------ hamburger navigation ------------*/
71 |
72 | @media (max-width: 767px) {
73 | header {
74 | height: 73px;
75 | }
76 |
77 | .nav__options {
78 | flex-direction: column;
79 | justify-items: center;
80 | gap: 0;
81 | }
82 |
83 | .logo {
84 | padding-left: 0;
85 | }
86 |
87 | .line {
88 | display: block;
89 | width: 28px;
90 | height: 4px;
91 | margin: var(--space-02) auto;
92 | -webkit-transition: all var(--duration-slow) ease-in-out;
93 | transition: all var(--duration-slow) ease-in-out;
94 | background-color: var(--primary-foreground);
95 | }
96 |
97 | .line-rotate:nth-child(1) {
98 | transform: translateY(10px) rotate(45deg);
99 | }
100 |
101 | .line-rotate:nth-child(2) {
102 | opacity: 0;
103 | }
104 |
105 | .line-rotate:nth-child(3) {
106 | transform: translateY(-11px) rotate(-45deg);
107 | }
108 |
109 | nav .menu {
110 | display: flex;
111 | position: absolute;
112 | top: 20px;
113 | right: 20px;
114 | flex-direction: column;
115 | justify-content: flex-end;
116 | align-items: center;
117 | width: var(--space-20);
118 | height: var(--space-20);
119 | }
120 |
121 | nav {
122 | flex-direction: column;
123 | align-items: center;
124 | }
125 |
126 | .logo {
127 | padding-top: var(--space-10);
128 | }
129 |
130 | .nav__list {
131 | opacity: 0;
132 | visibility: hidden;
133 | display: flex;
134 | flex-direction: column;
135 | justify-content: flex-start;
136 | align-items: flex-end;
137 | width: 100vw;
138 | margin-top: 17.5px;
139 | padding-top: var(--space-20);
140 | height: 100vh;
141 | gap: var(--space-20);
142 | background-color: var(--primary-background);
143 | transform: translateX(-200%);
144 | transition: transform var(--duration-slowest) ease-in-out,
145 | opacity var(--duration-slowest) ease-in-out,
146 | visibility var(--duration-slowest) ease-in-out;
147 | }
148 |
149 | .nav__list.open {
150 | opacity: 1;
151 | visibility: visible;
152 | transform: translateX(0);
153 | }
154 |
155 | .nav__list.close {
156 | margin-top: 65px;
157 | position: absolute;
158 | }
159 |
160 | .nav__item {
161 | padding-right: var(--space-10);
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/pages/Home/Home.tsx:
--------------------------------------------------------------------------------
1 | import zodiacWheel from '../../assets/images/zodiac-wheel.png';
2 | import moonPhases from '../../assets/images/moon-phases.png';
3 | import butterfly from '../../assets/images/butterfly.png';
4 | import butterflyTwo from '../../assets/images/butterfly-2.png';
5 | import moth from '../../assets/images/moth.png';
6 |
7 | import Button from '../../components/Button/Button';
8 | import {
9 | FormData,
10 | SubscribeForm,
11 | } from '../../components/SubscribeForm/SubscribeForm';
12 |
13 | import './Home.css';
14 |
15 | export default function Home() {
16 | const handleSubmit = (formData: FormData) => {
17 | alert(`Your subscription request has been submitted!
18 |
19 | Your Data:
20 | First Name: ${formData.firstName}
21 | Birthday: ${formData.birthday}
22 | Email: ${formData.email}
23 | `);
24 | };
25 |
26 | return (
27 |
28 |
29 |
30 |
lunar☾
31 |
Astrology in your pocket.
32 |
33 | Welcome to the mystical world of Lunar, your gateway to unlocking
34 | the secrets of the cosmos and discovering the hidden truths that lie
35 | within the Tarot cards.
36 |
37 |
44 |
45 |
50 |
51 |
52 |
53 | What can you find on Lunar?
54 |
55 |
56 |
57 | Zodiac Signs
58 |
59 | Dive into the cosmic mysteries of astrology on our website.
60 | Explore your birth chart, discover the secrets of your sun, moon,
61 | and rising signs, and gain insights into your life's path based on
62 | the movements of celestial bodies. Unlock the power of the stars
63 | and navigate your destiny with our expert astrologers.
64 |
65 |
66 |
67 |
68 | Tarot Cards
69 |
70 | Unveil the enigmatic world of tarot on our website. Experience the
71 | art of divination through tarot card readings, learn the symbolism
72 | behind each card, and receive guidance on love, career, and
73 | spirituality. Our skilled tarot readers are here to help you
74 | uncover the hidden wisdom within the cards.
75 |
76 |
77 |
78 |
79 | Numerology
80 |
81 | Unlock the mystical significance of numbers with our numerology
82 | resources. Explore the vibrations and energies associated with
83 | your name and birthdate, delve into the secrets of numerological
84 | calculations, and gain profound insights into your life's purpose.
85 | Join us on this journey of self-discovery.
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | Curious about your zodiac sign?
97 |
98 |
99 | Sign up for our newsletter!
100 |
101 |
102 |
103 |
104 |
105 |
106 | );
107 | }
108 |
--------------------------------------------------------------------------------
/src/pages/Home/Home.css:
--------------------------------------------------------------------------------
1 | /* first section */
2 |
3 | .home {
4 | min-height: 100vh;
5 | }
6 |
7 | .home__logo {
8 | font-size: var(--x-large-size);
9 | line-height: 0;
10 | }
11 |
12 | .home__first-section {
13 | display: flex;
14 | flex-direction: column;
15 | justify-content: flex-start;
16 | align-items: center;
17 | padding: 10% 0;
18 | gap: var(--space-10);
19 | text-align: start;
20 | }
21 |
22 | .home__info {
23 | width: 90%;
24 | display: flex;
25 | flex-direction: column;
26 | justify-content: center;
27 | align-items: center;
28 | margin: auto;
29 | }
30 |
31 | .home__heading {
32 | margin: auto;
33 | width: 90%;
34 | padding-bottom: var(--space-10);
35 | }
36 |
37 | .home__subheading {
38 | font-weight: var(--regular-weight);
39 | font-size: var(--medium-size);
40 | width: 90%;
41 | padding-top: var(--space-10);
42 | padding-bottom: var(--space-10);
43 | }
44 |
45 | .home__img--first-section {
46 | width: 17rem;
47 | height: 17rem;
48 | animation: rotation 20s infinite linear;
49 | background: var(--quaternary-foreground);
50 | background: radial-gradient(
51 | circle,
52 | var(--quaternary-foreground),
53 | rgba(255, 255, 255, 0) 50%
54 | );
55 | }
56 |
57 | html[data-theme='dark'] .home__img--first-section {
58 | background: none;
59 | }
60 |
61 | .home__button {
62 | margin: var(--space-10) auto;
63 | font-size: 1.2rem;
64 | }
65 |
66 | .home__button:hover {
67 | background-color: var(--quaternary-foreground);
68 | }
69 |
70 | /* second section */
71 |
72 | .home__second-section {
73 | display: flex;
74 | flex-direction: column;
75 | justify-content: center;
76 | align-items: center;
77 | margin: 20% auto;
78 | width: 85%;
79 | }
80 |
81 | .home__second-section h2 {
82 | text-align: center;
83 | }
84 |
85 | .home__img--second-section {
86 | width: 70%;
87 | }
88 |
89 | .home__list {
90 | display: flex;
91 | flex-direction: column;
92 | align-items: center;
93 | justify-content: center;
94 | gap: 5%;
95 | text-align: start;
96 | font-weight: var(--regular-weight);
97 | margin: auto;
98 | }
99 |
100 | .home__list-item {
101 | display: flex;
102 | flex-direction: column;
103 | gap: var(--space-10);
104 | align-items: center;
105 | text-align: start;
106 | }
107 |
108 | .home__icon {
109 | height: 10rem;
110 | margin-bottom: var(--space-10);
111 | margin-top: var(--space-20);
112 | }
113 |
114 | .home__icon:hover {
115 | animation: skew-y-shaking 0.3s infinite;
116 | }
117 |
118 | /* subscribe form */
119 |
120 | .home__third-section {
121 | background: var(--tertiary-background);
122 | margin: var(--space-40) auto;
123 | }
124 |
125 | .home__form-wrapper {
126 | display: flex;
127 | justify-content: center;
128 | align-content: center;
129 | flex-direction: column;
130 | margin: auto;
131 | gap: var(--space-20);
132 | padding: var(--space-20);
133 | width: 20rem;
134 | color: var(--tertiary-foreground);
135 | }
136 |
137 | .home__sub-heading--third-section {
138 | font-size: var(--regular-size);
139 | }
140 |
141 | /* dark mode & media queries */
142 |
143 | html[data-theme='dark'] .home__list-item:nth-child(3) > .home__icon {
144 | opacity: 75%;
145 | }
146 |
147 | @media (min-width: 768px) {
148 | .home__first-section {
149 | width: 80%;
150 | margin: auto;
151 | }
152 |
153 | .home__img--first-section {
154 | width: 20rem;
155 | height: 20rem;
156 | }
157 |
158 | .home__list {
159 | flex-direction: row;
160 | }
161 |
162 | .home__list-item {
163 | width: 30%;
164 | }
165 |
166 | .home__form-wrapper {
167 | flex-direction: row;
168 | align-items: center;
169 | gap: var(--space-20);
170 | }
171 |
172 | .home__second-section {
173 | margin: 10% auto;
174 | }
175 | }
176 |
177 | @media (min-width: 992px) {
178 | .home__first-section {
179 | flex-direction: row;
180 | padding: 5%;
181 | width: 80%;
182 | }
183 |
184 | .home__img--first-section {
185 | width: 25rem;
186 | height: 25rem;
187 | }
188 |
189 | .home__form-wrapper {
190 | gap: 10rem;
191 | }
192 |
193 | .home__form-wrapper article h2 {
194 | width: 20rem;
195 | }
196 |
197 | .home__second-section {
198 | margin: 5% auto;
199 | }
200 | }
201 |
202 | @media (min-width: 2560px) {
203 | .home__img--first-section {
204 | width: 50%;
205 | height: 50%;
206 | }
207 |
208 | .home__icon {
209 | height: 20rem;
210 | margin-bottom: var(--space-10);
211 | margin-top: var(--space-20);
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/src/pages/Numerology/Numerology.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useMemo } from 'react';
2 |
3 | import Button from '../../components/Button/Button';
4 | import ImageCarousel from '../../components/ImageCarousel/ImageCarousel';
5 | import { expressionLetters, soulUrgeLetters } from '../../data/lettersData';
6 | import { lifePathData } from '../../data/lifePathData';
7 | import { LifePathProps } from './Numerology.types';
8 |
9 | import './Numerology.css';
10 |
11 | export default function Numerology() {
12 | const [dateOfBirth, setDateOfBirth] = useState('');
13 | const [fullName, setFullName] = useState('');
14 | const [lifePathNumber, setLifePathNumber] = useState(0);
15 | const [expressionNumber, setExpressionNumber] = useState(0);
16 | const [soulUrgeNumber, setSoulUrgeNumber] = useState(0);
17 | const [showCarousel, setShowCarousel] = useState(false);
18 |
19 | const calculateLifePathNumber = (): void => {
20 | if (!fullName || !dateOfBirth) {
21 | alert('Please fill in all fields.');
22 | return;
23 | } else {
24 | const cleanedDateOfBirth = dateOfBirth.replace(/\D/g, '');
25 |
26 | let sum = 0;
27 | for (let i = 0; i < cleanedDateOfBirth.length; i++) {
28 | sum += parseInt(cleanedDateOfBirth[i]);
29 | }
30 |
31 | while (sum > 9 && sum !== 11 && sum !== 22 && sum !== 33) {
32 | sum = Math.floor(sum / 10) + (sum % 10);
33 | }
34 |
35 | setLifePathNumber(sum);
36 |
37 | calculateExpressionNumber(fullName);
38 | calculateSoulUrgeNumber(fullName);
39 | setShowCarousel(true);
40 | }
41 | };
42 |
43 | const calculateExpressionNumber = useMemo(() => {
44 | return (name: string): void => {
45 | let expressionNumber = 0;
46 | const nameWithoutSpaces = name.replace(/\s/g, '');
47 |
48 | for (const char of nameWithoutSpaces) {
49 | const uppercaseChar = char.toUpperCase();
50 |
51 | // eslint-disable-next-line no-prototype-builtins
52 | if (expressionLetters.hasOwnProperty(uppercaseChar)) {
53 | expressionNumber += expressionLetters[uppercaseChar];
54 | }
55 |
56 | while (
57 | expressionNumber > 9 &&
58 | expressionNumber !== 11 &&
59 | expressionNumber !== 22 &&
60 | expressionNumber !== 33
61 | ) {
62 | expressionNumber =
63 | Math.floor(expressionNumber / 10) + (expressionNumber % 10);
64 | }
65 | }
66 |
67 | setExpressionNumber(expressionNumber);
68 | };
69 | }, []);
70 |
71 | // !IMPORTANT! This is a unnecessary implementation of the useMemo hook, only included as a assignment requirement but not useful in a real-case scenario.
72 | const calculateSoulUrgeNumber = useMemo(() => {
73 | return (name: string): void => {
74 | let soulUrgeNumber = 0;
75 | const nameWithoutSpaces = name.replace(/\s/g, '');
76 |
77 | for (const char of nameWithoutSpaces) {
78 | const uppercaseChar = char.toUpperCase();
79 |
80 | // eslint-disable-next-line no-prototype-builtins
81 | if (soulUrgeLetters.hasOwnProperty(uppercaseChar)) {
82 | soulUrgeNumber += soulUrgeLetters[uppercaseChar];
83 | }
84 |
85 | while (
86 | soulUrgeNumber > 9 &&
87 | soulUrgeNumber !== 11 &&
88 | soulUrgeNumber !== 22 &&
89 | soulUrgeNumber !== 33
90 | ) {
91 | soulUrgeNumber =
92 | Math.floor(soulUrgeNumber / 10) + (soulUrgeNumber % 10);
93 | }
94 | }
95 |
96 | setSoulUrgeNumber(soulUrgeNumber);
97 | };
98 | }, []);
99 |
100 | const items: LifePathProps[] = lifePathData;
101 | const selectedItem = items.find((item) => item.number === lifePathNumber);
102 |
103 | return (
104 |
105 | Numerology Calculator
106 |
107 | Numerology has a rich and diverse history that spans millennia. It has
108 | evolved from ancient mystical beliefs to become a contemporary practice
109 | that some people use for self-reflection and guidance.
110 |
111 |
112 | Here you can calculate your numerology in one-click. Insert birth date
113 | and full name to discover your Life Path Number, Expression Number and
114 | Soul Urge Number.
115 |
116 |
117 |
137 |
138 |
139 | Calculate
140 |
141 |
142 | {showCarousel && (
143 |
148 | )}
149 | {selectedItem && (
150 |
151 |
{selectedItem.title}
152 |
{selectedItem.description}
153 |
154 | )}
155 |
156 |
157 | );
158 | }
159 |
--------------------------------------------------------------------------------
/src/assets/images/download-android.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
14 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
31 |
32 |
33 |
54 |
55 |
57 |
59 |
60 |
61 |
62 |
64 |
65 |
66 |
68 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/src/data/lifePathData.ts:
--------------------------------------------------------------------------------
1 | // Credits to @themuuln on Github for the API data and formulas
2 |
3 | export const lifePathData = [
4 | {
5 | number: 1,
6 | title: 'A Primal Force; if you have a 1 Life Path, you are a born leader.',
7 | description:
8 | "You insist on your right to make up your own mind; you demand freedom of thought and action. You have drive and determination. You don't let anything or anyone stand in your way once you are committed to your goal. You assume the responsibility to be the protector and provider for those you love.",
9 | url: 'https://www.worldnumerology.com/numerology-life-path/life-path-number-1/',
10 | },
11 | {
12 | number: 2,
13 | title:
14 | 'If you have a Life Path 2, you are the Peace Maker, with the soul of an artist.',
15 | description:
16 | 'You are extremely sensitive, perceptive, and a bit shy. These qualities are both your strengths and weaknesses, for while you possess enormous sensitivity to your feelings and those of others, that same sensitivity can cause you to hold back and repress your considerable talents.',
17 | url: 'https://www.worldnumerology.com/numerology-life-path/life-path-number-2/',
18 | },
19 | {
20 | number: 3,
21 | title:
22 | 'Those with a Life Path 3 possess a great talent for creativity and self expression.',
23 | description:
24 | 'Many writers, poets, actors and musicians are born under the 3 Life Path. You are witty, possess a gift for gab, and savor the limelight.',
25 | url: 'https://www.worldnumerology.com/numerology-life-path/life-path-number-3/',
26 | },
27 | {
28 | number: 4,
29 | title:
30 | 'You are practical, down to earth with strong ideas about right and wrong.',
31 | description:
32 | 'You are orderly and organized, systematic and controlled, you are decisive and methodical employing a step by step rational approach to problems solving. Once committed you do not give up easily!',
33 | url: 'https://www.worldnumerology.com/numerology-life-path/life-path-number-3/',
34 | },
35 | {
36 | number: 5,
37 | title: 'You are a Dynamic Force; the key to your personality is freedom.',
38 | description:
39 | 'You love travel, adventure, variety and meeting new people. You possess the curiosity of a cat and long to experience all of life. You like to be involved in several things at the same time as long as you are not tied down to any one area. You like change, new things and new horizons. You make friends easily, your personality is upbeat and often inspiring attracting people from all walks of life.',
40 | url: 'https://www.worldnumerology.com/numerology-life-path/life-path-number-5/',
41 | },
42 | {
43 | number: 6,
44 | title:
45 | 'A Life Path 6 possesses great compassion and seeks to be of service to others.',
46 | description:
47 | 'You have concern for the weak and the downtrodden. You are a healer and a helper to others. You are capable of giving comfort to those in need and will frequently offer a shoulder for others to cry on.',
48 | url: 'https://www.worldnumerology.com/numerology-life-path/life-path-number-6/',
49 | },
50 | {
51 | number: 7,
52 | title: 'A 7 Life Path is a searcher and a seeker of the truth.',
53 | description:
54 | 'You have a clear and compelling sense of yourself as a spiritual being. As a result, your life path is devoted to investigations into the unknown, and finding the answers to the mysteries of life.',
55 | url: 'https://www.worldnumerology.com/numerology-life-path/life-path-number-7/',
56 | },
57 | {
58 | number: 8,
59 | title:
60 | 'You are gifted with natural leadership and the capacity to accumulate great wealth.',
61 | description:
62 | 'You have great talent for management in all areas of life, especially in business and financial matters. You understand the material world; you intuitively know what makes virtually any enterprise work. ',
63 | url: 'https://www.worldnumerology.com/numerology-life-path/life-path-number-8/',
64 | },
65 | {
66 | number: 9,
67 | title: 'The 9 Life Path is a philanthropist and a humanitarian.',
68 | description:
69 | 'If you have a 9 Life Path, you are socially conscious, and deeply concerned about the state of the world, you have compassion and idealism. You are a utopian, and will spend your life trying to realize some aspect of your utopian dream, sacrificing money, time, and energy for a better world. It is in giving that you will find much satisfaction. You have a broad outlook on life. You tend to see the big picture, rather than the minute details.',
70 | url: 'https://www.worldnumerology.com/numerology-life-path/life-path-number-9/',
71 | },
72 | {
73 | number: 11,
74 | title:
75 | 'An 11 Life Path has the potential to be a source of inspiration and illumination.',
76 | description:
77 | 'You possess an inordinate amount of energy and intuition. There is so much going on in your psyche that you are often misunderstood early in life, making you shy and withdrawn. You have far more potential than you know.',
78 | url: 'https://www.worldnumerology.com/numerology-life-path/life-path-number-11/',
79 | },
80 | {
81 | number: 22,
82 | title:
83 | 'You are the Master Builder; you were born under the most powerful and potentially the most successful of all Life Path numbers.',
84 | description:
85 | "A 22 Life Path offers you the extremes of life's possibilities: on one hand, you have the potential to be the Master Builder, the person capable of perceiving something great in the archetypal world and manifesting it in the relative world; on the other hand, you can slip into the depths of obscurity, achieving little more than personal support.",
86 | url: 'https://www.worldnumerology.com/numerology-life-path/life-path-number-22/',
87 | },
88 | {
89 | number: 33,
90 | title: 'Those with a Life Path 33 have great potential to guide others.',
91 | description:
92 | "The 33, like the 11 and the 22, is considered a Master number. However, as with all Master numbers, you may or may not find the opportunity and inner resources to reach your full potential. If you do, you will be remembered for generations. If you don't, you will still have a strong, spiritually empowering influence on others.",
93 | url: 'https://www.worldnumerology.com/numerology-life-path/life-path-number-33/',
94 | },
95 | ];
96 |
--------------------------------------------------------------------------------
/src/styles/resets/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
2 |
3 | /* Document
4 | ========================================================================== */
5 |
6 | /**
7 | * 1. Correct the line height in all browsers.
8 | * 2. Prevent adjustments of font size after orientation changes in iOS.
9 | */
10 |
11 | *,
12 | *:before,
13 | *:after {
14 | box-sizing: border-box;
15 | margin: 0;
16 | padding: 0;
17 | }
18 |
19 | html {
20 | line-height: 1.15; /* 1 */
21 | -webkit-text-size-adjust: 100%; /* 2 */
22 | }
23 |
24 | /* Sections
25 | ========================================================================== */
26 |
27 | /**
28 | * Remove the margin in all browsers.
29 | */
30 |
31 | body {
32 | margin: 0;
33 | }
34 |
35 | /**
36 | * Render the `main` element consistently in IE.
37 | */
38 |
39 | main {
40 | display: block;
41 | }
42 |
43 | /**
44 | * Correct the font size and margin on `h1` elements within `section` and
45 | * `article` contexts in Chrome, Firefox, and Safari.
46 | */
47 |
48 | h1 {
49 | font-size: 2em;
50 | margin: 0.67em 0;
51 | }
52 |
53 | /* Grouping content
54 | ========================================================================== */
55 |
56 | /**
57 | * 1. Add the correct box sizing in Firefox.
58 | * 2. Show the overflow in Edge and IE.
59 | */
60 |
61 | hr {
62 | box-sizing: content-box; /* 1 */
63 | height: 0; /* 1 */
64 | overflow: visible; /* 2 */
65 | }
66 |
67 | /**
68 | * 1. Correct the inheritance and scaling of font size in all browsers.
69 | * 2. Correct the odd `em` font sizing in all browsers.
70 | */
71 |
72 | pre {
73 | font-family: monospace, monospace; /* 1 */
74 | font-size: 1em; /* 2 */
75 | }
76 |
77 | /* Text-level semantics
78 | ========================================================================== */
79 |
80 | /**
81 | * Remove the gray background on active links in IE 10.
82 | */
83 |
84 | a {
85 | background-color: transparent;
86 | }
87 |
88 | /**
89 | * 1. Remove the bottom border in Chrome 57-
90 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
91 | */
92 |
93 | abbr[title] {
94 | border-bottom: none; /* 1 */
95 | text-decoration: underline; /* 2 */
96 | text-decoration: underline dotted; /* 2 */
97 | }
98 |
99 | /**
100 | * Add the correct font weight in Chrome, Edge, and Safari.
101 | */
102 |
103 | b,
104 | strong {
105 | font-weight: bolder;
106 | }
107 |
108 | /**
109 | * 1. Correct the inheritance and scaling of font size in all browsers.
110 | * 2. Correct the odd `em` font sizing in all browsers.
111 | */
112 |
113 | code,
114 | kbd,
115 | samp {
116 | font-family: monospace, monospace; /* 1 */
117 | font-size: 1em; /* 2 */
118 | }
119 |
120 | /**
121 | * Add the correct font size in all browsers.
122 | */
123 |
124 | small {
125 | font-size: 80%;
126 | }
127 |
128 | /**
129 | * Prevent `sub` and `sup` elements from affecting the line height in
130 | * all browsers.
131 | */
132 |
133 | sub,
134 | sup {
135 | font-size: 75%;
136 | line-height: 0;
137 | position: relative;
138 | vertical-align: baseline;
139 | }
140 |
141 | sub {
142 | bottom: -0.25em;
143 | }
144 |
145 | sup {
146 | top: -0.5em;
147 | }
148 |
149 | /* Embedded content
150 | ========================================================================== */
151 |
152 | /**
153 | * Remove the border on images inside links in IE 10.
154 | */
155 |
156 | img {
157 | border-style: none;
158 | }
159 |
160 | /* Forms
161 | ========================================================================== */
162 |
163 | /**
164 | * 1. Change the font styles in all browsers.
165 | * 2. Remove the margin in Firefox and Safari.
166 | */
167 |
168 | button,
169 | input,
170 | optgroup,
171 | select,
172 | textarea {
173 | font-family: inherit; /* 1 */
174 | font-size: 100%; /* 1 */
175 | line-height: 1.15; /* 1 */
176 | margin: 0; /* 2 */
177 | }
178 |
179 | /**
180 | * Show the overflow in IE.
181 | * 1. Show the overflow in Edge.
182 | */
183 |
184 | button,
185 | input {
186 | /* 1 */
187 | overflow: visible;
188 | }
189 |
190 | /**
191 | * Remove the inheritance of text transform in Edge, Firefox, and IE.
192 | * 1. Remove the inheritance of text transform in Firefox.
193 | */
194 |
195 | button,
196 | select {
197 | /* 1 */
198 | text-transform: none;
199 | }
200 |
201 | /**
202 | * Correct the inability to style clickable types in iOS and Safari.
203 | */
204 |
205 | button,
206 | [type='button'],
207 | [type='reset'],
208 | [type='submit'] {
209 | -webkit-appearance: button;
210 | }
211 |
212 | /**
213 | * Remove the inner border and padding in Firefox.
214 | */
215 |
216 | button::-moz-focus-inner,
217 | [type='button']::-moz-focus-inner,
218 | [type='reset']::-moz-focus-inner,
219 | [type='submit']::-moz-focus-inner {
220 | border-style: none;
221 | padding: 0;
222 | }
223 |
224 | /**
225 | * Restore the focus styles unset by the previous rule.
226 | */
227 |
228 | button:-moz-focusring,
229 | [type='button']:-moz-focusring,
230 | [type='reset']:-moz-focusring,
231 | [type='submit']:-moz-focusring {
232 | outline: 1px dotted ButtonText;
233 | }
234 |
235 | /**
236 | * Correct the padding in Firefox.
237 | */
238 |
239 | fieldset {
240 | padding: 0.35em 0.75em 0.625em;
241 | }
242 |
243 | /**
244 | * 1. Correct the text wrapping in Edge and IE.
245 | * 2. Correct the color inheritance from `fieldset` elements in IE.
246 | * 3. Remove the padding so developers are not caught out when they zero out
247 | * `fieldset` elements in all browsers.
248 | */
249 |
250 | legend {
251 | box-sizing: border-box; /* 1 */
252 | color: inherit; /* 2 */
253 | display: table; /* 1 */
254 | max-width: 100%; /* 1 */
255 | padding: 0; /* 3 */
256 | white-space: normal; /* 1 */
257 | }
258 |
259 | /**
260 | * Add the correct vertical alignment in Chrome, Firefox, and Opera.
261 | */
262 |
263 | progress {
264 | vertical-align: baseline;
265 | }
266 |
267 | /**
268 | * Remove the default vertical scrollbar in IE 10+.
269 | */
270 |
271 | textarea {
272 | overflow: auto;
273 | }
274 |
275 | /**
276 | * 1. Add the correct box sizing in IE 10.
277 | * 2. Remove the padding in IE 10.
278 | */
279 |
280 | [type='checkbox'],
281 | [type='radio'] {
282 | box-sizing: border-box; /* 1 */
283 | padding: 0; /* 2 */
284 | }
285 |
286 | /**
287 | * Correct the cursor style of increment and decrement buttons in Chrome.
288 | */
289 |
290 | [type='number']::-webkit-inner-spin-button,
291 | [type='number']::-webkit-outer-spin-button {
292 | height: auto;
293 | }
294 |
295 | /**
296 | * 1. Correct the odd appearance in Chrome and Safari.
297 | * 2. Correct the outline style in Safari.
298 | */
299 |
300 | [type='search'] {
301 | -webkit-appearance: textfield; /* 1 */
302 | outline-offset: -2px; /* 2 */
303 | }
304 |
305 | /**
306 | * Remove the inner padding in Chrome and Safari on macOS.
307 | */
308 |
309 | [type='search']::-webkit-search-decoration {
310 | -webkit-appearance: none;
311 | }
312 |
313 | /**
314 | * 1. Correct the inability to style clickable types in iOS and Safari.
315 | * 2. Change font properties to `inherit` in Safari.
316 | */
317 |
318 | ::-webkit-file-upload-button {
319 | -webkit-appearance: button; /* 1 */
320 | font: inherit; /* 2 */
321 | }
322 |
323 | /* Interactive
324 | ========================================================================== */
325 |
326 | /*
327 | * Add the correct display in Edge, IE 10+, and Firefox.
328 | */
329 |
330 | details {
331 | display: block;
332 | }
333 |
334 | /*
335 | * Add the correct display in all browsers.
336 | */
337 |
338 | summary {
339 | display: list-item;
340 | }
341 |
342 | /* Misc
343 | ========================================================================== */
344 |
345 | /**
346 | * Add the correct display in IE 10+.
347 | */
348 |
349 | template {
350 | display: none;
351 | }
352 |
353 | /**
354 | * Add the correct display in IE 10.
355 | */
356 |
357 | [hidden] {
358 | display: none;
359 | }
360 |
--------------------------------------------------------------------------------
/src/assets/images/download-apple.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
9 |
10 |
11 |
16 |
18 |
19 |
20 |
21 |
24 |
30 |
36 |
43 |
46 |
52 |
55 |
61 |
62 |
63 |
64 |
69 |
75 |
79 |
83 |
84 |
90 |
96 |
102 |
108 |
112 |
115 |
118 |
124 |
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------