iphone-xudale 0.2.9
Sign up to get free protection for your applications and to get access to all the features.
- package/.eslintrc.json +3 -0
- package/app/(pages)/options/page.tsx +720 -0
- package/app/(pages)/popup/page.tsx +166 -0
- package/app/(pages)/tips/page.tsx +40 -0
- package/app/components/DropListBox.tsx +157 -0
- package/app/components/SVGPlay.tsx +14 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +27 -0
- package/app/layout.tsx +15 -0
- package/app/page.tsx +3 -0
- package/app/scripts/content/checkoutSteps.ts +341 -0
- package/app/scripts/content/doFroApplePages.ts +206 -0
- package/app/scripts/content/getPageInitInfo.ts +49 -0
- package/app/scripts/content/getStoreCanPickInfo.ts +251 -0
- package/app/scripts/content/goOrderSteps.ts +179 -0
- package/app/scripts/content/index.ts +56 -0
- package/app/scripts/content/playSystemNotifacation.ts +39 -0
- package/app/scripts/content/sendSelfNotificatioin.ts +27 -0
- package/app/scripts/inject/index.ts +18 -0
- package/app/shared/constants.ts +236 -0
- package/app/shared/interface.ts +25 -0
- package/app/shared/location/city.json +1774 -0
- package/app/shared/location/county.json +17115 -0
- package/app/shared/location/province.json +94 -0
- package/app/shared/util.ts +93 -0
- package/buildAfter.js +86 -0
- package/bunBuild.ts +54 -0
- package/extension/content-script.js +7 -0
- package/extension/favicon.ico +0 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon19-disable.png +0 -0
- package/extension/icons/icon19.png +0 -0
- package/extension/icons/icon32.png +0 -0
- package/extension/icons/icon38.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/inject-script.js +1 -0
- package/extension/manifest.json +38 -0
- package/extension/service-worker.js +62 -0
- package/extension.next.config.js +25 -0
- package/icon_generator.py +30 -0
- package/middleware.ts +21 -0
- package/next.config.js +15 -0
- package/package.json +67 -0
- package/postcss.config.js +6 -0
- package/public/assets/images/SCR-20230916-nbkz.png +0 -0
- package/public/assets/images/SCR-20230916-nbyv.png +0 -0
- package/public/assets/images/SCR-20230916-ncte.png +0 -0
- package/public/assets/images/SCR-20230916-ndgw.png +0 -0
- package/public/assets/images/SCR-20230916-ndks.png +0 -0
- package/public/assets/images/SCR-20230916-neaa.png +0 -0
- package/public/assets/images/SCR-20230916-neeq.jpeg +0 -0
- package/public/assets/images/SCR-20230916-nfkt.png +0 -0
- package/public/assets/images/SCR-20230919-ulfn.png +0 -0
- package/public/assets/images/SCR-20230919-ulzd.png +0 -0
- package/public/assets/images/SCR-20230919-uocr.png +0 -0
- package/public/icon_original.png +0 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/tailwind.config.ts +20 -0
- package/tsconfig.json +27 -0
- package/types/global.d.ts +19 -0
@@ -0,0 +1,166 @@
|
|
1
|
+
'use client'
|
2
|
+
import { restoreFromStorage, saveToStorage } from '@/app/shared/util'
|
3
|
+
import { defaultiPhoneOrderConfig, storeKeys } from '@/app/shared/constants'
|
4
|
+
import { useEffect, useState } from 'react'
|
5
|
+
import { Match_URL } from '@/app/shared/constants'
|
6
|
+
import { IPHONEORDER_CONFIG } from '@/app/shared/interface'
|
7
|
+
import SVGPlay from '@/app/components/SVGPlay'
|
8
|
+
|
9
|
+
const Popup = () => {
|
10
|
+
const [orderEnabled, setOrderEnable] = useState<boolean>(false)
|
11
|
+
const [config, setConfig] = useState<IPHONEORDER_CONFIG>(defaultiPhoneOrderConfig)
|
12
|
+
// 异步获取enable状态
|
13
|
+
useEffect(() => {
|
14
|
+
const getOrderEnable = async () => {
|
15
|
+
const isEnabled = await restoreFromStorage(storeKeys.orderEnabled)
|
16
|
+
const config = await restoreFromStorage(storeKeys.orderConfig)
|
17
|
+
setOrderEnable(!!isEnabled)
|
18
|
+
setConfig(config as IPHONEORDER_CONFIG)
|
19
|
+
}
|
20
|
+
getOrderEnable()
|
21
|
+
}, [])
|
22
|
+
|
23
|
+
const handleOptionClick = () => {
|
24
|
+
if (typeof chrome !== 'undefined' && chrome?.runtime) {
|
25
|
+
chrome.runtime.openOptionsPage()
|
26
|
+
} else {
|
27
|
+
console.log(`please open in chrome`)
|
28
|
+
}
|
29
|
+
}
|
30
|
+
const handleConfirm = () => {
|
31
|
+
if (
|
32
|
+
!orderEnabled ||
|
33
|
+
(config?.lastName &&
|
34
|
+
config?.mobile &&
|
35
|
+
config?.firstName &&
|
36
|
+
config?.appleId &&
|
37
|
+
config?.last4code &&
|
38
|
+
config?.cityName &&
|
39
|
+
config?.districtName &&
|
40
|
+
config?.provinceName)
|
41
|
+
) {
|
42
|
+
confirmAsync(orderEnabled)
|
43
|
+
} else {
|
44
|
+
setOrderEnable(false)
|
45
|
+
alert(`请先配置必要信息`)
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
const handleTestNotification = () => {
|
50
|
+
if (typeof chrome !== 'undefined' && chrome?.tabs) {
|
51
|
+
// @ts-ignore
|
52
|
+
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
|
53
|
+
chrome.tabs.sendMessage(tabs[0].id, {
|
54
|
+
data: 'playSystemNotification',
|
55
|
+
voiceInfo: config?.voiceInfo || {},
|
56
|
+
})
|
57
|
+
})
|
58
|
+
chrome.runtime.sendMessage()
|
59
|
+
}
|
60
|
+
|
61
|
+
console.log(`chrome?.runtime`, chrome?.tabs)
|
62
|
+
}
|
63
|
+
|
64
|
+
return (
|
65
|
+
<div className="mx-auto mt-1 mb-2 w-[18rem] h-[15rem]">
|
66
|
+
<main className="flex w-fit flex-col items-center gap-2 justify-between py-3 px-2 mx-auto mb-2 mt-2">
|
67
|
+
<div className="flex w-full gap-3 justify-center py-1 mb-2 bg-white border-b border-solid border-slate-300">
|
68
|
+
<div
|
69
|
+
className={`flex w-40 h-9 ${'bg-indigo-600 cursor-pointer'} bg-opacity-90 border border-indigo-500 rounded-md my-2 items-center align-middle justify-center text-center min-w-min px-3 hover:shadow-md hover:bg-indigo-500`}
|
70
|
+
onClick={handleTestNotification}
|
71
|
+
>
|
72
|
+
<SVGPlay className="w-5 h-5 mr-2" />
|
73
|
+
通知测试
|
74
|
+
</div>
|
75
|
+
</div>
|
76
|
+
<div className="flex flex-row gap-3 h-10 justify-between mb-2 px-4 py-6 rounded-xl bg-slate-100">
|
77
|
+
<div className="flex text-gray-600 items-center text-base font-bold">开启自动抢购</div>
|
78
|
+
<SelectItem
|
79
|
+
enabled={orderEnabled}
|
80
|
+
index={0}
|
81
|
+
callback={({ enabled }) => {
|
82
|
+
setOrderEnable(enabled)
|
83
|
+
}}
|
84
|
+
/>
|
85
|
+
</div>
|
86
|
+
</main>
|
87
|
+
<div className="w-full flex flex-row justify-center text-center items-center gap-5 text-sm">
|
88
|
+
<div
|
89
|
+
className={`flex w-1/3 h-9 bg-white text-indigo-500 cursor-pointer bg-opacity-90 border-4 border-t-2 border-indigo-500 rounded-3xl my-2 items-center align-middle justify-center text-center min-w-min px-3 hover:shadow-md hover:border-t-[3px] hover:border-b-[3px]`}
|
90
|
+
onClick={handleOptionClick}
|
91
|
+
>
|
92
|
+
配置
|
93
|
+
</div>
|
94
|
+
<div
|
95
|
+
className={`flex w-1/3 h-9 ${'bg-indigo-600 cursor-pointer'} bg-opacity-90 border border-indigo-500 rounded-3xl my-2 items-center align-middle justify-center text-center min-w-min px-3 hover:shadow-md hover:bg-indigo-500`}
|
96
|
+
onClick={handleConfirm}
|
97
|
+
>
|
98
|
+
确认
|
99
|
+
</div>
|
100
|
+
</div>
|
101
|
+
</div>
|
102
|
+
)
|
103
|
+
}
|
104
|
+
|
105
|
+
export default Popup
|
106
|
+
|
107
|
+
interface ISelectItemProps {
|
108
|
+
enabled?: boolean
|
109
|
+
index: number
|
110
|
+
callback: ({ enabled }: { enabled: boolean }) => void
|
111
|
+
}
|
112
|
+
const SelectItem = ({ enabled, callback }: ISelectItemProps) => {
|
113
|
+
const handleToggle = () => {
|
114
|
+
callback({
|
115
|
+
enabled: !enabled,
|
116
|
+
})
|
117
|
+
}
|
118
|
+
return (
|
119
|
+
<div className="flex flex-row gap-6">
|
120
|
+
<div className="w-20 flex-col justify-center items-end gap-1.5 inline-flex">
|
121
|
+
{enabled ? (
|
122
|
+
<div
|
123
|
+
className="w-14 h-7 bg-indigo-600 bg-opacity-70 rounded-2xl py-0.5 px-[0.2rem] flex relative cursor-pointer ease-linear duration-500 shadow-indigo-500/50 shadow-md"
|
124
|
+
onClick={handleToggle}
|
125
|
+
>
|
126
|
+
<div className="w-6 h-6 bg-gray-100 rounded-full pt-1 pb-2 px-1 ease-linear duration-300 ml-[1.65rem]"></div>
|
127
|
+
</div>
|
128
|
+
) : (
|
129
|
+
<div
|
130
|
+
className="w-14 h-7 bg-slate-400 bg-opacity-50 rounded-2xl py-0.5 px-[0.2rem] flex relative cursor-pointer ease-linear duration-500 shadow-slate-500/50 shadow-md"
|
131
|
+
onClick={handleToggle}
|
132
|
+
>
|
133
|
+
<div className="w-6 h-6 bg-gray-100 rounded-full pt-1 pb-2 px-1 ease-linear duration-300 ml-0"></div>
|
134
|
+
</div>
|
135
|
+
)}
|
136
|
+
</div>
|
137
|
+
</div>
|
138
|
+
)
|
139
|
+
}
|
140
|
+
|
141
|
+
interface ICondirmLoadProps {
|
142
|
+
callback?: (msg: string) => void
|
143
|
+
}
|
144
|
+
const confirmLoad = async ({ callback }: ICondirmLoadProps) => {
|
145
|
+
let msg = ``
|
146
|
+
if (typeof chrome === 'undefined' || !chrome?.tabs) {
|
147
|
+
msg = 'Please use as chrome extension'
|
148
|
+
callback && callback(msg)
|
149
|
+
return
|
150
|
+
}
|
151
|
+
|
152
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
|
153
|
+
const tabUrl = (tab?.url || '').toLowerCase()
|
154
|
+
const urlObj = new URL(tabUrl)
|
155
|
+
const isInMatchUrl = urlObj.href.includes(Match_URL)
|
156
|
+
|
157
|
+
if (isInMatchUrl) {
|
158
|
+
chrome.tabs.reload(tab.id)
|
159
|
+
return
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
163
|
+
const confirmAsync = async (orderEnabled: boolean) => {
|
164
|
+
await saveToStorage(orderEnabled, storeKeys.orderEnabled)
|
165
|
+
window.close()
|
166
|
+
}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
'use client'
|
2
|
+
import { useEffect, useState } from 'react'
|
3
|
+
import { iframeMessagePass } from '@/app/shared/constants'
|
4
|
+
|
5
|
+
const Tips = () => {
|
6
|
+
const [fetchCount, setFetchCount] = useState(0)
|
7
|
+
const [beforeReload, setBeforeReload] = useState(50)
|
8
|
+
useEffect(() => {
|
9
|
+
window.addEventListener('message', function (event) {
|
10
|
+
if (event.data.action === iframeMessagePass.messageAction) {
|
11
|
+
setFetchCount(event.data.count)
|
12
|
+
setBeforeReload(event.data.beforeReload)
|
13
|
+
}
|
14
|
+
})
|
15
|
+
}, [])
|
16
|
+
|
17
|
+
const bgTransparent = `body{background: transparent}`
|
18
|
+
if (!fetchCount)
|
19
|
+
return (
|
20
|
+
<div>
|
21
|
+
<style>{bgTransparent}</style>
|
22
|
+
</div>
|
23
|
+
)
|
24
|
+
return (
|
25
|
+
<main className="flex w-52 h-fit flex-col text-gray-700 items-center gap-1 px-2 justify-between py-2 mx-auto rounded-lg bg-slate-100 text-base font-bold cursor-pointer shadow-slate-500/50 shadow-lg">
|
26
|
+
<style>{bgTransparent}</style>
|
27
|
+
<div className="flex flex-row gap-0 h-8 items-center justify-center py-2 ">
|
28
|
+
<div className=" ">
|
29
|
+
当前正在重试:
|
30
|
+
<span className=" text-indigo-700">{fetchCount > 9 ? fetchCount : '0' + fetchCount}</span>次
|
31
|
+
</div>
|
32
|
+
</div>
|
33
|
+
<div className="flex flex-row h-8">
|
34
|
+
<span className="text-indigo-700">{beforeReload}</span>次之后将刷新页面
|
35
|
+
</div>
|
36
|
+
</main>
|
37
|
+
)
|
38
|
+
}
|
39
|
+
|
40
|
+
export default Tips
|
@@ -0,0 +1,157 @@
|
|
1
|
+
'use client'
|
2
|
+
import { Fragment, useEffect, useState } from 'react'
|
3
|
+
import { Listbox, Transition } from '@headlessui/react'
|
4
|
+
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'
|
5
|
+
import { find as _find } from 'lodash'
|
6
|
+
|
7
|
+
type DropItem = {
|
8
|
+
id: number | string
|
9
|
+
name: string
|
10
|
+
icon?: string
|
11
|
+
} & Record<string, any>
|
12
|
+
|
13
|
+
interface IDropListBoxProps {
|
14
|
+
domID?: string
|
15
|
+
title?: string
|
16
|
+
itemList: Array<DropItem>
|
17
|
+
selectedIndex?: number
|
18
|
+
|
19
|
+
callback?: (item: DropItem) => void
|
20
|
+
}
|
21
|
+
export default function DropListBox({ domID, title, itemList, selectedIndex, callback }: IDropListBoxProps) {
|
22
|
+
const [selected, setSelected] = useState(itemList[selectedIndex || 0])
|
23
|
+
|
24
|
+
const handleSelect = (item: DropItem) => {
|
25
|
+
setSelected(item)
|
26
|
+
if (callback) callback(item)
|
27
|
+
}
|
28
|
+
|
29
|
+
// useEffect(() => {
|
30
|
+
// let newItem: DropItem
|
31
|
+
// if (selectedIndex !== undefined) {
|
32
|
+
// newItem = itemList[selectedIndex]
|
33
|
+
// } else {
|
34
|
+
// newItem = itemList[0]
|
35
|
+
// }
|
36
|
+
// setSelected(newItem)
|
37
|
+
// if (callback) callback(newItem)
|
38
|
+
// }, [itemList, selectedIndex])
|
39
|
+
|
40
|
+
// useEffect(() => {
|
41
|
+
// console.log(`selectedIndex`, selectedIndex)
|
42
|
+
// // 当之前的selected对象在新的itemList中存在时,不做处理
|
43
|
+
// if(!_find(itemList, _item=>{
|
44
|
+
// return _item.id == selected.id && _item.name == selected.name
|
45
|
+
// })){
|
46
|
+
// let newItem: DropItem
|
47
|
+
// if (selectedIndex !== undefined && itemList[selectedIndex]) {
|
48
|
+
// newItem = itemList[selectedIndex]
|
49
|
+
// }else{
|
50
|
+
// newItem = itemList[0]
|
51
|
+
// }
|
52
|
+
// setSelected(newItem)
|
53
|
+
// if (callback) callback(newItem)
|
54
|
+
// }
|
55
|
+
// }, [itemList])
|
56
|
+
|
57
|
+
useEffect(() => {
|
58
|
+
if (selectedIndex && itemList[selectedIndex]) {
|
59
|
+
setSelected(itemList[selectedIndex])
|
60
|
+
}
|
61
|
+
}, [selectedIndex])
|
62
|
+
|
63
|
+
useEffect(() => {
|
64
|
+
// 表示itemList已经变更
|
65
|
+
if (
|
66
|
+
!_find(itemList, _item => {
|
67
|
+
return _item.id == selected.id && _item.name == selected.name
|
68
|
+
})
|
69
|
+
) {
|
70
|
+
let newItem: DropItem = (selectedIndex !== undefined ? itemList[selectedIndex] : itemList[0]) || itemList[0]
|
71
|
+
setSelected(newItem)
|
72
|
+
}
|
73
|
+
}, [itemList])
|
74
|
+
|
75
|
+
return (
|
76
|
+
<Listbox value={selected} onChange={handleSelect}>
|
77
|
+
{({ open }) => (
|
78
|
+
<>
|
79
|
+
{title ? (
|
80
|
+
<Listbox.Label className="block text-sm font-medium leading-6 text-gray-900">
|
81
|
+
{title}
|
82
|
+
</Listbox.Label>
|
83
|
+
) : null}
|
84
|
+
<div className="relative mt-2">
|
85
|
+
<Listbox.Button
|
86
|
+
id={domID || ''}
|
87
|
+
className="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 sm:text-sm sm:leading-6"
|
88
|
+
>
|
89
|
+
<span className="flex items-start">
|
90
|
+
{selected?.icon ? (
|
91
|
+
<img src={selected.icon} alt="" className="h-5 w-5 flex-shrink-0 rounded-full" />
|
92
|
+
) : null}
|
93
|
+
<span className=" block truncate h-6">{selected.name}</span>
|
94
|
+
</span>
|
95
|
+
<span className="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2">
|
96
|
+
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
97
|
+
</span>
|
98
|
+
</Listbox.Button>
|
99
|
+
|
100
|
+
<Transition
|
101
|
+
show={open}
|
102
|
+
as={Fragment}
|
103
|
+
leave="transition ease-in duration-100"
|
104
|
+
leaveFrom="opacity-100"
|
105
|
+
leaveTo="opacity-0"
|
106
|
+
>
|
107
|
+
<Listbox.Options className="absolute z-10 mt-1 max-h-56 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
108
|
+
{itemList.map(item => (
|
109
|
+
<Listbox.Option
|
110
|
+
key={item.id}
|
111
|
+
className={({ active }) =>
|
112
|
+
`${
|
113
|
+
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
|
114
|
+
} relative cursor-default select-none py-2 pl-3 pr-9`
|
115
|
+
}
|
116
|
+
value={item}
|
117
|
+
>
|
118
|
+
{({ selected, active }) => (
|
119
|
+
<>
|
120
|
+
<div className="flex items-center">
|
121
|
+
{item.icon ? (
|
122
|
+
<img
|
123
|
+
src={item.icon}
|
124
|
+
alt=""
|
125
|
+
className="h-5 w-5 flex-shrink-0 rounded-full"
|
126
|
+
/>
|
127
|
+
) : null}
|
128
|
+
<span
|
129
|
+
className={`${
|
130
|
+
selected ? 'font-semibold' : 'font-normal'
|
131
|
+
} 'ml-3 block truncate`}
|
132
|
+
>
|
133
|
+
{item.name}
|
134
|
+
</span>
|
135
|
+
</div>
|
136
|
+
|
137
|
+
{selected ? (
|
138
|
+
<span
|
139
|
+
className={`${
|
140
|
+
active ? 'text-white' : 'text-indigo-600'
|
141
|
+
} absolute inset-y-0 right-0 flex items-center pr-4`}
|
142
|
+
>
|
143
|
+
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
144
|
+
</span>
|
145
|
+
) : null}
|
146
|
+
</>
|
147
|
+
)}
|
148
|
+
</Listbox.Option>
|
149
|
+
))}
|
150
|
+
</Listbox.Options>
|
151
|
+
</Transition>
|
152
|
+
</div>
|
153
|
+
</>
|
154
|
+
)}
|
155
|
+
</Listbox>
|
156
|
+
)
|
157
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
const SVGPlay = ({ className }: { className?: string }) => {
|
2
|
+
const svgTest = `
|
3
|
+
<svg width="64" height="64" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
4
|
+
<path fill="#ffffff" d="M2.93 17.07A10 10 0 1 1 17.07 2.93A10 10 0 0 1 2.93 17.07zm12.73-1.41A8 8 0 1 0 4.34 4.34a8 8 0 0 0 11.32 11.32zM7 6l8 4l-8 4V6z"/>
|
5
|
+
</svg>
|
6
|
+
`
|
7
|
+
|
8
|
+
const base64String = btoa(svgTest)
|
9
|
+
|
10
|
+
const imgSrc = `data:image/svg+xml;base64,${base64String}`
|
11
|
+
return <img src={imgSrc} className={className || ''} />
|
12
|
+
}
|
13
|
+
|
14
|
+
export default SVGPlay
|
package/app/favicon.ico
ADDED
Binary file
|
package/app/globals.css
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
@tailwind base;
|
2
|
+
@tailwind components;
|
3
|
+
@tailwind utilities;
|
4
|
+
|
5
|
+
:root {
|
6
|
+
--foreground-rgb: 255, 255, 255;
|
7
|
+
--background-start-rgb: 255, 255, 255;
|
8
|
+
--background-end-rgb: 255, 255, 255;
|
9
|
+
}
|
10
|
+
|
11
|
+
@media (prefers-color-scheme: dark) {
|
12
|
+
:root {
|
13
|
+
--foreground-rgb: 255, 255, 255;
|
14
|
+
--background-start-rgb: 255, 255, 255;
|
15
|
+
--background-end-rgb: 255, 255, 255;
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
body {
|
20
|
+
color: rgb(var(--foreground-rgb));
|
21
|
+
background: linear-gradient(
|
22
|
+
to bottom,
|
23
|
+
transparent,
|
24
|
+
rgb(var(--background-end-rgb))
|
25
|
+
)
|
26
|
+
rgb(var(--background-start-rgb));
|
27
|
+
}
|
package/app/layout.tsx
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
import './globals.css'
|
2
|
+
import type { Metadata } from 'next'
|
3
|
+
|
4
|
+
export const metadata: Metadata = {
|
5
|
+
title: 'iPhoneOrder',
|
6
|
+
description: 'iPhoneOrder',
|
7
|
+
}
|
8
|
+
|
9
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
10
|
+
return (
|
11
|
+
<html lang="en">
|
12
|
+
<body className={''}>{children}</body>
|
13
|
+
</html>
|
14
|
+
)
|
15
|
+
}
|
package/app/page.tsx
ADDED