diff --git a/app/(standalone)/layout.tsx b/app/(standalone)/layout.tsx new file mode 100644 index 0000000..72dc28a --- /dev/null +++ b/app/(standalone)/layout.tsx @@ -0,0 +1,13 @@ +import { ReactNode } from 'react' + +export default function ObLocationLayout({ + children, +}: { + children: ReactNode +}) { + return ( +
+ { children } +
+ ) +} diff --git a/app/(standalone)/ob-location/page.tsx b/app/(standalone)/ob-location/page.tsx new file mode 100644 index 0000000..c0759e4 --- /dev/null +++ b/app/(standalone)/ob-location/page.tsx @@ -0,0 +1,116 @@ +'use client' + +import { useSearchParams } from 'next/navigation' +import { useEffect, useState } from 'react' +import { noto_sc, rubik } from '@/app/fonts' +import { Button } from '@douyinfe/semi-ui' +import { observerLocation } from '@/app/actions' + +export default function Page() { + const searchParams = useSearchParams() + const ref = searchParams.get('ref') + + const [loading, setLoading] = useState(false) + const [done, setDone] = useState(false) + + const [location, setLocation] = useState(null) + useEffect(() => { + if (!ref) return + (async () => { + await observerLocation(ref, 'pending') + })() + const watcher = navigator.geolocation.watchPosition(setLocation, null, { + enableHighAccuracy: true, + }) + return () => { + navigator.geolocation.clearWatch(watcher) + } + }, [ref]) + + const handleClick = () => { + if (!ref || !location) return + setLoading(true) + observerLocation(ref, { + longitude: location.coords.longitude, + latitude: location.coords.latitude, + altitude: location.coords.altitude, + accuracy: location.coords.accuracy, + altitudeAccuracy: location.coords.altitudeAccuracy, + }) + .then(() => { + setDone(true) + }) + .catch(err => { + console.error(err) + }) + .finally(() => { + setLoading(false) + }) + } + + return !ref ? ( +
+

No ref provided

+
+ ) : ( +
+ + + + + + +

上传地面站位置

+

您正在与电脑网页端同步位置信息

+
+
+

经度

+

+ { location?.coords.longitude.toFixed(6) || 'locating...' } +

+
+
+

纬度

+

+ { location?.coords.latitude.toFixed(6) || 'locating...' } +

+
+
+

海拔

+

+ { location?.coords.altitude?.toFixed(2) || '-' } +

+
+
+

位置精度

+

+ { location?.coords.accuracy.toFixed(2) || '-' } +

+
+
+

海拔精度

+

+ { location?.coords.altitudeAccuracy?.toFixed(2) || '-' } +

+
+
+
+ +
+

+ 您的位置会在服务器临时存储 5 分钟 +

+
+ ) +} diff --git a/app/actions.ts b/app/actions.ts index 44f6814..6f76cfe 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -1,7 +1,10 @@ 'use server' -import {sql} from '@vercel/postgres' -import {GeetestCaptchaSuccess, geetestValidate} from '@/app/geetest' +import { sql } from '@vercel/postgres' +import { GeetestCaptchaSuccess, geetestValidate } from '@/app/geetest' +import { UUID } from '@uniiem/uuid' +import { kv } from '@vercel/kv' +import { ObserverLocationStore } from '@/types/types' export interface Annotation { id: number @@ -31,12 +34,12 @@ export async function newAnnotation( } export async function getAnnotationsByLk(lk: string): Promise { - const {rows} = await sql`SELECT * FROM annotations WHERE lk = ${lk} order by upvote desc, id desc` + const { rows } = await sql`SELECT * FROM annotations WHERE lk = ${lk} order by upvote desc, id desc` return rows || [] } export async function getAnnotationsList(): Promise { - const {rows} = await sql`SELECT * FROM annotations order by upvote desc, id desc` + const { rows } = await sql`SELECT * FROM annotations order by upvote desc, id desc` return rows } @@ -103,3 +106,17 @@ export async function pastebin( .catch(reject) }) } + +export async function observerLocation(ref: UUID, payload?: ObserverLocationStore) { + if (!payload) { + return await kv.get(ref) + } else { + try { + return await kv.set(ref, payload, { + ex: 5 * 60, + }) + } catch (e) { + console.error(e) + } + } +} diff --git a/app/api/satellite/satnogs/[endpoint]/route.ts b/app/api/satellite/satnogs/[endpoint]/route.ts index 0dfea73..e217819 100644 --- a/app/api/satellite/satnogs/[endpoint]/route.ts +++ b/app/api/satellite/satnogs/[endpoint]/route.ts @@ -21,10 +21,13 @@ export async function GET( data, }) } catch (err) { + // return HTTP error return NextResponse.json>({ code: 1, message: (err as Error).message, data: null, + }, { + status: 500, }) } } diff --git a/app/satellites/Main.tsx b/app/satellites/Main.tsx index 2570395..2648acc 100644 --- a/app/satellites/Main.tsx +++ b/app/satellites/Main.tsx @@ -1,17 +1,27 @@ 'use client' import './styles.scss' -import { useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import useSWR from 'swr' import { Icon } from '@iconify-icon/react' +import QRCode from 'react-qr-code' import { noto_sc, rubik } from '@/app/fonts' -import { Input } from '@douyinfe/semi-ui' +import { Banner, Button, Input, Modal, Toast, Tooltip } from '@douyinfe/semi-ui' import { IconSearch } from '@douyinfe/semi-icons' import { BaseResponse, LatestTleSet, Satellite } from '@/app/api/types' import { SatelliteTable } from '@/app/satellites/SatelliteTable' -import dayjs from '@/app/utils/dayjs' +import { UUID, uuidv4 } from '@uniiem/uuid' +import { observerLocation } from '@/app/actions' +import { ObserverLocationStore } from '@/types/types' export const Main = () => { + const [origin, setOrigin] = useState('https://ham-dev.c5r.app') + useEffect(() => { + if (window.location.origin !== origin) { + setOrigin(window.location.origin) + } + }, []) + const compositionRef = useRef({ isComposition: false }) const [filteredValue, setFilteredValue] = useState([]) const [pagination, setPagination] = useState({ @@ -19,33 +29,60 @@ export const Main = () => { pageSize: 10, }) - const tle_case = [ - 'ISS (ZARYA)', - '1 25544U 98067A 23006.23627037 .00014367 00000+0 25952-3 0 9997', - '2 25544 51.6447 50.7420 0004899 231.6381 243.6067 15.49962952376670', - ] - - const startMS = dayjs().unix() * 1000 // current time - const endMS = dayjs().add(1, 'day').unix() * 1000 // 1 day later + const [mobileLocationRefId, setMobileLocationRefId] = useState(null) + const [mobileScanned, setMobileScanned] = useState(false) + useEffect(() => { + let timer = setInterval(() => { + if (!mobileLocationRefId) return + observerLocation(mobileLocationRefId).then(loc => { + if (loc === 'pending') { + setMobileScanned(true) + } else if (typeof loc === 'object' && loc !== null) { + setLocationFromMobile(loc) + setMobileLocationRefId(null) + Toast.success('获取手机位置成功') + } + }) + }, 2000) + return () => { + clearTimeout(timer) + } + }, [mobileLocationRefId]) - // console.log(getPasses(tle_case as TLE, startMS, endMS, 1000)) + const [locationFromBrowser, setLocationFromBrowser] = useState | null>(null) + const [locationError, setLocationError] = useState(null) + const recommendMobileLocation = locationFromBrowser ? locationFromBrowser.accuracy > 1000 : false + useEffect(() => { + navigator.geolocation.getCurrentPosition( + (position) => { + setLocationFromBrowser({ + accuracy: position.coords.accuracy, + altitude: position.coords.altitude, + altitudeAccuracy: position.coords.altitudeAccuracy, + latitude: position.coords.latitude, + longitude: position.coords.longitude, + }) + }, + (error) => { + setLocationError(error.message) + }, + { + enableHighAccuracy: true, + }, + ) + }, []) + const [locationFromMobile, setLocationFromMobile] = useState | null>(null) + const location = locationFromMobile || locationFromBrowser const { data: satellitesData, isLoading: isSatellitesLoading, + error: satellitesError, } = useSWR>('/api/satellite/satnogs/satellites', { refreshWhenHidden: false, refreshWhenOffline: false, }) - // const { - // data: transmittersData, - // isLoading: isTransmittersLoading, - // } = useSWR>(`/api/satellite/satnogs/${ encodeURIComponent('transmitters/?format=json&service=Amateur') }`, { - // refreshWhenHidden: false, - // refreshWhenOffline: false, - // }) - const { data: tleData, isLoading: isTleLoading, @@ -73,37 +110,119 @@ export const Main = () => { setFilteredValue(newFilteredValue) } + // noinspection RequiredAttributes return ( -
-
-

- - 业余无线电卫星数据库 - Amateur Radio Satellites Database -

- } - onCompositionStart={ handleCompositionStart } - onCompositionEnd={ handleCompositionEnd } - onChange={ handleChange } - /> -
-
- { - return a.name.localeCompare(b.name) - } } - /> + <> +
+
+

+ + 业余无线电卫星数据库 + Amateur Radio Satellites Database +

+ } + onCompositionStart={ handleCompositionStart } + onCompositionEnd={ handleCompositionEnd } + onChange={ handleChange } + /> +
+

+ 台站位置: + + { location ? `${ location.latitude.toFixed(6) }, ${ location.longitude.toFixed(6) }` : 'unknown' || 'loading...' } + +

+
+ { !locationFromMobile && (locationError || recommendMobileLocation) && ( + +
+
+
+
+ { satellitesData && satellitesData.code !== 0 && ( + + ) } + + { + return a.name.localeCompare(b.name) + } } + /> +
-
+ setMobileLocationRefId(null) } + closeOnEsc={ true } + > +
+ + { mobileScanned && ( +
+ +

+ 扫码成功,请在手机上确认 +

+
+ ) } +
+
+ ) } \ No newline at end of file diff --git a/app/satellites/SatelliteTable.tsx b/app/satellites/SatelliteTable.tsx index e08bfd0..3dfe4a5 100644 --- a/app/satellites/SatelliteTable.tsx +++ b/app/satellites/SatelliteTable.tsx @@ -1,12 +1,17 @@ 'use client' -import { LatestTleSet, Satellite } from '@/app/api/types' +import './styles.scss' +import { BaseResponse, LatestTleSet, Satellite, Transmitter } from '@/app/api/types' import dayjs from '@/app/utils/dayjs' import { CSSProperties, ReactNode, useEffect, useState } from 'react' import { Icon } from '@iconify-icon/react' import { noto_sc, rubik } from '@/app/fonts' import { IconSpinner } from '@/components/Icon/IconSpinner' -import { Button } from '@douyinfe/semi-ui' +import { Button, SideSheet } from '@douyinfe/semi-ui' +import useSWR, { SWRConfig } from 'swr' +import { SatelliteSighting } from '@/types/types' +import Image from 'next/image' +import { TransponderCard } from '@/app/satellites/TransponderCard' const NationalFlag = ({ countries }: { countries: string }) => { const countriesList = countries.split(',') @@ -86,22 +91,88 @@ const SatelliteTableRow = ({ compact?: boolean }) => { const [expanded, setExpanded] = useState(false) + const [sidePopVisible, setSidePopVisible] = useState(false) + const shimmer = (w: number, h: number) => ` + + + + + + + + + + + + ` + + const toBase64 = (str: string) => + typeof window === 'undefined' + ? Buffer.from(str).toString('base64') + : window.btoa(str) + + const SideLoadingPlaceholder = ({ text, className }: { text: string, className?: string }) => ( +
+ +

{ text }

+
+ ) + + const { + data: sightingData, + isLoading: isSightingLoading, + } = useSWR(sidePopVisible && { + resource: 'https://ham-api.c5r.app/sat/sightings', + init: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + tle0: tle?.tle0, + tle1: tle?.tle1, + tle2: tle?.tle2, + hours: 24, + elevation_threshold: 20, + observer: { + lat: 0, + lon: 0, + alt: 0, + }, + }), + }, + }) + + const { + data: transmittersData, + isLoading: isTransmittersLoading, + } = useSWR>(sidePopVisible && { + resource: `/api/satellite/satnogs/${ encodeURIComponent(`transmitters/?format=json&service=Amateur&sat_id=${ satellite.sat_id }`) }`, + }, { + refreshWhenHidden: false, + refreshWhenOffline: false, + }) + + // noinspection RequiredAttributes return ( <> setExpanded(!expanded) } + className={ `cursor-pointer hover:bg-neutral-50 transition border-y ${ expanded && 'border-b-transparent' }` } >
- -
@@ -121,6 +192,16 @@ const SatelliteTableRow = ({ { satellite.launched ? dayjs(satellite.launched).format('YYYY-MM-DD') : '-' } + + + { expanded && ( -
- 什么都没有 +
+ { satellite.image && ( +
+ { +
+ ) } +
+ { satellite.names && ( + <> +
卫星别名
+
{ satellite.names }
+ + ) } +
NORAD ID
+
{ satellite.norad_cat_id || '未知' }
+
SatNOGS ID
+
{ satellite.sat_id || '未知' }
+
+
+ { satellite.launched && ( + <> +
发射日期
+
{ dayjs(satellite.launched).format('YYYY-MM-DD') }
+ + ) } + { satellite.deployed && ( + <> +
部署日期
+
{ dayjs(satellite.deployed).format('YYYY-MM-DD') }
+ + ) } + { satellite.decayed && ( + <> +
衰变日期
+
{ dayjs(satellite.decayed).format('YYYY-MM-DD') }
+ + ) } + { satellite.associated_satellites.length > 0 && ( + <> +
相关卫星
+
{ satellite.associated_satellites.join(', ') }
+ + ) } +
) } + setSidePopVisible(false) } + size={ 'medium' } + className={ '!w-full md:!w-auto' } + > + { isSightingLoading ? + + : ( + <> +

+ + 未来 24 小时过境 +

+
+ { (sightingData && typeof sightingData.length === 'number') && sightingData.map((sighting, index) => ( +
+
+
+ { dayjs(sighting.rise.time).format('YYYY-MM-DD') } +
+
+
+
+ + { dayjs(sighting.rise.time).format('HH:mm:ss') } +
+
+ + { Math.floor((dayjs(sighting.set.time).unix() - dayjs(sighting.rise.time).unix()) / 60) }min +
+
+ + { dayjs(sighting.set.time).format('HH:mm:ss') } +
+
+
+
+ + : { sighting.rise.azimuth }° +
+
+ + { sighting.culminate.elevation }° +
+
+ + : { sighting.set.azimuth }° +
+
+
+ )) } +
+ + ) } + + { isTransmittersLoading ? + + : ( + <> +

+ + 卫星收发器 +

+
+ { transmittersData?.data && transmittersData.data.map(transponder => ( + + )) } +
+ + ) } +
) } @@ -198,31 +422,40 @@ export const SatelliteTable = ({ 卫星 NORAD ID 发射日期 + { satellites.length === 0 && ( - 暂无数据 + 暂无数据 ) } - { filteredSatellites.slice( - (pagination?.current || 1) * (pagination?.pageSize || 10) - (pagination?.pageSize || 10), - (pagination?.current || 1) * (pagination?.pageSize || 10), - ).map((satellite, index) => ( - i.norad_cat_id === satellite.norad_cat_id) || null } - timestamp={ timestamp } - compact={ compact } - /> - )) } + fetch(resource, init).then(res => res.json()), + } }> + { filteredSatellites.slice( + (pagination?.current || 1) * (pagination?.pageSize || 10) - (pagination?.pageSize || 10), + (pagination?.current || 1) * (pagination?.pageSize || 10), + ).map((satellite, index) => ( + i.norad_cat_id === satellite.norad_cat_id) || null } + timestamp={ timestamp } + compact={ compact } + /> + )) } + - 数据来源于 SatNOGS + 星历数据来源 SatNOGS DB | 卫星过境信息计算接口
diff --git a/app/satellites/TransponderCard.tsx b/app/satellites/TransponderCard.tsx index a7bb11d..fdab275 100644 --- a/app/satellites/TransponderCard.tsx +++ b/app/satellites/TransponderCard.tsx @@ -10,27 +10,40 @@ export const TransponderCard = ({ return (
-
-
+
+
- { transmitter.mode || 'UNKNOWN MODE' } + { transmitter.description || 'Transponder' }
-
-
- +
+ + + { transmitter.mode || 'Unknown mode' } + +
+
+
+ { transmitter.uplink_low || '--' }
-
- +
+ { transmitter.downlink_low || '--' }
diff --git a/app/satellites/styles.scss b/app/satellites/styles.scss index c8115fe..f64a4e4 100644 --- a/app/satellites/styles.scss +++ b/app/satellites/styles.scss @@ -5,3 +5,19 @@ table { @apply text-nowrap; } + +dt { + @apply opacity-80; +} + +dd { + @apply font-medium; +} + +@media (max-width: 448px) { + .semi-sidesheet-inner, + .semi-sidesheet-inner-wrap, + .semi-sidesheet-size-small { + width: 100% !important; + } +} diff --git a/app/swr-provider.tsx b/app/swr-provider.tsx index 8923c1a..4430aae 100644 --- a/app/swr-provider.tsx +++ b/app/swr-provider.tsx @@ -1,6 +1,6 @@ 'use client'; -import {SWRConfig} from 'swr' -import {ReactNode} from 'react'; +import { SWRConfig } from 'swr' +import { ReactNode } from 'react' export const SWRProvider = ({ children, @@ -9,5 +9,7 @@ export const SWRProvider = ({ }) => { return fetch(resource, init).then(res => res.json()), - }}>{children} + } }> + { children } + }; \ No newline at end of file diff --git a/app/utils/sat.ts b/app/utils/sat.ts deleted file mode 100644 index ac9ac0e..0000000 --- a/app/utils/sat.ts +++ /dev/null @@ -1,36 +0,0 @@ -'use client' - -import { getVisibleSatellites, TLE } from 'tle.js' - -export const getPasses = ( - tle: TLE, - startMS: number, - endMS: number, - stepMS: number = 1000, -) => { - const passes = [] - - let ms = startMS - let prevElevation = 0 - - while (ms <= endMS) { - try { - const pass = getVisibleSatellites({ - tles: [tle], - observerLat: 29.590509292552934, - observerLng: 106.31482098268704, - observerHeight: 240, - elevationThreshold: 20, - timestampMS: ms / 1000, - }) - if (pass && pass.length > 0) { - passes.push(pass[0]) - } - } catch (e) { - console.error(e) - } - ms += stepMS - } - - return passes -} \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index d595690..a3ebe82 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,11 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - transpilePackages: ['@douyinfe/semi-ui', '@douyinfe/semi-icons', '@douyinfe/semi-illustrations'] + transpilePackages: ['@douyinfe/semi-ui', '@douyinfe/semi-icons', '@douyinfe/semi-illustrations'], + images: { + domains: [ + 'db-satnogs.freetls.fastly.net' + ] + } }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 3e3cda1..e662f46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@douyinfe/semi-ui": "^2.55.0", "@hamset/maidenhead-locator": "^0.2.1", "@uiw/react-amap": "^6.0.3", + "@uniiem/uuid": "^0.2.1", "@vercel/analytics": "^1.2.2", "@vercel/blob": "^0.22.1", "@vercel/kv": "^1.0.1", @@ -22,12 +23,12 @@ "react": "^18", "react-dom": "^18", "react-markdown": "^9.0.1", + "react-qr-code": "^2.0.12", "react-transition-group": "^4.4.5", "rehype-katex": "^7.0.0", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", - "swr": "^2.2.5", - "tle.js": "^4.9.0" + "swr": "^2.2.5" }, "devDependencies": { "@iconify-icon/react": "^2.0.1", @@ -1500,6 +1501,11 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, + "node_modules/@uniiem/uuid": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@uniiem/uuid/-/uuid-0.2.1.tgz", + "integrity": "sha512-p8DOA3BTkZgvgtOCtK5x7Y2l+GRTFhYrOua70YPiEEUomQFirwxpWrQBst+7oB/iPTeY1zHuF6MKl+mxi0R00A==" + }, "node_modules/@upstash/redis": { "version": "1.25.1", "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.25.1.tgz", @@ -6141,6 +6147,11 @@ "node": ">=6" } }, + "node_modules/qr.js": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", + "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6227,6 +6238,24 @@ "react": ">=18" } }, + "node_modules/react-qr-code": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.12.tgz", + "integrity": "sha512-k+pzP5CKLEGBRwZsDPp98/CAJeXlsYRHM2iZn1Sd5Th/HnKhIZCSg27PXO58zk8z02RaEryg+60xa4vyywMJwg==", + "dependencies": { + "prop-types": "^15.8.1", + "qr.js": "0.0.0" + }, + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x", + "react-native-svg": "*" + }, + "peerDependenciesMeta": { + "react-native-svg": { + "optional": true + } + } + }, "node_modules/react-resizable": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", @@ -6585,11 +6614,6 @@ "node": ">=14.0.0" } }, - "node_modules/satellite.js": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/satellite.js/-/satellite.js-4.1.4.tgz", - "integrity": "sha512-OGUrs1GoGKjy9FYbW2ZaM5slaPfJaYrcxjE3YQTeR/OGyFPCxb6HK22Chao55POU06pdmNAdW5rtKJURse0+XA==" - }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -7105,17 +7129,6 @@ "node": ">=0.8" } }, - "node_modules/tle.js": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/tle.js/-/tle.js-4.9.0.tgz", - "integrity": "sha512-W3pLz/hvmo8LfACbXZqW86Q1DmdfD87xv5VGjONilZMzHDz29p3k3YW4exEz+SueDn1O+vByk/sFbO4/jIbaOg==", - "dependencies": { - "satellite.js": "^4.1.3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index 42660f3..1081495 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@douyinfe/semi-ui": "^2.55.0", "@hamset/maidenhead-locator": "^0.2.1", "@uiw/react-amap": "^6.0.3", + "@uniiem/uuid": "^0.2.1", "@vercel/analytics": "^1.2.2", "@vercel/blob": "^0.22.1", "@vercel/kv": "^1.0.1", @@ -24,12 +25,12 @@ "react": "^18", "react-dom": "^18", "react-markdown": "^9.0.1", + "react-qr-code": "^2.0.12", "react-transition-group": "^4.4.5", "rehype-katex": "^7.0.0", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", - "swr": "^2.2.5", - "tle.js": "^4.9.0" + "swr": "^2.2.5" }, "devDependencies": { "@iconify-icon/react": "^2.0.1", diff --git a/types/types.ts b/types/types.ts index e69de29..8d833a9 100644 --- a/types/types.ts +++ b/types/types.ts @@ -0,0 +1,23 @@ +export type ObserverLocationStore = { + longitude: number, + latitude: number, + altitude?: number | null, + accuracy: number, + altitudeAccuracy?: number | null, +} | 'pending' + +export interface SatelliteSighting { + rise: SightingDetail; + culminate: SightingDetail; + set: SightingDetail; +} + +export interface SightingDetail { + time: Date; + lat: number; + lon: number; + elevation: number; + azimuth: number; + distance: number; +} +