Compare commits

..

23 Commits
old ... dev

Author SHA1 Message Date
433af10b78
feat(package): add @lottielab/lottie-player dependency
Added @lottielab/lottie-player version 1.1.2 to the project dependencies in package.json and updated pnpm-lock.yaml accordingly. This integration introduces Lottie animations to improve the user interface.
2024-09-27 16:58:38 +02:00
4716d8ce50
feat(animation): add new weather animation JSON files
Added Sunny, Windy, and Fog animations in clear.json, cloud.json, and fog.json. These files contain vector graphic animations for various weather conditions.
2024-09-27 16:58:24 +02:00
eca28c9fed
feat(component): add weather sprite and temperature display
Enhanced HomePage with WeatherSprite component for current weather. Added temperature display showing max and min values, including visual separators for better UI organization.
2024-09-27 16:58:10 +02:00
47361f453f
feat(layout): add dark mode background class to body element
Updated the body element in the layout component to include a background class supporting dark mode. This change enhances the application's theme capability by accommodating dark mode styling.
2024-09-27 16:57:48 +02:00
e417e46dee
feat(lib): add date formatting utilities
Introducing `DateFormatter` class with methods for formatted date output, relative date calculations, and distance to current date using `date-fns` and localization support for French and English.
2024-09-27 16:57:37 +02:00
00fbfeeb17
feat(component): add WeatherSprite with animations
Introduces a new WeatherSprite component featuring Lottie animations based on WMO weather codes. Includes utility subcomponents for displaying titles and indicators.
2024-09-27 16:57:22 +02:00
fd591b44c3
feat(package): add openmeteo dependency
Added openmeteo to package.json and updated pnpm-lock.yaml accordingly. This includes a new dependency on flatbuffers.
2024-09-27 12:08:33 +02:00
797452732f
feat: add axios dependency
Add axios version 1.7.7 to package.json and pnpm-lock.yaml to enable HTTP requests. This includes related dependencies and their configurations.
2024-09-27 12:07:46 +02:00
151925bc1c
refactor(component): simplify HomePage layout
Simplified the HomePage component by removing extensive HTML structure and redundant elements. This change focuses on streamlining the design and improving readability.
2024-09-27 12:07:29 +02:00
bce1c393b3
feat(component): integrate Ubuntu font
Add @fontsource/ubuntu dependency and update global styles to use the Ubuntu font. Removed local font imports from layout.tsx and updated body font-family in globals.css.
2024-09-27 12:05:52 +02:00
11f472646d
feat(app): add favicon.ico and remove city.json
Added a new favicon.ico to the src/app directory and deleted the city.json file from the configs folder. This change updates the application icon and removes unnecessary configuration.
2024-09-27 11:59:48 +02:00
ff3821e777
docs: simplify README instructions and description
Streamlined the description of weather checking capabilities in the README. Removed detailed setup steps about API key creation and environment variable configuration to focus on essential installation commands.
2024-09-27 11:58:42 +02:00
ed37351c49
feat(fonts): add Geist font files
Introduce GeistMonoVF.woff and GeistVF.woff to the project. This enhances the typography options available for the application.
2024-09-27 11:58:28 +02:00
cbf1047244
feat(app): add base layout and home page
Introduced global CSS styling and created the initial structure for the Home page and Root layout. The Home page includes a logo, instructions, and helpful links, while the layout sets up metadata and font usage.
2024-09-27 11:58:10 +02:00
1d93fd9aec
chore(build): configure Next.js with TypeScript and TailwindCSS
Removed VSCode settings and set up essential configuration files. Added Next.js, PostCSS, and TailwindCSS configurations for improved development workflow. Updated tsconfig.json and .gitignore for TypeScript support.
2024-09-27 11:57:39 +02:00
2997df14e0
build: update pnpm-lock.yaml dependencies
Updated multiple dependencies in pnpm-lock.yaml, removing outdated ones like axios and adding new ones such as @radix-ui/react components, react 18.x, and others. This includes upgrading Next.js to version 14.2.13 and adjusting devDependencies accordingly.
2024-09-27 11:56:58 +02:00
9b2a86d887
feat(utils): add utility function for class merging
Introduced `cn` function leveraging `clsx` and `twMerge` to merge class names. This utility simplifies handling of conditional class names with Tailwind CSS.
2024-09-27 11:56:34 +02:00
f982d076a7
feat(hooks): add use-toast hook for toast notifications
Introduce a new use-toast hook inspired by the react-hot-toast library. This hook manages toast notifications with actions such as ADD_TOAST, UPDATE_TOAST, DISMISS_TOAST, and REMOVE_TOAST.
2024-09-27 11:56:13 +02:00
45562d875c
feat(components): add new UI components for checkbox, chart, command, collapsible, dialog, and context-menu
Introduce various UI components including Checkbox, Chart, Command, Collapsible, Dialog, and ContextMenu. These components leverage Radix UI and Recharts to enhance the UI toolkit.
2024-09-27 11:55:48 +02:00
c7bf608462
feat: remove obsolete and outdated components and assets
Deleted unused components and icon assets for codebase cleanup. This includes various UI components, CSS modules, and image assets no longer needed for the project.
2024-09-27 11:55:23 +02:00
f87e388ddd
feat(build): add project configuration files and dependencies
Add IntelliJ IDEA project settings, TypeScript configuration, and pnpm lock file. Updated `package.json` with new dependencies and refactored API handler to TypeScript.
2024-09-27 11:37:21 +02:00
69d1186333
feat(configs): add city configuration file
Introduced a new JSON configuration file for city settings. This file currently includes the city name "Chambéry".
2024-09-27 11:00:32 +02:00
d3e1b122e4
docs: update README with new repo and install instructions
Changed the repository URL and adjusted installation instructions to use pnpm or yarn instead of npm. Removed outdated feature list and image, updated contributions section to include license information.
2024-09-27 10:52:12 +02:00
134 changed files with 8562 additions and 9826 deletions

View File

@ -1 +0,0 @@
OPENWEATHER_API_KEY=lngstr20

View File

@ -1,3 +0,0 @@
{
"extends": ["next", "next/core-web-vitals"]
}

10
.gitignore vendored
View File

@ -4,6 +4,7 @@
/node_modules /node_modules
/.pnp /.pnp
.pnp.js .pnp.js
.yarn/install-state.gz
# testing # testing
/coverage /coverage
@ -25,12 +26,11 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
# local env files # local env files
.env.local .env*.local
.env.development.local
.env.test.local
.env.production.local
# vercel # vercel
.vercel .vercel
.env # typescript
*.tsbuildinfo
next-env.d.ts

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

7
.idea/discord.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
</component>
</project>

12
.idea/forecast.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/git_toolbox_blame.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxBlameSettings">
<option name="version" value="2" />
</component>
</project>

15
.idea/git_toolbox_prj.xml generated Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/forecast.iml" filepath="$PROJECT_DIR$/.idea/forecast.iml" />
</modules>
</component>
</project>

12
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CommitMessageInspectionProfile">
<profile version="1.0">
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -1,4 +0,0 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}

View File

@ -1,46 +1,25 @@
# Weather App # Weather App
Check the current weather on any city on the planet. Switch between metric and imperial units. Check the current weather of the city.
![Alt img](https://images.ctfassets.net/zlsyc9paq6sa/3uBrJ07WSM40FpolgjInHY/7d886cb4187b52194bf9b63c183a1d3a/1627637330_x.gif)
## Features ## Features
1. User's ability to search cities Currently being redesigned
2. Current local time and date
3. Temperatures and humidity
4. Wind speed and direction
5. Sunrise and sunset times
6. Metric vs Imperial system
7. Error handling and loading info
## Installation ## Installation
1. `git clone https://github.com/madzadev/weather-app.git` 1. `git clone ssh://git@git.yidhra.fr:3022/Mathis/forecast.git`
2. `cd weather-app` 2. `cd forecast`
3. `npm install` 3. `pnpm install` || `yarn install`
4. Log-in to [Openweathermap.com](https://openweathermap.org/) 8. `pnpm dev` || `yarn run dev`
5. Create an API key
6. `cp .env.example .env.local`
7. Paste API key for `OPENWEATHER_API_KEY`
8. `npm run dev`
## Contributions ## Contributions
Any feature requests and pull requests are welcome! You're free to go! For more information, see the license.
## License ## License

20
components.json Normal file
View File

@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "stone",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

View File

@ -1,5 +0,0 @@
import styles from "./ContentBox.module.css";
export const ContentBox = ({ children }) => {
return <div className={styles.wrapper}>{children}</div>;
};

View File

@ -1,4 +0,0 @@
.wrapper {
background-color: rgb(247, 247, 247);
padding: 30px;
}

View File

@ -1,16 +0,0 @@
import { getWeekDay, getTime, getAMPM } from "../services/helpers";
import styles from "./DateAndTime.module.css";
export const DateAndTime = ({ weatherData, unitSystem }) => {
return (
<div className={styles.wrapper}>
<h2>
{`${getWeekDay(weatherData)}, ${getTime(
unitSystem,
weatherData.dt,
weatherData.timezone
)} ${getAMPM(unitSystem, weatherData.dt, weatherData.timezone)}`}
</h2>
</div>
);
};

View File

@ -1,4 +0,0 @@
.wrapper {
display: flex;
align-items: center;
}

View File

@ -1,10 +0,0 @@
import styles from "./ErrorScreen.module.css";
export const ErrorScreen = ({ errorMessage, children }) => {
return (
<div className={styles.wrapper}>
<h1 className={styles.message}>{errorMessage}</h1>
{children}
</div>
);
};

View File

@ -1,8 +0,0 @@
.wrapper {
max-width: 260px;
text-align: center;
}
.message {
margin-bottom: 30px;
}

View File

@ -1,5 +0,0 @@
import styles from "./Header.module.css";
export const Header = ({ children }) => {
return <div className={styles.wrapper}>{children}</div>;
};

View File

@ -1,13 +0,0 @@
.wrapper {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
@media only screen and (max-width: 520px) {
.wrapper {
grid-template-columns: 1fr;
place-items: center;
}
}

View File

@ -1 +0,0 @@
export const LoadingScreen = ({ loadingMessage }) => <h1>{loadingMessage}</h1>;

View File

@ -1,40 +0,0 @@
import Image from "next/image";
import { ctoF } from "../services/converters";
import styles from "./MainCard.module.css";
export const MainCard = ({
city,
country,
description,
iconName,
unitSystem,
weatherData,
}) => {
return (
<div className={styles.wrapper}>
<h1 className={styles.location}>
{city}, {country}
</h1>
<p className={styles.description}>{description}</p>
<Image
width="300px"
height="300px"
src={`/icons/${iconName}.svg`}
alt="weatherIcon"
/>
<h1 className={styles.temperature}>
{unitSystem == "metric"
? Math.round(weatherData.main.temp)
: Math.round(ctoF(weatherData.main.temp))}
°{unitSystem == "metric" ? "C" : "F"}
</h1>
<p>
Feels like{" "}
{unitSystem == "metric"
? Math.round(weatherData.main.feels_like)
: Math.round(ctoF(weatherData.main.feels_like))}
°{unitSystem == "metric" ? "C" : "F"}
</p>
</div>
);
};

View File

@ -1,18 +0,0 @@
.wrapper {
text-align: center;
padding: 30px;
}
.location {
font-size: 38px;
margin-bottom: 10px;
}
.description {
font-size: 24px;
margin-bottom: 20px;
}
.temperature {
font-size: 84px;
}

View File

@ -1,63 +0,0 @@
import { degToCompass } from "../services/converters";
import {
getTime,
getAMPM,
getVisibility,
getWindSpeed,
} from "../services/helpers";
import { MetricsCard } from "./MetricsCard";
import styles from "./MetricsBox.module.css";
export const MetricsBox = ({ weatherData, unitSystem }) => {
return (
<div className={styles.wrapper}>
<MetricsCard
title={"Humidity"}
iconSrc={"/icons/humidity.png"}
metric={weatherData.main.humidity}
unit={"%"}
/>
<MetricsCard
title={"Wind speed"}
iconSrc={"/icons/wind.png"}
metric={getWindSpeed(unitSystem, weatherData.wind.speed)}
unit={unitSystem == "metric" ? "m/s" : "m/h"}
/>
<MetricsCard
title={"Wind direction"}
iconSrc={"/icons/compass.png"}
metric={degToCompass(weatherData.wind.deg)}
/>
<MetricsCard
title={"Visibility"}
iconSrc={"/icons/binocular.png"}
metric={getVisibility(unitSystem, weatherData.visibility)}
unit={unitSystem == "metric" ? "km" : "miles"}
/>
<MetricsCard
title={"Sunrise"}
iconSrc={"/icons/sunrise.png"}
metric={getTime(
unitSystem,
weatherData.sys.sunrise,
weatherData.timezone
)}
unit={getAMPM(
unitSystem,
weatherData.sys.sunrise,
weatherData.timezone
)}
/>
<MetricsCard
title={"Sunset"}
iconSrc={"/icons/sunset.png"}
metric={getTime(
unitSystem,
weatherData.sys.sunset,
weatherData.timezone
)}
unit={getAMPM(unitSystem, weatherData.sys.sunset, weatherData.timezone)}
/>
</div>
);
};

View File

@ -1,18 +0,0 @@
.wrapper {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
@media only screen and (max-width: 600px) {
.wrapper {
grid-template-columns: 1fr 1fr;
}
}
@media only screen and (max-width: 475px) {
.wrapper {
grid-template-columns: 1fr;
}
}

View File

@ -1,17 +0,0 @@
import Image from "next/image";
import styles from "./MetricsCard.module.css";
export const MetricsCard = ({ title, iconSrc, metric, unit }) => {
return (
<div className={styles.wrapper}>
<p>{title}</p>
<div className={styles.content}>
<Image width="100px" height="100px" src={iconSrc} alt="weatherIcon" />
<div>
<h1>{metric}</h1>
<p>{unit}</p>
</div>
</div>
</div>
);
};

View File

@ -1,17 +0,0 @@
.wrapper {
background: rgba(255, 255, 255, 0.95);
padding: 20px;
text-align: right;
border-radius: 20px;
}
.content {
display: grid;
grid-template-columns: 1fr 1fr;
}
@media only screen and (max-width: 475px) {
.content {
grid-template-columns: 1fr 2fr;
}
}

View File

@ -1,21 +0,0 @@
import styles from "./Search.module.css";
export const Search = ({
placeHolder,
value,
onFocus,
onChange,
onKeyDown,
}) => {
return (
<input
className={styles.search}
type="text"
placeholder={placeHolder}
value={value}
onFocus={onFocus}
onChange={onChange}
onKeyDown={onKeyDown}
/>
);
};

View File

@ -1,17 +0,0 @@
.search {
height: 40px;
font-size: 18px;
font-family: "Varela Round", sans-serif;
color: #303030;
text-align: right;
padding: 0 10px;
border: none;
border-radius: 10px;
}
@media only screen and (max-width: 520px) {
.search {
width: 100%;
text-align: center;
}
}

View File

@ -1,24 +0,0 @@
import styles from "./UnitSwitch.module.css";
export const UnitSwitch = ({ onClick, unitSystem }) => {
return (
<div className={styles.wrapper}>
<p
className={`${styles.switch} ${
unitSystem == "metric" ? styles.active : styles.inactive
}`}
onClick={onClick}
>
Metric System
</p>
<p
className={`${styles.switch} ${
unitSystem == "metric" ? styles.inactive : styles.active
}`}
onClick={onClick}
>
Imperial System
</p>
</div>
);
};

View File

@ -1,34 +0,0 @@
.wrapper {
text-align: right;
}
.switch {
display: inline;
margin: 0 10px;
cursor: pointer;
}
.active {
color: green;
}
.inactive {
color: black;
}
@media only screen and (max-width: 475px) {
.wrapper {
text-align: center;
}
}
@media only screen and (max-width: 335px) {
.wrapper {
display: grid;
grid-template-columns: 1fr;
}
.switch {
margin: 10px 0;
}
}

4
next.config.mjs Normal file
View File

@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

9198
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{ {
"name": "weather-app", "name": "weather",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -9,12 +9,66 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"next": "11.0.1", "@fontsource/ubuntu": "^5.1.0",
"react": "17.0.2", "@hookform/resolvers": "^3.9.0",
"react-dom": "17.0.2" "@lottielab/lottie-player": "^1.1.2",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.1",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"axios": "^1.7.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
"lucide-react": "^0.446.0",
"next": "14.2.13",
"next-themes": "^0.3.0",
"openmeteo": "^1.1.4",
"react": "^18",
"react-day-picker": "8.10.1",
"react-dom": "^18",
"react-hook-form": "^7.53.0",
"react-resizable-panels": "^2.1.3",
"recharts": "^2.12.7",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.0.0",
"zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"eslint": "7.30.0", "@types/node": "^20",
"eslint-config-next": "11.0.1" "@types/react": "^18",
"@types/react-dom": "^18",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
} }
} }

View File

@ -1,7 +0,0 @@
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;

View File

@ -1,8 +0,0 @@
export default async function handler(req, res) {
const { cityInput } = req.body;
const getWeatherData = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${cityInput}&units=metric&appid=${process.env.OPENWEATHER_API_KEY}`
);
const data = await getWeatherData.json();
res.status(200).json(data);
}

View File

@ -1,84 +0,0 @@
import { useState, useEffect } from "react";
import { MainCard } from "../components/MainCard";
import { ContentBox } from "../components/ContentBox";
import { Header } from "../components/Header";
import { DateAndTime } from "../components/DateAndTime";
import { Search } from "../components/Search";
import { MetricsBox } from "../components/MetricsBox";
import { UnitSwitch } from "../components/UnitSwitch";
import { LoadingScreen } from "../components/LoadingScreen";
import { ErrorScreen } from "../components/ErrorScreen";
import styles from "../styles/Home.module.css";
export const App = () => {
const [cityInput, setCityInput] = useState("Riga");
const [triggerFetch, setTriggerFetch] = useState(true);
const [weatherData, setWeatherData] = useState();
const [unitSystem, setUnitSystem] = useState("metric");
useEffect(() => {
const getData = async () => {
const res = await fetch("api/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cityInput }),
});
const data = await res.json();
setWeatherData({ ...data });
setCityInput("");
};
getData();
}, [triggerFetch]);
const changeSystem = () =>
unitSystem == "metric"
? setUnitSystem("imperial")
: setUnitSystem("metric");
return weatherData && !weatherData.message ? (
<div className={styles.wrapper}>
<MainCard
city={weatherData.name}
country={weatherData.sys.country}
description={weatherData.weather[0].description}
iconName={weatherData.weather[0].icon}
unitSystem={unitSystem}
weatherData={weatherData}
/>
<ContentBox>
<Header>
<DateAndTime weatherData={weatherData} unitSystem={unitSystem} />
<Search
placeHolder="Search a city..."
value={cityInput}
onFocus={(e) => {
e.target.value = "";
e.target.placeholder = "";
}}
onChange={(e) => setCityInput(e.target.value)}
onKeyDown={(e) => {
e.keyCode === 13 && setTriggerFetch(!triggerFetch);
e.target.placeholder = "Search a city...";
}}
/>
</Header>
<MetricsBox weatherData={weatherData} unitSystem={unitSystem} />
<UnitSwitch onClick={changeSystem} unitSystem={unitSystem} />
</ContentBox>
</div>
) : weatherData && weatherData.message ? (
<ErrorScreen errorMessage="City not found, try again!">
<Search
onFocus={(e) => (e.target.value = "")}
onChange={(e) => setCityInput(e.target.value)}
onKeyDown={(e) => e.keyCode === 13 && setTriggerFetch(!triggerFetch)}
/>
</ErrorScreen>
) : (
<LoadingScreen loadingMessage="Loading data..." />
);
};
export default App;

3637
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

8
postcss.config.mjs Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

1
public/clear.json Normal file

File diff suppressed because one or more lines are too long

1
public/cloud.json Normal file
View File

@ -0,0 +1 @@
{"v":"5.1.1","fr":60,"ip":0,"op":180,"w":256,"h":256,"nm":"Windy","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"cloud","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[117.135,125.78,0],"e":[123.135,125.78,0],"to":[1,0,0],"ti":[3.56038412974158e-7,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":45,"s":[123.135,125.78,0],"e":[117.135,125.78,0],"to":[-3.56038412974158e-7,0,0],"ti":[0.33333370089531,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":90,"s":[117.135,125.78,0],"e":[121.135,125.78,0],"to":[-0.33333370089531,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":135,"s":[121.135,125.78,0],"e":[117.135,125.78,0],"to":[0,0,0],"ti":[0.66666668653488,0,0]},{"t":180}],"ix":2},"a":{"a":0,"k":[102.135,62.78,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,24.37],[-24.44,0],[-6.12,-3.18],[-27.6,0],[0,-34.67],[0.03,-0.7],[0,-12.29],[17.86,0],[0,0],[0,0],[0,0]],"o":[[0,0],[-24.44,0],[0,-24.36],[7.38,0],[8.51,-24.62],[34.78,0],[0,0.71],[10.16,5.44],[0,17.8],[0,0],[-0.1,0],[0,0],[0,0]],"v":[[123.61,125.56],[44.26,125.56],[0,81.44],[44.26,37.33],[64.71,42.31],[124.27,0],[187.25,62.78],[187.21,64.9],[204.27,93.32],[171.93,125.56],[124.27,125.56],[123.61,125.56],[124.27,125.56]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.909803986549,0.909803986549,0.909803986549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"cloud","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":0,"s":[195.37,81.5,0],"e":[199.37,81.5,0],"to":[0.66666668653488,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":35,"s":[199.37,81.5,0],"e":[195.37,81.5,0],"to":[0,0,0],"ti":[4.57763661643185e-7,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":80,"s":[195.37,81.5,0],"e":[199.37,81.5,0],"to":[-4.57763661643185e-7,0,0],"ti":[0.66666668653488,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":125,"s":[199.37,81.5,0],"e":[195.37,81.5,0],"to":[-0.64289206266403,0,0],"ti":[0.03566187247634,0,0]},{"t":180}],"ix":2},"a":{"a":0,"k":[46.37,28.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,11.06],[-11.09,0],[-2.78,-1.45],[-12.52,0],[0,-15.74],[0.01,-0.32],[0,-5.58],[8.11,0],[0,0],[0,0],[0,0]],"o":[[0,0],[-11.09,0],[0,-11.06],[3.35,0],[3.86,-11.18],[15.8,0],[0,0.32],[4.61,2.47],[0,8.09],[0,0],[-0.04,0],[0,0],[0,0]],"v":[[56.11,57],[20.09,57],[0,36.97],[20.09,16.95],[29.38,19.21],[56.41,0],[85.01,28.5],[84.99,29.46],[92.74,42.36],[78.05,57],[56.41,57],[56.11,57],[56.41,57]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.909803986549,0.909803986549,0.909803986549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"bond","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[128,128,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[256,256],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"bond","np":1,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}

1
public/fog.json Normal file

File diff suppressed because one or more lines are too long

1
public/foggy.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
<svg height="511.99986pt" viewBox="0 0 511.99986 511.99986" width="511.99986pt" xmlns="http://www.w3.org/2000/svg"><path d="m477.449219 256c0-21.75 39.410156-48.386719 34.050781-68.449219-5.550781-20.757812-53.15625-24.101562-63.671875-42.277343-10.667969-18.433594 10.125-61.304688-4.835937-76.265626-14.960938-14.960937-57.832032 5.828126-76.269532-4.835937-18.171875-10.515625-21.515625-58.121094-42.273437-63.671875-20.0625-5.359375-46.699219 34.050781-68.449219 34.050781s-48.386719-39.410156-68.449219-34.050781c-20.757812 5.550781-24.101562 53.15625-42.277343 63.671875-18.433594 10.667969-61.304688-10.125-76.265626 4.835937-14.960937 14.960938 5.828126 57.832032-4.835937 76.269532-10.515625 18.171875-58.121094 21.515625-63.671875 42.273437-5.359375 20.0625 34.050781 46.699219 34.050781 68.449219s-39.410156 48.386719-34.050781 68.449219c5.550781 20.757812 53.15625 24.101562 63.671875 42.277343 10.667969 18.433594-10.125 61.304688 4.835937 76.265626 14.960938 14.960937 57.832032-5.828126 76.269532 4.835937 18.171875 10.515625 21.515625 58.121094 42.273437 63.671875 20.0625 5.359375 46.699219-34.050781 68.449219-34.050781s48.386719 39.410156 68.449219 34.050781c20.757812-5.550781 24.101562-53.15625 42.277343-63.671875 18.433594-10.667969 61.304688 10.125 76.265626-4.835937 14.960937-14.960938-5.828126-57.832032 4.835937-76.269532 10.515625-18.171875 58.121094-21.515625 63.671875-42.273437 5.359375-20.0625-34.050781-46.699219-34.050781-68.449219zm0 0" fill="#ffee8c"/><path d="m426.957031 256c0 86.347656-64.019531 157.746094-147.191406 169.3125-7.765625 1.089844-15.695313 1.640625-23.761719 1.640625-94.417968 0-170.96875-76.539063-170.96875-170.953125 0-94.417969 76.550782-170.957031 170.96875-170.957031 8.066406 0 15.996094.554687 23.761719 1.640625 83.171875 11.566406 147.191406 82.964844 147.191406 169.316406zm0 0" fill="#f28f44"/><path d="m426.957031 256c0 86.347656-64.019531 157.746094-147.191406 169.3125-83.175781-11.566406-147.195313-82.964844-147.195313-169.3125 0-86.351562 64.019532-157.75 147.195313-169.316406 83.171875 11.566406 147.191406 82.964844 147.191406 169.316406zm0 0" fill="#ffd073"/><g fill="#cc9236"><path d="m155.875 229.488281c-2.003906 0-4.011719-.765625-5.539062-2.296875-3.0625-3.0625-3.0625-8.023437 0-11.082031 17.53125-17.53125 46.054687-17.53125 63.582031 0 3.0625 3.058594 3.0625 8.019531 0 11.082031-3.058594 3.058594-8.019531 3.058594-11.082031 0-11.417969-11.421875-29.996094-11.421875-41.417969 0-1.53125 1.53125-3.535157 2.296875-5.542969 2.296875zm0 0"/><path d="m356.125 229.488281c-2.007812 0-4.011719-.765625-5.542969-2.296875-11.417969-11.421875-30-11.417968-41.417969 0-3.0625 3.058594-8.023437 3.058594-11.082031 0-3.0625-3.0625-3.0625-8.023437 0-11.082031 17.527344-17.53125 46.054688-17.53125 63.585938 0 3.058593 3.058594 3.058593 8.023437 0 11.082031-1.53125 1.53125-3.539063 2.296875-5.542969 2.296875zm0 0"/><path d="m256 310.582031c-17.015625 0-33.015625-6.628906-45.046875-18.660156-3.058594-3.0625-3.058594-8.023437 0-11.082031 3.0625-3.0625 8.023437-3.0625 11.082031 0 9.074219 9.070312 21.132813 14.066406 33.964844 14.066406s24.890625-4.996094 33.964844-14.066406c3.058594-3.0625 8.023437-3.0625 11.082031 0 3.058594 3.058594 3.058594 8.019531 0 11.082031-12.03125 12.03125-28.03125 18.660156-45.046875 18.660156zm0 0"/></g></svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -1 +0,0 @@
<svg height="512pt" viewBox="0 0 512 512.00001" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m509.53125 291.945312c-79.9375 79.933594-209.539062 79.933594-289.472656 0-79.9375-79.9375-79.9375-209.539062 0-289.476562.835937-.835938 1.683594-1.648438 2.53125-2.46875-53.863282 7.269531-105.867188 31.585938-147.265625 72.984375-100.429688 100.429687-100.429688 263.261719 0 363.691406 100.429687 100.429688 263.261719 100.429688 363.691406 0 41.398437-41.398437 65.714844-93.402343 72.984375-147.265625-.820312.847656-1.632812 1.695313-2.46875 2.535156zm0 0" fill="#ffd073"/><path d="m368.015625 486.964844c-95.585937 45.640625-213.523437 28.875-292.691406-50.289063-100.433594-100.433593-100.433594-263.257812 0-363.691406 38.574219-38.574219 86.367187-62.328125 136.277343-71.25-25.664062 12.238281-49.722656 29.003906-70.976562 50.257813-100.433594 100.433593-100.433594 263.257812 0 363.691406 61.847656 61.84375 147.363281 85.609375 227.390625 71.28125zm0 0" fill="#f28f44"/><path d="m355.84375 117.804688 28.492188-5.566407c12.484374-2.441406 12.484374-20.300781 0-22.738281l-28.492188-5.570312c-16.21875-3.167969-28.898438-15.847657-32.066406-32.066407l-5.570313-28.492187c-2.4375-12.484375-20.300781-12.484375-22.738281 0l-5.570312 28.492187c-3.167969 16.21875-15.847657 28.898438-32.0625 32.066407l-28.496094 5.570312c-12.484375 2.4375-12.484375 20.296875 0 22.738281l28.496094 5.566407c16.214843 3.167968 28.894531 15.847656 32.0625 32.066406l5.570312 28.496094c2.4375 12.484374 20.300781 12.484374 22.738281 0l5.570313-28.496094c3.167968-16.214844 15.847656-28.898438 32.066406-32.066406zm0 0" fill="#ffee8c"/><path d="m424.875 251.722656 18.40625-3.597656c8.066406-1.574219 8.066406-13.113281 0-14.6875l-18.40625-3.597656c-10.476562-2.046875-18.667969-10.238282-20.714844-20.714844l-3.597656-18.40625c-1.574219-8.066406-13.113281-8.066406-14.6875 0l-3.597656 18.40625c-2.046875 10.476562-10.238282 18.667969-20.714844 20.714844l-18.40625 3.597656c-8.066406 1.574219-8.066406 13.113281 0 14.6875l18.40625 3.597656c10.476562 2.046875 18.667969 10.238282 20.714844 20.714844l3.597656 18.40625c1.574219 8.066406 13.113281 8.066406 14.6875 0l3.597656-18.40625c2.046875-10.476562 10.238282-18.664062 20.714844-20.714844zm0 0" fill="#ffee8c"/><g fill="#cc9236"><path d="m80.242188 257.46875c-3.292969 0-6.363282-2.078125-7.464844-5.375l-2.980469-8.90625c-1.378906-4.121094.84375-8.582031 4.964844-9.964844 4.125-1.378906 8.585937.84375 9.964843 4.96875l2.980469 8.90625c1.378907 4.121094-.84375 8.582032-4.964843 9.964844-.828126.277344-1.671876.40625-2.5.40625zm0 0"/><path d="m123.632812 242.949219c-3.292968 0-6.363281-2.082031-7.464843-5.378907l-2.980469-8.90625c-1.378906-4.121093.84375-8.582031 4.964844-9.964843 4.125-1.378907 8.585937.847656 9.964844 4.96875l2.980468 8.90625c1.378906 4.121093-.84375 8.582031-4.964844 9.964843-.828124.277344-1.671874.410157-2.5.410157zm0 0"/><path d="m105.152344 300.09375c-9.335938 0-18.613282-2.167969-27.214844-6.453125-3.890625-1.9375-5.472656-6.664063-3.535156-10.558594 1.941406-3.890625 6.667968-5.472656 10.558594-3.53125 22.402343 11.164063 49.707031 2.023438 60.875-20.375 1.9375-3.894531 6.667968-5.476562 10.554687-3.535156 3.894531 1.9375 5.476563 6.664063 3.535156 10.558594-7.285156 14.613281-19.824219 25.515625-35.308593 30.699219-6.375 2.132812-12.933594 3.195312-19.464844 3.195312zm0 0"/></g></svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -1 +0,0 @@
<svg height="512pt" viewBox="0 -10 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m483.324219 151.332031c-36.757813 36.757813-96.347657 36.757813-133.105469 0-36.757812-36.757812-36.757812-96.347656 0-133.105469.382812-.382812.773438-.757812 1.164062-1.132812-24.765624 3.339844-48.679687 14.519531-67.714843 33.558594-46.179688 46.179687-46.179688 121.050781 0 167.230468 46.179687 46.179688 121.050781 46.179688 167.230469 0 19.039062-19.035156 30.21875-42.949218 33.5625-67.714843-.378907.390625-.753907.78125-1.136719 1.164062zm0 0" fill="#ffd073"/><path d="m418.253906 241.007812c-43.953125 20.984376-98.183594 13.277344-134.585937-23.125-46.179688-46.183593-46.179688-121.050781 0-167.230468 17.738281-17.738282 39.714843-28.660156 62.664062-32.761719-11.800781 5.625-22.863281 13.335937-32.636719 23.109375-46.179687 46.179688-46.179687 121.046875 0 167.230469 28.4375 28.4375 67.757813 39.363281 104.558594 32.777343zm0 0" fill="#f28f44"/><path d="m119.625 66.523438 18.261719-3.566407c8-1.5625 8-13.011719 0-14.574219l-18.261719-3.570312c-10.394531-2.03125-18.519531-10.15625-20.550781-20.550781l-3.566407-18.261719c-1.5625-8-13.011718-8-14.574218 0l-3.570313 18.261719c-2.03125 10.394531-10.15625 18.519531-20.550781 20.550781l-18.261719 3.570312c-8 1.5625-8 13.007813 0 14.574219l18.261719 3.566407c10.394531 2.03125 18.519531 10.160156 20.550781 20.550781l3.570313 18.261719c1.5625 8.003906 13.011718 8.003906 14.574218 0l3.570313-18.261719c2.027344-10.390625 10.152344-18.519531 20.546875-20.550781zm0 0" fill="#ffee8c"/><path d="m408.09375 304.191406c0 37.882813-30.710938 68.59375-68.609375 68.59375h-248.265625c-50.371094 0-91.21875-40.832031-91.21875-91.21875 0-50.375 40.847656-91.21875 91.21875-91.21875 4.519531 0 8.964844.347656 13.316406.976563 20.601563-40.910157 62.964844-68.980469 111.910156-68.980469 59.222657 0 108.835938 41.117188 121.867188 96.355469 1.300781 5.511719 2.238281 11.164062 2.78125 16.921875v.011718c37.136719.851563 67 31.214844 67 68.558594zm0 0" fill="#84abc1"/><path d="m259.96875 130.125c-13.558594-5.035156-28.226562-7.777344-43.523438-7.777344-48.945312 0-91.296874 28.070313-111.910156 68.96875v.011719c-4.355468-.632813-8.796875-.980469-13.316406-.980469-50.386719.003906-91.21875 40.847656-91.21875 91.222656 0 50.386719 40.832031 91.21875 91.21875 91.21875h44.089844c-45.402344 0-82.191406-36.800781-82.191406-82.191406 0-40.277344 28.972656-73.796875 67.230468-80.828125 3.785156-.707031 7.660156-1.144531 11.613282-1.300781 1.105468-.039062 2.226562-.066406 3.347656-.066406 14.757812 0 28.613281 3.890625 40.574218 10.714844-1.660156-11.578126-13.632812-22.425782-31.28125-31.167969 9.691407-32.777344 42.675782-63.175781 115.367188-57.824219zm0 0" fill="#4d87a1"/><path d="m512 436.441406c0 30.636719-24.835938 55.472656-55.484375 55.472656h-200.785156c-40.738281 0-73.769531-33.019531-73.769531-73.769531 0-40.742187 33.03125-73.773437 73.769531-73.773437 3.65625 0 7.25.28125 10.769531.789062 16.664062-33.082031 50.925781-55.785156 90.507812-55.785156 47.894532 0 88.019532 33.25 98.558594 77.925781 1.050782 4.457031 1.8125 9.03125 2.25 13.683594v.011719c30.035156.6875 54.183594 25.242187 54.183594 55.445312zm0 0" fill="#d3ddea"/><path d="m392.207031 295.667969c-10.964843-4.074219-22.828125-6.289063-35.199219-6.289063-39.582031 0-73.835937 22.699219-90.507812 55.777344v.007812c-3.519531-.507812-7.113281-.789062-10.769531-.789062-40.75 0-73.769531 33.03125-73.769531 73.769531 0 40.75 33.019531 73.773438 73.769531 73.773438h35.660156c-36.71875 0-66.472656-29.761719-66.472656-66.472657 0-32.574218 23.429687-59.679687 54.371093-65.367187 3.0625-.574219 6.195313-.925781 9.394532-1.050781.894531-.03125 1.800781-.054688 2.707031-.054688 11.933594 0 23.140625 3.144532 32.8125 8.664063-1.34375-9.363281-11.023437-18.136719-25.300781-25.207031 7.839844-26.507813 34.515625-51.089844 93.304687-46.761719zm0 0" fill="#84abc1"/></svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1 +0,0 @@
<svg height="512pt" viewBox="0 -99 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m512 228.148438c0 47.527343-38.53125 86.058593-86.078125 86.058593h-311.476563c-63.199218 0-114.44140575-51.226562-114.44140575-114.445312 0-63.199219 51.24218775-114.445313 114.44140575-114.445313 5.671876 0 11.246094.4375 16.707032 1.226563 25.847656-51.324219 79-86.542969 140.40625-86.542969 74.296875 0 136.546875 51.585938 152.894531 120.890625 1.632813 6.914063 2.8125 14.007813 3.492187 21.230469v.015625c46.589844 1.066406 84.054688 39.160156 84.054688 86.011719zm0 0" fill="#d3ddea"/><path d="m326.164062 9.765625c-17.011718-6.316406-35.414062-9.7578125-54.605468-9.7578125-61.40625 0-114.542969 35.2187495-140.410156 86.5273435v.015625c-5.457032-.792969-11.03125-1.226562-16.703126-1.226562-63.214843 0-114.445312 51.246093-114.445312 114.445312 0 63.214844 51.230469 114.445313 114.445312 114.445313h55.316407c-56.964844 0-103.121094-46.171875-103.121094-103.121094 0-50.535156 36.351563-92.585938 84.347656-101.40625 4.75-.886719 9.613281-1.4375 14.570313-1.632812 1.390625-.046876 2.796875-.082032 4.203125-.082032 18.511719 0 35.894531 4.878906 50.90625 13.445313-2.085938-14.53125-17.105469-28.140625-39.25-39.105469 12.160156-41.125 53.542969-79.265625 144.746093-72.546875zm0 0" fill="#84abc1"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1 +0,0 @@
<svg height="512pt" viewBox="0 -99 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m512 228.148438c0 47.527343-38.53125 86.058593-86.078125 86.058593h-311.476563c-63.199218 0-114.44140575-51.226562-114.44140575-114.445312 0-63.199219 51.24218775-114.445313 114.44140575-114.445313 5.671876 0 11.246094.4375 16.707032 1.226563 25.847656-51.324219 79-86.542969 140.40625-86.542969 74.296875 0 136.546875 51.585938 152.894531 120.890625 1.632813 6.914063 2.8125 14.007813 3.492187 21.230469v.015625c46.589844 1.066406 84.054688 39.160156 84.054688 86.011719zm0 0" fill="#d3ddea"/><path d="m326.164062 9.765625c-17.011718-6.316406-35.414062-9.7578125-54.605468-9.7578125-61.40625 0-114.542969 35.2187495-140.410156 86.5273435v.015625c-5.457032-.792969-11.03125-1.226562-16.703126-1.226562-63.214843 0-114.445312 51.246093-114.445312 114.445312 0 63.214844 51.230469 114.445313 114.445312 114.445313h55.316407c-56.964844 0-103.121094-46.171875-103.121094-103.121094 0-50.535156 36.351563-92.585938 84.347656-101.40625 4.75-.886719 9.613281-1.4375 14.570313-1.632812 1.390625-.046876 2.796875-.082032 4.203125-.082032 18.511719 0 35.894531 4.878906 50.90625 13.445313-2.085938-14.53125-17.105469-28.140625-39.25-39.105469 12.160156-41.125 53.542969-79.265625 144.746093-72.546875zm0 0" fill="#84abc1"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1 +0,0 @@
<svg height="512pt" viewBox="0 -71 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m408.09375 181.847656c0 37.882813-30.710938 68.59375-68.609375 68.59375h-248.265625c-50.371094 0-91.21875-40.832031-91.21875-91.21875 0-50.375 40.847656-91.21875 91.21875-91.21875 4.519531 0 8.964844.347656 13.316406.976563 20.601563-40.910157 62.964844-68.980469 111.910156-68.980469 59.222657 0 108.835938 41.117188 121.867188 96.355469 1.300781 5.511719 2.238281 11.164062 2.78125 16.921875v.011718c37.136719.851563 67 31.214844 67 68.558594zm0 0" fill="#84abc1"/><path d="m259.96875 7.78125c-13.558594-5.035156-28.226562-7.77734375-43.523438-7.77734375-48.945312 0-91.296874 28.07421875-111.910156 68.96874975v.011719c-4.355468-.628906-8.796875-.976563-13.316406-.976563-50.386719 0-91.21875 40.84375-91.21875 91.21875 0 50.386719 40.832031 91.21875 91.21875 91.21875h44.089844c-45.402344 0-82.191406-36.800781-82.191406-82.191406 0-40.28125 28.972656-73.796875 67.230468-80.828125 3.785156-.707031 7.660156-1.144531 11.613282-1.300781 1.105468-.039062 2.226562-.066406 3.347656-.066406 14.757812 0 28.613281 3.890625 40.574218 10.714844-1.660156-11.578126-13.632812-22.425782-31.28125-31.167969 9.691407-32.777344 42.675782-63.175781 115.367188-57.824219zm0 0" fill="#4d87a1"/><path d="m264.3125 245.925781c.496094.085938.789062.085938.835938.085938h-3.796876c.992188 0 1.976563-.03125 2.960938-.085938zm0 0" fill="#84abc1"/><path d="m512 314.097656c0 30.636719-24.835938 55.476563-55.484375 55.476563h-200.785156c-40.738281 0-73.769531-33.023438-73.769531-73.773438 0-40.738281 33.03125-73.773437 73.769531-73.773437 3.65625 0 7.25.28125 10.769531.792968 16.664062-33.085937 50.925781-55.789062 90.507812-55.789062 47.894532 0 88.019532 33.25 98.558594 77.929688 1.050782 4.457031 1.8125 9.027343 2.25 13.683593v.007813c30.035156.6875 54.183594 25.246094 54.183594 55.445312zm0 0" fill="#d3ddea"/><path d="m392.207031 173.324219c-10.964843-4.070313-22.828125-6.289063-35.199219-6.289063-39.582031 0-73.835937 22.703125-90.507812 55.777344v.007812c-3.519531-.507812-7.113281-.789062-10.769531-.789062-40.75 0-73.769531 33.03125-73.769531 73.773438 0 40.75 33.019531 73.769531 73.769531 73.769531h35.660156c-36.71875 0-66.472656-29.761719-66.472656-66.46875 0-32.578125 23.429687-59.683594 54.371093-65.371094 3.0625-.570313 6.195313-.925781 9.394532-1.050781.894531-.03125 1.800781-.050782 2.707031-.050782 11.933594 0 23.140625 3.144532 32.8125 8.664063-1.34375-9.367187-11.023437-18.140625-25.300781-25.210937 7.839844-26.507813 34.515625-51.089844 93.304687-46.761719zm0 0" fill="#84abc1"/></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1 +0,0 @@
<svg height="512pt" viewBox="0 -71 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m408.09375 181.847656c0 37.882813-30.710938 68.59375-68.609375 68.59375h-248.265625c-50.371094 0-91.21875-40.832031-91.21875-91.21875 0-50.375 40.847656-91.21875 91.21875-91.21875 4.519531 0 8.964844.347656 13.316406.976563 20.601563-40.910157 62.964844-68.980469 111.910156-68.980469 59.222657 0 108.835938 41.117188 121.867188 96.355469 1.300781 5.511719 2.238281 11.164062 2.78125 16.921875v.011718c37.136719.851563 67 31.214844 67 68.558594zm0 0" fill="#84abc1"/><path d="m259.96875 7.78125c-13.558594-5.035156-28.226562-7.77734375-43.523438-7.77734375-48.945312 0-91.296874 28.07421875-111.910156 68.96874975v.011719c-4.355468-.628906-8.796875-.976563-13.316406-.976563-50.386719 0-91.21875 40.84375-91.21875 91.21875 0 50.386719 40.832031 91.21875 91.21875 91.21875h44.089844c-45.402344 0-82.191406-36.800781-82.191406-82.191406 0-40.28125 28.972656-73.796875 67.230468-80.828125 3.785156-.707031 7.660156-1.144531 11.613282-1.300781 1.105468-.039062 2.226562-.066406 3.347656-.066406 14.757812 0 28.613281 3.890625 40.574218 10.714844-1.660156-11.578126-13.632812-22.425782-31.28125-31.167969 9.691407-32.777344 42.675782-63.175781 115.367188-57.824219zm0 0" fill="#4d87a1"/><path d="m264.3125 245.925781c.496094.085938.789062.085938.835938.085938h-3.796876c.992188 0 1.976563-.03125 2.960938-.085938zm0 0" fill="#84abc1"/><path d="m512 314.097656c0 30.636719-24.835938 55.476563-55.484375 55.476563h-200.785156c-40.738281 0-73.769531-33.023438-73.769531-73.773438 0-40.738281 33.03125-73.773437 73.769531-73.773437 3.65625 0 7.25.28125 10.769531.792968 16.664062-33.085937 50.925781-55.789062 90.507812-55.789062 47.894532 0 88.019532 33.25 98.558594 77.929688 1.050782 4.457031 1.8125 9.027343 2.25 13.683593v.007813c30.035156.6875 54.183594 25.246094 54.183594 55.445312zm0 0" fill="#d3ddea"/><path d="m392.207031 173.324219c-10.964843-4.070313-22.828125-6.289063-35.199219-6.289063-39.582031 0-73.835937 22.703125-90.507812 55.777344v.007812c-3.519531-.507812-7.113281-.789062-10.769531-.789062-40.75 0-73.769531 33.03125-73.769531 73.773438 0 40.75 33.019531 73.769531 73.769531 73.769531h35.660156c-36.71875 0-66.472656-29.761719-66.472656-66.46875 0-32.578125 23.429687-59.683594 54.371093-65.371094 3.0625-.570313 6.195313-.925781 9.394532-1.050781.894531-.03125 1.800781-.050782 2.707031-.050782 11.933594 0 23.140625 3.144532 32.8125 8.664063-1.34375-9.367187-11.023437-18.140625-25.300781-25.210937 7.839844-26.507813 34.515625-51.089844 93.304687-46.761719zm0 0" fill="#84abc1"/></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -1 +0,0 @@
<svg height="512pt" viewBox="0 0 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m511.433594 227.894531c0 47.476563-38.488282 85.964844-85.980469 85.964844h-311.132813c-63.128906 0-114.320312-51.171875-114.320312-114.316406 0-63.132813 51.191406-114.320313 114.320312-114.320313 5.664063 0 11.230469.433594 16.6875 1.226563 25.820313-51.269531 78.910157-86.449219 140.25-86.449219 74.21875 0 136.398438 51.527344 152.726563 120.757812 1.632813 6.90625 2.808594 13.992188 3.488281 21.207032v.015625c46.539063 1.0625 83.960938 39.117187 83.960938 85.914062zm0 0" fill="#84abc1"/><path d="m325.800781 9.753906c-16.992187-6.3125-35.371093-9.74999975-54.542969-9.74999975-61.339843 0-114.414062 35.18359375-140.253906 86.43359375v.015625c-5.453125-.789063-11.019531-1.226563-16.683594-1.226563-63.148437 0-114.320312 51.191407-114.320312 114.320313 0 63.148437 51.171875 114.320313 114.320312 114.320313h55.253907c-56.902344 0-103.007813-46.121094-103.007813-103.007813 0-50.476563 36.3125-92.484375 84.253906-101.292969 4.746094-.886718 9.601563-1.4375 14.558594-1.632812 1.386719-.046875 2.789063-.078125 4.195313-.078125 18.492187 0 35.855469 4.871093 50.851562 13.425781-2.085937-14.511719-17.085937-28.105469-39.207031-39.0625 12.144531-41.078125 53.484375-79.175781 144.582031-72.464844zm0 0" fill="#4d87a1"/><path d="m357.722656 139.523438-194.324218 174.421874h48.257812l-70.503906 81.363282h50.609375l-80.035157 116.691406 191.855469-133.492188h-52.96875l84.746094-95.222656h-52.964844z" fill="#ffee8c"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1 +0,0 @@
<svg height="512pt" viewBox="0 0 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m511.433594 227.894531c0 47.476563-38.488282 85.964844-85.980469 85.964844h-311.132813c-63.128906 0-114.320312-51.171875-114.320312-114.316406 0-63.132813 51.191406-114.320313 114.320312-114.320313 5.664063 0 11.230469.433594 16.6875 1.226563 25.820313-51.269531 78.910157-86.449219 140.25-86.449219 74.21875 0 136.398438 51.527344 152.726563 120.757812 1.632813 6.90625 2.808594 13.992188 3.488281 21.207032v.015625c46.539063 1.0625 83.960938 39.117187 83.960938 85.914062zm0 0" fill="#84abc1"/><path d="m325.800781 9.753906c-16.992187-6.3125-35.371093-9.74999975-54.542969-9.74999975-61.339843 0-114.414062 35.18359375-140.253906 86.43359375v.015625c-5.453125-.789063-11.019531-1.226563-16.683594-1.226563-63.148437 0-114.320312 51.191407-114.320312 114.320313 0 63.148437 51.171875 114.320313 114.320312 114.320313h55.253907c-56.902344 0-103.007813-46.121094-103.007813-103.007813 0-50.476563 36.3125-92.484375 84.253906-101.292969 4.746094-.886718 9.601563-1.4375 14.558594-1.632812 1.386719-.046875 2.789063-.078125 4.195313-.078125 18.492187 0 35.855469 4.871093 50.851562 13.425781-2.085937-14.511719-17.085937-28.105469-39.207031-39.0625 12.144531-41.078125 53.484375-79.175781 144.582031-72.464844zm0 0" fill="#4d87a1"/><path d="m357.722656 139.523438-194.324218 174.421874h48.257812l-70.503906 81.363282h50.609375l-80.035157 116.691406 191.855469-133.492188h-52.96875l84.746094-95.222656h-52.964844z" fill="#ffee8c"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

89
public/route_glisante.svg Normal file
View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In -->
<svg
version="1.1"
x="0px"
y="0px"
width="576.53827"
height="507.94376"
viewBox="-0.781 -0.08 576.53826 507.94375"
enable-background="new -0.781 -0.08 576 506"
xml:space="preserve"
id="svg2"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
sodipodi:docname="route_glisante.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata
id="metadata18"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1389"
id="namedview16"
showgrid="false"
fit-margin-top="0.3"
fit-margin-left="0.3"
fit-margin-right="0.3"
fit-margin-bottom="0.3"
inkscape:zoom="1.3736857"
inkscape:cx="291.18742"
inkscape:cy="218.39057"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-page="true"
inkscape:snap-text-baseline="true"
inkscape:snap-center="true"
inkscape:snap-object-midpoints="true"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" /><defs
id="defs4"><pattern
patternUnits="userSpaceOnUse"
width="7"
height="7"
patternTransform="translate(-27.409426,37.156635)"
id="pattern12310-9"><path
inkscape:connector-curvature="0"
id="rect12307-0"
d="M 0,0 V 3.5 A 3.5,3.5 0 0 1 3.5,0 Z M 3.5,0 A 3.5,3.5 0 0 1 7,3.5 V 0 Z M 7,3.5 A 3.5,3.5 0 0 1 3.5,7 H 7 Z M 3.5,7 A 3.5,3.5 0 0 1 0,3.5 V 7 Z"
style="opacity:1;fill:#1a1a1a;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.50953102;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:1.50953107, 1.50953107;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers stroke fill" /></pattern></defs><path
style="fill:#f8190d;stroke:none"
d="M 288.26562 13.638672 C 281.64663 13.638672 275.53166 17.170344 272.22266 22.902344 L 16.126953 466.47266 C 12.816953 472.20366 12.816953 479.26705 16.126953 484.99805 C 19.435953 490.73005 25.552875 494.26172 32.171875 494.26172 L 544.36133 494.26172 C 550.97933 494.26172 557.09625 490.73105 560.40625 484.99805 C 563.71625 479.26705 563.71625 472.20366 560.40625 466.47266 L 304.31055 22.902344 C 301.00055 17.170344 294.88462 13.638672 288.26562 13.638672 z M 288.26562 69.425781 L 512.09375 457.10547 L 64.439453 457.10547 L 288.26562 69.425781 z "
transform="translate(-0.781,-0.08)"
id="path8" /><g
id="g4255"
transform="translate(580.56445,111.04893)"
style="fill:#ffffff"><path
sodipodi:nodetypes="ccsssccsccc"
inkscape:connector-curvature="0"
id="path4251"
d="m -381.50857,328.78348 46.58999,0 c 7.70191,-8.02921 14.53678,-15.84448 17.5048,-24.65402 4.02448,-11.94525 10.22837,-27.86902 -17.07764,-33.0728 -7.75922,-1.4787 -18.05652,-3.44089 -23.60395,-4.80015 -30.23113,-7.40739 -24.65773,-22.74177 24.99671,-44.12042 l -10.55555,-2.91187 c -22.84086,5.68299 -42.97537,15.79971 -47.26554,29.13535 -3.07654,9.56316 -3.89895,30.38668 48.72148,29.28412 10.59081,-0.2566 13.76411,5.46023 10.89022,10.715 -13.78682,25.26167 -39.77266,34.84985 -50.20052,40.42479 z"
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:0.30000001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
sodipodi:nodetypes="ccccccccccccc"
inkscape:connector-curvature="0"
id="path4253"
d="m -248.6543,255.98663 -44.77007,14.37738 c -8.10485,2.68237 -5.9882,5.112 3.82184,6.09673 l 103.55353,16.10631 c 10.02965,1.39835 16.57126,13.25211 7.82566,23.113 l -12.55746,13.46771 -20.20113,0 16.67299,-15.00272 c 8.90076,-9.44867 -1.64649,-11.31998 -11.48621,-12.93336 l -114.01808,-21.20209 c -5.57706,-7.37893 -1.34448,-17.17543 9.46359,-19.66429 l 55.6896,-9.09047 z"
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:0.30000001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
sodipodi:nodetypes="cccsscssssccsssscssccccccccccccccc"
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m -289.54679,129.08019 c -7.13922,-2.8416 -13.65378,-1.58761 -19.24913,6.34905 l -14.81984,21.10294 c -3.99203,-1.49365 -8.41698,0.49508 -9.94303,4.48301 l -8.42759,22.02321 c -1.48381,3.87752 0.33974,8.1703 4.09022,9.83297 l -8.6877,22.70294 c -0.63662,1.66365 0.18946,3.51384 1.85309,4.15045 l 9.99202,3.82363 c 1.66363,0.63663 3.51602,-0.18861 4.15265,-1.85225 l 8.68014,-22.68322 68.33908,26.1512 -8.68013,22.68322 c -0.63663,1.66364 0.19165,3.51468 1.85529,4.15129 l 9.992,3.82363 c 1.66364,0.63662 3.51385,-0.18944 4.15047,-1.85309 l 8.68769,-22.70294 c 3.90184,1.26437 8.12609,-0.7137 9.60961,-4.59048 l 8.42758,-22.02323 c 1.52606,-3.98792 -0.44304,-8.42346 -4.41241,-9.97636 l 3.05395,-25.60539 c 1.13239,-9.64447 -2.8786,-14.92627 -10.09104,-17.57658 l -29.28755,-11.20742 z m -0.82343,8.91795 50.24639,19.28304 c 11.50836,4.12065 11.83426,4.52207 10.09106,17.57658 l -1.20275,10.07994 c -0.32048,2.76834 -1.41713,4.65344 -3.49058,5.60727 l -78.09869,-29.88588 c -0.90701,-2.09436 -0.46506,-4.22999 1.14442,-6.50507 l 5.83367,-8.30782 c 6.48995,-9.52306 7.6941,-10.77539 15.47647,-7.84803 z"
id="rect4200"
inkscape:connector-curvature="0" /></g></svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -1,41 +0,0 @@
export const ctoF = (c) => (c * 9) / 5 + 32;
export const mpsToMph = (mps) => (mps * 2.236936).toFixed(2);
export const kmToMiles = (km) => (km / 1.609).toFixed(1);
export const timeTo12HourFormat = (time) => {
let [hours, minutes] = time.split(":");
return `${(hours %= 12) ? hours : 12}:${minutes}`;
};
export const degToCompass = (num) => {
var val = Math.round(num / 22.5);
var arr = [
"N",
"NNE",
"NE",
"ENE",
"E",
"ESE",
"SE",
"SSE",
"S",
"SSW",
"SW",
"WSW",
"W",
"WNW",
"NW",
"NNW",
];
return arr[val % 16];
};
export const unixToLocalTime = (unixSeconds, timezone) => {
let time = new Date((unixSeconds + timezone) * 1000)
.toISOString()
.match(/(\d{2}:\d{2})/)[0];
return time.startsWith("0") ? time.substring(1) : time;
};

View File

@ -1,41 +0,0 @@
import {
unixToLocalTime,
kmToMiles,
mpsToMph,
timeTo12HourFormat,
} from "./converters";
export const getWindSpeed = (unitSystem, windInMps) =>
unitSystem == "metric" ? windInMps : mpsToMph(windInMps);
export const getVisibility = (unitSystem, visibilityInMeters) =>
unitSystem == "metric"
? (visibilityInMeters / 1000).toFixed(1)
: kmToMiles(visibilityInMeters / 1000);
export const getTime = (unitSystem, currentTime, timezone) =>
unitSystem == "metric"
? unixToLocalTime(currentTime, timezone)
: timeTo12HourFormat(unixToLocalTime(currentTime, timezone));
export const getAMPM = (unitSystem, currentTime, timezone) =>
unitSystem === "imperial"
? unixToLocalTime(currentTime, timezone).split(":")[0] >= 12
? "PM"
: "AM"
: "";
export const getWeekDay = (weatherData) => {
const weekday = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
return weekday[
new Date((weatherData.dt + weatherData.timezone) * 1000).getUTCDay()
];
};

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

BIN
src/app/fonts/GeistVF.woff Normal file

Binary file not shown.

70
src/app/globals.css Normal file
View File

@ -0,0 +1,70 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: 'Ubuntu', sans-serif;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@layer base {
:root {
--background: 0 0% 87.18%;
--foreground: 0 0% 12.94%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 26 83.33% 14.12%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 12.94%;
--card: 0 0% 100%;
--card-foreground: 0 0% 12.94%;
--border: 20 0% 52.31%;
--input: 20 21.42% 41.39%;
--primary: 47.9 100% 48.08%;
--primary-foreground: 26 83.3% 14.1%;
--secondary: 26 49.13% 43.5%;
--secondary-foreground: 0 0% 92.55%;
--accent: 60 0% 93.08%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--ring: 20 14.3% 4.1%;
--radius: 0.5rem;
}
.dark {
--background: 20 14.3% 4.1%;
--foreground: 0 0% 92.55%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--card-foreground: 0 0% 92.55%;
--border: 12 6.19% 17.63%;
--input: 12 6.5% 15.1%;
--primary: 60 96.56% 44.61%;
--primary-foreground: 26 90.03% 12.55%;
--secondary: 12 26.8% 16.18%;
--secondary-foreground: 0 0% 92.55%;
--accent: 12 5.7% 26.68%;
--accent-foreground: 0 0% 92.55%;
--destructive: 0 76.12% 38%;
--destructive-foreground: 0 0% 92.55%;
--ring: 47.95 95.82% 53.14%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

26
src/app/layout.tsx Normal file
View File

@ -0,0 +1,26 @@
import type { Metadata } from "next";
import '@fontsource/ubuntu/400.css';
import '@fontsource/ubuntu/500.css';
import "./globals.css";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`antialiased bg-background dark`}
>
{children}
</body>
</html>
);
}

33
src/app/page.tsx Normal file
View File

@ -0,0 +1,33 @@
import Image from "next/image";
import WeatherSprite from "@/components/weather/animated-sprite";
import {Separator} from "@/components/ui/separator";
import {ChevronsLeftRightEllipsis, SeparatorVertical} from "lucide-react";
export default function HomePage() {
return (<main className="flex flex-row w-full h-screen p-2">
<div className={"flex flex-col justify-center items-center gap-1 h-fit w-fit text-xl px-3 py-2 font-bold rounded-xl relative bg-background"}>
<WeatherSprite
weather={3}
title={"Maintenant"}
className={""}
indicator={{type: "slippery"}}
/>
<Separator className={"h-1 rounded-2xl"}/>
<div className={"my-3 p-2 flex flex-col items-center"}>
<h2 className={"mb-2"}>Températures</h2>
<div className={"flex flex-row justify-center items-center gap-1"}>
<div>
<p className={"text-red-300"}>Max</p>
<p className={"text-blue-300"}>Min</p>
</div>
<ChevronsLeftRightEllipsis className={"w-12 h-12 rotate-90"}/>
<div>
<p className={"text-red-300"}>19,6°C</p>
<p className={"text-blue-300"}>11,2°C</p>
</div>
</div>
</div>
<Separator className={"h-1 rounded-2xl"}/>
</div>
</main>);
}

View File

@ -0,0 +1,57 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@ -0,0 +1,141 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@ -0,0 +1,7 @@
"use client"
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }

View File

@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,115 @@
import * as React from "react"
import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
import { Slot } from "@radix-ui/react-slot"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRightIcon />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View File

@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,72 @@
"use client"
import * as React from "react"
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
),
day: cn(
buttonVariants({ variant: "ghost" }),
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
),
day_range_start: "day-range-start",
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />,
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

View File

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,262 @@
"use client"
import * as React from "react"
import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeftIcon className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRightIcon className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

370
src/components/ui/chart.tsx Normal file
View File

@ -0,0 +1,370 @@
"use client"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import {
NameType,
Payload,
ValueType,
} from "recharts/types/component/DefaultTooltipContent"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@ -0,0 +1,155 @@
"use client"
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { MagnifyingGlassIcon } from "@radix-ui/react-icons"
import { Command as CommandPrimitive } from "cmdk"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<MagnifyingGlassIcon className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@ -0,0 +1,204 @@
"use client"
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

View File

@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -0,0 +1,118 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View File

@ -0,0 +1,205 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

178
src/components/ui/form.tsx Normal file
View File

@ -0,0 +1,178 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }

Some files were not shown because too many files have changed in this diff Show More