inital commit

This commit is contained in:
tmddn3070 2024-06-24 13:23:00 +09:00
commit 78c2abe791
26 changed files with 7594 additions and 0 deletions

3
.eslintrc.json Normal file
View File

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

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/node_modules
/.next/

7
lib/utils.ts Normal file
View File

@ -0,0 +1,7 @@
// lib/utils.ts
import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

5
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

4541
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "timerweb",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"clsx": "^2.1.1",
"framer-motion": "^11.2.11",
"next": "12.3.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwind-merge": "^2.3.0"
},
"devDependencies": {
"@types/node": "18.7.18",
"@types/react": "18.0.20",
"@types/react-dom": "18.0.6",
"autoprefixer": "^10.4.19",
"eslint": "8.23.1",
"eslint-config-next": "12.3.1",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.4",
"typescript": "4.8.3"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

158
public/global.css Normal file
View File

@ -0,0 +1,158 @@
/* global */
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css");
html {
overflow: hidden;
font-family: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
}
body {
margin: 0;
}
a {
text-decoration: none;
color: inherit;
}
/* util */
div.divider {
width: 100%;
height: 1px;
background-color: rgba(0, 0, 0, 0.12);
}
label.error {
margin-left: 10px;
color: red;
width: auto;
white-space: nowrap;
}
/* main page */
div.main-container {
height: 100%;
padding: 10px;
}
div.content-container {
margin-left: 20px;
justify-content: center;
}
div.content-container > iframe {
border: 2px solid gray;
border: none;
border-bottom: 1px solid black;
height: 85px;
}
form.embed-config-form li {
margin: 5px auto;
}
form.embed-config-form input {
width: 150px;
display: inline-block;
}
form.embed-config-form input:only-of-type:not([type=color]) {
width: 200px;
}
form.embed-config-form input[type=color] {
width: 50px;
}
form.embed-config-form label {
width: 100px;
display: inline-block;
}
div.clipboard-bar {
width: 100%;
display: flex;
margin: 20px auto;
flex-direction: column;
justify-content: flex-start;
}
div.clipboard-bar > div {
width: 100%;
height: 30px;
border: 2px solid black;
display: flex;
align-items: stretch;
margin: 5px 0;
}
div.clipboard-bar div.icon {
padding: 5px;
border-right: 1px solid black;
background-color: gray;
}
div.clipboard-bar div.icon svg {
width: 20px;
height: 20px;
}
div.clipboard-bar span.text {
padding: 5px;
white-space: nowrap;
overflow-x: scroll;
width: 50vw;
font-size: 0.8rem;
scrollbar-width: none;
}
div.clipboard-bar span.text::-webkit-scrollbar {
display: none;
}
/* Digit style */
div.timer-container {
max-width: 380px;
padding: 10px;
margin: 10px auto;
/* 보더 대신 그림자로 대체 */
box-shadow: 0 0 1px rgba(0, 0, 0, 0.1);
border-radius: 10px;
}
div.timer-container > div.title {
display: flex;
font-weight: bold;
}
div.timer-container > div.timer {
width: 100%;
display: flex;
justify-content: center;
align-items: first baseline;
font-family: 'Share Tech Mono';
font-size: 30px;
height: 2rem;
}
div.digit {
transition: opacity, color 500ms;
margin: 0 0.5rem;
width: 30px;
text-align: center;
}
div.digit:hover {
opacity: 0.5;
}

4
public/vercel.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,36 @@
import React from 'react'
import ClipboardIcon from './icons/ClipboardIcon';
function copyContent(text: string) {
return new Promise<string>((res, rej) => {
navigator.clipboard.writeText(text)
.then(() => res('복사 성공!'))
.catch(err => rej(`복사 실패: ${err}`))
})
}
const ClipCopybar: React.FC<{ text: string, icon?: JSX.Element }> = ({ text, icon = <ClipboardIcon /> }) => {
const [clickResult, setClickResult] = React.useState('');
const handleCopyClick = () => {
copyContent(text)
.then(res => setClickResult(res))
.catch(err => setClickResult(err))
.then(() => setTimeout(() => setClickResult(''), 1000))
}
return (
<>
<div>
<div className='icon' onClick={handleCopyClick}>
{icon}
</div>
<span className='text'>
<a href={text}>{text}</a>
</span>
</div>
{clickResult}
</>
)
}
export default ClipCopybar;

View File

@ -0,0 +1,24 @@
import React from 'react';
import ClipCopybar from './ClipCopybar';
import LinkIcon from './icons/LinkIcon';
import GithubIcon from './icons/GithubIcon';
export interface ClipboardCopybarProps {
time: string
title: string
color: string
}
const ClipboardCopybar: React.FC<ClipboardCopybarProps> = ({ time, title, color }) => {
const link = `https://timer.runa.pw/embed/?date=${time}&title=${title}&color=${color}`;
const text = `<iframe src='${link}' style='border: none; height: 85px;'></iframe>`;
return (
<div className='clipboard-bar flex gap-3 mt-5 text-sm w-min'>
<ClipCopybar text={text} />
<ClipCopybar text={link} icon={<LinkIcon />} />
</div>
)
}
export default ClipboardCopybar;

View File

@ -0,0 +1,32 @@
import React from 'react'
const ColorPickInput: React.FC<{
color: string,
setColor: React.Dispatch<React.SetStateAction<string>>
}> = ({ color, setColor }) => {
const [input, setInput] = React.useState<JSX.Element>(<></>);
React.useEffect(() => {
setInput(<input type='color' value={color} onChange={handleColorChange} className='text-black border border-gray-300 rounded h-10 w-10' />);
}, [color]);
const handleColorChange: React.ChangeEventHandler<HTMLInputElement> = ({ target: { value } }) => {
setColor(value);
const elem = document.querySelector('form.embed-config-form > ul > li > label.error');
if (elem) elem.innerHTML = /^#?[a-f|A-F|0-9]{6}$/.test(value) ? '' : 'invalid format!';
}
return (
<>
<label> </label>
<br />
<div className='flex items-center gap-2'>
<input type='text' value={color} onChange={handleColorChange} className='text-black border border-gray-300 rounded px-3 py-2' />
{input}
</div>
<label className='error'></label>
</>
)
}
export default ColorPickInput;

37
src/components/Digit.tsx Normal file
View File

@ -0,0 +1,37 @@
import React from 'react'
import useInterval from '../hooks/useInterval';
export interface DigitProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
getter: () => string
ignoreBlink?: boolean
}
const Digit: React.FC<DigitProps> = ({ getter, ignoreBlink = false, ...props }) => {
const [time, setTime] = React.useState<string>('00');
const ref = React.useRef<HTMLDivElement>(null);
useInterval(() => {
const remainingTime = getter();
if (time !== remainingTime) {
setTime(remainingTime);
if (ignoreBlink) {
if (ref.current?.className.includes('blink')) ref.current.className = 'digit'
}
else if (ref.current) {
ref.current.className = 'digit blink';
setTimeout(() => {
if (ref.current) ref.current.className = 'digit'
}, 200);
}
}
}, 10);
return (
<div ref={ref} className='digit blink' {...props}>
{time}
</div>
)
}
export default Digit;

View File

@ -0,0 +1,40 @@
const ClipboardIcon: React.FC = () => (
<svg
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
xmlSpace="preserve"
x="0px" y="0px"
viewBox="0 0 64 64"
enableBackground="new 0 0 64 64"
>
<g id="Text-files">
<path d="M53.9791489,9.1429005H50.010849c-0.0826988,0-0.1562004,0.0283995-0.2331009,0.0469999V5.0228
C49.7777481,2.253,47.4731483,0,44.6398468,0h-34.422596C7.3839517,0,5.0793519,2.253,5.0793519,5.0228v46.8432999
c0,2.7697983,2.3045998,5.0228004,5.1378999,5.0228004h6.0367002v2.2678986C16.253952,61.8274002,18.4702511,64,21.1954517,64
h32.783699c2.7252007,0,4.9414978-2.1725998,4.9414978-4.8432007V13.9861002
C58.9206467,11.3155003,56.7043495,9.1429005,53.9791489,9.1429005z M7.1110516,51.8661003V5.0228
c0-1.6487999,1.3938999-2.9909999,3.1062002-2.9909999h34.422596c1.7123032,0,3.1062012,1.3422,3.1062012,2.9909999v46.8432999
c0,1.6487999-1.393898,2.9911003-3.1062012,2.9911003h-34.422596C8.5049515,54.8572006,7.1110516,53.5149002,7.1110516,51.8661003z
M56.8888474,59.1567993c0,1.550602-1.3055,2.8115005-2.9096985,2.8115005h-32.783699
c-1.6042004,0-2.9097996-1.2608986-2.9097996-2.8115005v-2.2678986h26.3541946
c2.8333015,0,5.1379013-2.2530022,5.1379013-5.0228004V11.1275997c0.0769005,0.0186005,0.1504021,0.0469999,0.2331009,0.0469999
h3.9682999c1.6041985,0,2.9096985,1.2609005,2.9096985,2.8115005V59.1567993z"/>
<path d="M38.6031494,13.2063999H16.253952c-0.5615005,0-1.0159006,0.4542999-1.0159006,1.0158005
c0,0.5615997,0.4544001,1.0158997,1.0159006,1.0158997h22.3491974c0.5615005,0,1.0158997-0.4542999,1.0158997-1.0158997
C39.6190491,13.6606998,39.16465,13.2063999,38.6031494,13.2063999z"/>
<path d="M38.6031494,21.3334007H16.253952c-0.5615005,0-1.0159006,0.4542999-1.0159006,1.0157986
c0,0.5615005,0.4544001,1.0159016,1.0159006,1.0159016h22.3491974c0.5615005,0,1.0158997-0.454401,1.0158997-1.0159016
C39.6190491,21.7877007,39.16465,21.3334007,38.6031494,21.3334007z"/>
<path d="M38.6031494,29.4603004H16.253952c-0.5615005,0-1.0159006,0.4543991-1.0159006,1.0158997
s0.4544001,1.0158997,1.0159006,1.0158997h22.3491974c0.5615005,0,1.0158997-0.4543991,1.0158997-1.0158997
S39.16465,29.4603004,38.6031494,29.4603004z"/>
<path d="M28.4444485,37.5872993H16.253952c-0.5615005,0-1.0159006,0.4543991-1.0159006,1.0158997
s0.4544001,1.0158997,1.0159006,1.0158997h12.1904964c0.5615025,0,1.0158005-0.4543991,1.0158005-1.0158997
S29.0059509,37.5872993,28.4444485,37.5872993z"/>
</g>
</svg>
)
export default ClipboardIcon;

View File

@ -0,0 +1,9 @@
const GithubIcon: React.FC = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
/>
</svg>
)
export default GithubIcon;

View File

@ -0,0 +1,7 @@
const LinkIcon: React.FC = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M8,12a1,1,0,0,0,1,1h6a1,1,0,0,0,0-2H9A1,1,0,0,0,8,12Zm2,3H7A3,3,0,0,1,7,9h3a1,1,0,0,0,0-2H7A5,5,0,0,0,7,17h3a1,1,0,0,0,0-2Zm7-8H14a1,1,0,0,0,0,2h3a3,3,0,0,1,0,6H14a1,1,0,0,0,0,2h3A5,5,0,0,0,17,7Z" />
</svg>
)
export default LinkIcon;

17
src/hooks/useInterval.ts Normal file
View File

@ -0,0 +1,17 @@
import React from 'react';
function useInterval(callback: Function, delay: number) {
const savedCallback = React.useRef<Function>();
React.useEffect(() => {
savedCallback.current = callback;
}, [callback]);
React.useEffect(() => {
if (delay === null) return;
const id = setInterval(() => savedCallback.current?.call(null), delay);
return () => clearInterval(id);
}, [delay]);
}
export default useInterval;

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

@ -0,0 +1,15 @@
import type { AppProps } from 'next/app'
import Head from 'next/head';
import 'public/global.css'
function MyApp({ Component, pageProps }: AppProps) {
return <>
<Head>
<title></title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Component {...pageProps} />
</>
}
export default MyApp

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

@ -0,0 +1,18 @@
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html>
<Head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link href="https://fonts.googleapis.com/css2?family=Orbitron&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Share%20Tech%20Mono&display=swap" rel="stylesheet" />
</Head>
<body className='font-orbitron justify-center flex bg-gray-300'>
<Main />
<NextScript />
</body>
</Html>
)
}

66
src/pages/embed.tsx Normal file
View File

@ -0,0 +1,66 @@
import { useRouter } from 'next/router';
import React from 'react'
import Digit from '../components/Digit';
import { getRemainingTime } from '../utils/getRemainingTime';
import useInterval from '../hooks/useInterval';
const Title: React.FC<{ format: string, date: string }> = ({ format, date }) => {
const [title, setTitle] = React.useState('');
useInterval(() => {
setTitle(format.replace(/(?:^\$|[^\\]\$)(D|H|M|S)/gi, (_, p: string, index: number, string: string) => {
const obj: Record<string, number> = {
d: getRemainingTime(date, 'day', true),
D: getRemainingTime(date, 'day', true),
H: getRemainingTime(date, 'hour', true),
h: getRemainingTime(date, 'hour'),
M: getRemainingTime(date, 'minute', true),
m: getRemainingTime(date, 'minute'),
S: getRemainingTime(date, 'second', true),
s: getRemainingTime(date, 'second')
}
return (index === 0 ? '' : string[index]) + (obj[p] ?? '').toString();
}))
}, 10);
if (title === '') return null;
return <div className='title'>{title}</div>
}
const TimerEmbed: React.FC = () => {
const { query } = useRouter();
const date = query.date?.toString() || "";
const title = query.title?.toString() || "";
const color = query.color?.toString().replaceAll('#', '') || '000000';
return (
<div className='timer-container bg-white'>
<Title format={title} date={date} />
<div className='timer' style={{ color: '#' + color }}>
{date && !/\d{8},\d{6}/.test(date) ? (
<span className='text-red-500'> </span>
) : (
<>
<Digit getter={() => getRemainingTime(date, 'day').toString().padStart(2, '0')} />
:
<Digit getter={() => getRemainingTime(date, 'hour').toString().padStart(2, '0')} />
:
<Digit getter={() => getRemainingTime(date, 'minute').toString().padStart(2, '0')} />
:
<Digit getter={() => getRemainingTime(date, 'second').toString().padStart(2, '0')} style={{ marginRight: 0 }} />
<div className='flex transform -translate-x-1 items-baseline'>
.
<Digit
ignoreBlink
getter={() => Math.round(getRemainingTime(date, 'milisecond') / 10).toString().slice(0, 2).padStart(2, '0')}
style={{ textAlign: 'left', fontSize: '0.5em', margin: 0, width: '20px' }}
/>
</div>
</>
)}
</div>
</div>
)
}
export default TimerEmbed;

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

@ -0,0 +1,53 @@
import React from 'react';
import ClipboardCopybar from '../components/ClipboardCopybar';
import ColorPickInput from '../components/ColorPickInput';
const MainPage: React.FC = () => {
const [time, setTime] = React.useState('20240703,082000');
const [color, setColor] = React.useState('000000');
const [title, setTitle] = React.useState('기말까지');
return (
<div className='flex flex-col items-center justify-center text-black bg-gray-900 p-5 gap-5 h-screen w-screen'>
<div className='w-1/2 p-5 rounded-lg shadow bg-gray-50'>
<h2 className='text-2xl mb-3 font-bold'> </h2>
<form className='mb-5'>
<ul>
<li className='mb-3'>
<label className='text-lg'></label>
<input
className='text-black border border-gray-300 rounded px-3 py-2 w-full'
type='datetime-local'
defaultValue={'2024-07-03T08:20'}
onChange={ev =>
setTime(ev.target.value.replaceAll(
/[(\-|:)|(T)]/g,
(match) => match === 'T' ? ',' : ''
) + '00')
}
/>
</li>
<li className='mb-3'>
<ColorPickInput color={color} setColor={setColor} />
</li>
<li className='mb-3'>
<label className='text-lg'></label>
<input
type='text'
defaultValue='기말까지'
onChange={ev => setTitle(ev.target.value)}
className='text-black border border-gray-300 rounded px-3 py-2 w-full'
/>
</li>
</ul>
</form>
<iframe src={`/embed/?date=${time}&title=${encodeURIComponent(title)}&color=${encodeURIComponent(color)}`} style={{ border: 'none', height: '95px', backgroundColor: "#D1D5DB", justifySelf: 'center' }} className='w-full' />
<div className='w-full h-px bg-gray-50 mt-8 flex items-center justify-center' />
<ClipboardCopybar time={time} title={title} color={color} />
</div>
</div>
);
}
export default MainPage;

View File

@ -0,0 +1,21 @@
const parseDate = (date: string) => {
return new Date(+date.slice(0, 4), +date.slice(4, 6) - 1, +date.slice(6, 8), +date.slice(9, 11), +date.slice(11, 13), +date.slice(13, 15));
};
type timeType = 'day' | 'hour' | 'minute' | 'second' | 'milisecond';
const cal: Record<timeType, (n: number, all: boolean) => number> = {
day: n => n / (1000 * 60 * 60 * 24),
hour: (n, a) => n / (1000 * 60 * 60) % (a ? Infinity : 24),
minute: (n, a) => n / (1000 * 60) % (a ? Infinity : 60),
second: (n, a) => n / 1000 % (a ? Infinity : 60),
milisecond: (n, a) => n % (a ? Infinity : 1000),
};
export const getRemainingTime = (date: string, type: timeType, all = false) => {
const currentDate = new Date();
const targetDate = parseDate(date);
const timeDifference = targetDate.getTime() - currentDate.getTime();
let result = Math.max(Math.floor(cal[type](timeDifference, all)), 0);
//print out the result
return Math.max(Math.floor(cal[type](timeDifference, all)), 0);
};

15
tailwind.config.js Normal file
View File

@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
// Or if using `src` directory:
"./src/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [],
}

20
tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

2430
yarn.lock Normal file

File diff suppressed because it is too large Load Diff