mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-23 08:32:23 +03:00
WIP: homepage redo
This commit is contained in:
parent
3731b9204a
commit
c9b2179e89
@ -9,6 +9,7 @@
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -1,10 +0,0 @@
|
||||
<svg width="122" height="81" viewBox="0 0 122 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6_651)">
|
||||
<path d="M89.3665 8.06803L121.5 0.35155L66.5111 0.320312L63.7089 7.69502L0.5 5.7032L54.0253 32.9925L36.1529 80.3203L89.3665 8.06803Z" fill="#FFF5D9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6_651">
|
||||
<rect width="121" height="80" fill="white" transform="translate(0.5 0.320312)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 431 B |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#FFF5D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 188 B |
@ -1,18 +1,10 @@
|
||||
import classNames from "classnames"
|
||||
import useHomepageStore from "../store/homepageStore"
|
||||
import { isMobileCheck } from "../utils/dimensions"
|
||||
import AppDisplay from "./AppDisplay"
|
||||
|
||||
const AllApps: React.FC<{ expanded: boolean }> = ({ expanded }) => {
|
||||
const AllApps: React.FC<{ expanded: boolean }> = () => {
|
||||
const { apps } = useHomepageStore()
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
return <div className={classNames('flex-center flex-wrap overflow-y-auto fixed h-screen w-screen backdrop-blur-md transition transition-all ease-in-out duration-500', {
|
||||
'top-[100vh]': !expanded,
|
||||
'top-0': expanded,
|
||||
'gap-4 p-8': isMobile,
|
||||
'gap-8 p-16': !isMobile,
|
||||
})}>
|
||||
return <div>
|
||||
{apps.length === 0
|
||||
? <div>Loading apps...</div>
|
||||
: apps.map(app => <AppDisplay key={app.package_name} app={app} />)}
|
||||
|
@ -1,9 +1,5 @@
|
||||
import classNames from "classnames"
|
||||
import { HomepageApp } from "../store/homepageStore"
|
||||
import { FaHeart, FaRegHeart, } from "react-icons/fa6"
|
||||
import { useState } from "react"
|
||||
import usePersistentStore from "../store/persistentStore"
|
||||
import { isMobileCheck } from "../utils/dimensions"
|
||||
import AppIconPlaceholder from "./AppIconPlaceholder"
|
||||
|
||||
interface AppDisplayProps {
|
||||
@ -11,42 +7,25 @@ interface AppDisplayProps {
|
||||
}
|
||||
|
||||
const AppDisplay: React.FC<AppDisplayProps> = ({ app }) => {
|
||||
const { favoriteApp, favoriteApps } = usePersistentStore();
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
return <a
|
||||
className={classNames("flex-col-center gap-2 relative hover:opacity-90 transition-opacity", {
|
||||
'cursor-pointer': app?.path,
|
||||
'cursor-not-allowed': !app?.path,
|
||||
})}
|
||||
id={app?.package_name}
|
||||
href={app?.path}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{app?.base64_icon
|
||||
? <img
|
||||
src={app.base64_icon}
|
||||
className={classNames('rounded', {
|
||||
'h-8 w-8': isMobile,
|
||||
'h-16 w-16': !isMobile
|
||||
})}
|
||||
/>
|
||||
? <img src={app.base64_icon} />
|
||||
: <AppIconPlaceholder
|
||||
text={app?.state?.our_version || '0'}
|
||||
size={'small'}
|
||||
className="h-16 w-16"
|
||||
/>}
|
||||
<h6>{app?.label || app?.package_name}</h6>
|
||||
{app?.path && isHovered && <button
|
||||
className="absolute p-2 -top-2 -right-2 clear text-sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
favoriteApp(app.package_name)
|
||||
}}
|
||||
>
|
||||
{favoriteApps[app.package_name]?.favorite ? <FaHeart /> : <FaRegHeart />}
|
||||
</button>}
|
||||
</a>
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { isMobileCheck } from '../utils/dimensions';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const AppIconPlaceholder: React.FC<{ text: string, className?: string, size: 'small' | 'medium' | 'large' }> = ({ text, className, size }) => {
|
||||
const AppIconPlaceholder: React.FC<{ text: string }> = ({ text }) => {
|
||||
const index = text.split('').pop()?.toUpperCase() || '0'
|
||||
const derivedFilename = `/icons/${index}`
|
||||
|
||||
@ -11,16 +8,7 @@ const AppIconPlaceholder: React.FC<{ text: string, className?: string, size: 'sm
|
||||
return null
|
||||
}
|
||||
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
return <img
|
||||
src={derivedFilename}
|
||||
className={classNames('m-0 align-self-center rounded-full', {
|
||||
'h-32 w-32': !isMobile && size === 'large',
|
||||
'h-18 w-18': !isMobile && size === 'medium',
|
||||
'h-12 w-12': isMobile || size === 'small',
|
||||
}, className)}
|
||||
/>
|
||||
return <img src={derivedFilename} />
|
||||
}
|
||||
|
||||
export default AppIconPlaceholder
|
@ -1,23 +1,14 @@
|
||||
import useHomepageStore, { HomepageApp } from "../store/homepageStore"
|
||||
import AppDisplay from "./AppDisplay"
|
||||
import usePersistentStore from "../store/persistentStore"
|
||||
import { useEffect, useState } from "react"
|
||||
import { isMobileCheck } from "../utils/dimensions"
|
||||
import classNames from "classnames"
|
||||
import { DragDropContext, Draggable, DropResult, Droppable } from 'react-beautiful-dnd'
|
||||
import { getFetchUrl } from "../utils/fetch"
|
||||
|
||||
const AppsDock: React.FC = () => {
|
||||
const { apps } = useHomepageStore()
|
||||
const { favoriteApps, setFavoriteApps } = usePersistentStore()
|
||||
const [dockedApps, setDockedApps] = useState<HomepageApp[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
let final: HomepageApp[] = []
|
||||
const dockedApps = Object.entries(favoriteApps)
|
||||
.filter(([_, { favorite }]) => favorite)
|
||||
.map(([name, { order }]) => ({ ...apps.find(a => a.package_name === name), order }))
|
||||
.filter(a => a) as HomepageApp[]
|
||||
const orderedApps = dockedApps.filter(a => a.order !== undefined && a.order !== null)
|
||||
const unorderedApps = dockedApps.filter(a => a.order === undefined || a.order === null)
|
||||
|
||||
@ -27,11 +18,8 @@ const AppsDock: React.FC = () => {
|
||||
|
||||
final = final.filter(a => a)
|
||||
unorderedApps.forEach(a => final.push(a))
|
||||
// console.log({ final })
|
||||
setDockedApps(final)
|
||||
}, [apps, favoriteApps])
|
||||
|
||||
const isMobile = isMobileCheck()
|
||||
}, [apps])
|
||||
|
||||
// a little function to help us with reordering the result
|
||||
const reorder = (list: HomepageApp[], startIndex: number, endIndex: number) => {
|
||||
@ -56,18 +44,7 @@ const AppsDock: React.FC = () => {
|
||||
|
||||
const packageNames = items.map(app => app.package_name);
|
||||
|
||||
const faves = { ...favoriteApps }
|
||||
|
||||
packageNames.forEach((name, i) => {
|
||||
// console.log('setting order for', name, 'to', i)
|
||||
faves[name].order = i
|
||||
})
|
||||
|
||||
setFavoriteApps(faves)
|
||||
|
||||
console.log({ favoriteApps })
|
||||
|
||||
fetch(getFetchUrl('/order'), {
|
||||
fetch('/order', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@ -84,13 +61,6 @@ const AppsDock: React.FC = () => {
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
className={classNames('flex-center flex-wrap border border-orange bg-orange/25 p-2 rounded !rounded-xl', {
|
||||
'gap-8': !isMobile && dockedApps.length > 0,
|
||||
'gap-4': !isMobile && dockedApps.length === 0,
|
||||
'mb-4': !isMobile,
|
||||
'gap-4 mb-2': isMobile,
|
||||
'flex-col': dockedApps.length === 0
|
||||
})}
|
||||
>
|
||||
{/*dockedApps.length === 0
|
||||
? <AppDisplay app={apps.find(app => app.package_name === 'app_store')!} />
|
||||
|
@ -1,51 +0,0 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import { hexToRgb, hslToRgb, rgbToHex, rgbToHsl } from '../utils/colors'
|
||||
import { isMobileCheck } from '../utils/dimensions'
|
||||
|
||||
interface ColorDotProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
num: string,
|
||||
}
|
||||
|
||||
const ColorDot: React.FC<ColorDotProps> = ({
|
||||
num,
|
||||
...props
|
||||
}) => {
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
num = (num || '').replace(/(0x|\.)/g, '')
|
||||
|
||||
while (num.length < 6) {
|
||||
num = '0' + num
|
||||
}
|
||||
|
||||
const leftHsl = rgbToHsl(hexToRgb(num.slice(0, 6)))
|
||||
const rightHsl = rgbToHsl(hexToRgb(num.length > 6 ? num.slice(num.length - 6) : num))
|
||||
leftHsl.s = rightHsl.s = 1
|
||||
const leftColor = rgbToHex(hslToRgb(leftHsl))
|
||||
const rightColor = rgbToHex(hslToRgb(rightHsl))
|
||||
|
||||
const angle = (parseInt(num, 16) % 360) || -45
|
||||
|
||||
return (
|
||||
<div {...props} className={classNames('flex', props.className)}>
|
||||
<div
|
||||
className={classNames('m-0 align-self-center border rounded-full outline-black', {
|
||||
'h-16 w-16 border-4': !isMobile,
|
||||
'h-8 w-8 border-2': isMobile
|
||||
})}
|
||||
style={{
|
||||
borderTopColor: leftColor,
|
||||
borderRightColor: rightColor,
|
||||
borderBottomColor: rightColor,
|
||||
borderLeftColor: leftColor,
|
||||
background: `linear-gradient(${angle}deg, ${leftColor} 0 50%, ${rightColor} 50% 100%)`,
|
||||
filter: 'saturate(0.25)',
|
||||
opacity: '0.75'
|
||||
}} />
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ColorDot
|
@ -1,6 +1,4 @@
|
||||
import { FaX } from "react-icons/fa6"
|
||||
import { isMobileCheck } from "../utils/dimensions"
|
||||
import classNames from "classnames"
|
||||
|
||||
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
title: string
|
||||
@ -8,19 +6,12 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
}
|
||||
|
||||
export const Modal: React.FC<Props> = ({ title, onClose, children }) => {
|
||||
const isMobile = isMobileCheck()
|
||||
return (
|
||||
<div className="flex fixed top-0 left-0 w-full h-full bg-black bg-opacity-50 place-items-center place-content-center backdrop-blur-md">
|
||||
<div className={classNames("flex flex-col rounded-lg bg-black py-4 shadow-lg max-h-screen overflow-y-auto", {
|
||||
'min-w-[500px] px-8 w-1/2': !isMobile,
|
||||
'px-4 w-full': isMobile
|
||||
})}>
|
||||
<div className="flex">
|
||||
<h1 className="grow">{title}</h1>
|
||||
<button
|
||||
className="icon self-start"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<h1>{title}</h1>
|
||||
<button onClick={onClose}>
|
||||
<FaX />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,8 +1,5 @@
|
||||
import classNames from "classnames"
|
||||
import { useEffect, useState } from "react"
|
||||
import usePersistentStore from "../store/persistentStore"
|
||||
import useHomepageStore from "../store/homepageStore"
|
||||
import { isMobileCheck } from "../utils/dimensions"
|
||||
|
||||
interface WidgetProps {
|
||||
package_name: string,
|
||||
@ -10,32 +7,20 @@ interface WidgetProps {
|
||||
forceLarge?: boolean
|
||||
}
|
||||
|
||||
const Widget: React.FC<WidgetProps> = ({ package_name, widget, forceLarge }) => {
|
||||
const Widget: React.FC<WidgetProps> = ({ package_name, widget }) => {
|
||||
const { apps } = useHomepageStore()
|
||||
const { widgetSettings } = usePersistentStore()
|
||||
const isMobile = isMobileCheck()
|
||||
const isLarge = forceLarge || widgetSettings[package_name]?.size === "large"
|
||||
const isSmall = !widgetSettings[package_name]?.size || widgetSettings[package_name]?.size === "small"
|
||||
const [tallScreen, setTallScreen] = useState(window.innerHeight > window.innerWidth)
|
||||
const [_tallScreen, setTallScreen] = useState(window.innerHeight > window.innerWidth)
|
||||
|
||||
useEffect(() => {
|
||||
setTallScreen(window.innerHeight > window.innerWidth)
|
||||
}, [window.innerHeight, window.innerWidth])
|
||||
|
||||
return <div
|
||||
className={classNames("self-stretch flex-col-center shadow-lg rounded-lg relative", {
|
||||
"max-w-1/2 min-w-1/2": isLarge && !isMobile,
|
||||
"min-w-1/4": isSmall && !isMobile,
|
||||
"max-w-1/4": isSmall && !tallScreen,
|
||||
'w-full': isMobile
|
||||
})}
|
||||
>
|
||||
<h6 className="flex-center my-2">
|
||||
return <div>
|
||||
<h6>
|
||||
{apps.find(app => app.package_name === package_name)?.label || package_name}
|
||||
</h6>
|
||||
<iframe
|
||||
srcDoc={widget || ""}
|
||||
className="grow self-stretch"
|
||||
data-widget-code={widget}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,18 +1,12 @@
|
||||
import useHomepageStore from "../store/homepageStore"
|
||||
import Widget from "./Widget"
|
||||
import usePersistentStore from "../store/persistentStore"
|
||||
import { isMobileCheck } from "../utils/dimensions"
|
||||
import classNames from "classnames"
|
||||
|
||||
const Widgets = () => {
|
||||
const { apps } = useHomepageStore()
|
||||
const { widgetSettings } = usePersistentStore();
|
||||
const isMobile = isMobileCheck()
|
||||
|
||||
return <div className={classNames("flex-center flex-wrap flex-grow self-stretch", {
|
||||
'gap-2 m-2': isMobile,
|
||||
'gap-4 m-4': !isMobile
|
||||
})}>
|
||||
return <div>
|
||||
{apps
|
||||
.filter(app => app.widget)
|
||||
.map(({ widget, package_name }, _i, _appsWithWidgets) => !widgetSettings[package_name]?.hide && <Widget
|
||||
|
File diff suppressed because one or more lines are too long
@ -2,9 +2,6 @@ import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import Homepage from './pages/Homepage.tsx'
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
||||
import '@unocss/reset/tailwind.css'
|
||||
import 'uno.css'
|
||||
import './index.css'
|
||||
import { Settings } from './pages/Settings.tsx'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
|
@ -6,12 +6,8 @@ import { FaChevronDown, FaChevronUp, FaScrewdriverWrench } from 'react-icons/fa6
|
||||
import AppsDock from '../components/AppsDock'
|
||||
import AllApps from '../components/AllApps'
|
||||
import Widgets from '../components/Widgets'
|
||||
import { isMobileCheck } from '../utils/dimensions'
|
||||
import classNames from 'classnames'
|
||||
import WidgetsSettingsModal from '../components/WidgetsSettingsModal'
|
||||
|
||||
import { getFetchUrl } from '../utils/fetch'
|
||||
|
||||
interface AppStoreApp {
|
||||
package: string,
|
||||
publisher: string,
|
||||
@ -23,28 +19,26 @@ function Homepage() {
|
||||
const [our, setOur] = useState('')
|
||||
const [version, setVersion] = useState('')
|
||||
const [allAppsExpanded, setAllAppsExpanded] = useState(false)
|
||||
const { setApps, fetchHostedStatus, showWidgetsSettings, setShowWidgetsSettings } = useHomepageStore()
|
||||
const isMobile = isMobileCheck()
|
||||
const { setApps, showWidgetsSettings, setShowWidgetsSettings } = useHomepageStore()
|
||||
|
||||
const getAppPathsAndIcons = () => {
|
||||
Promise.all([
|
||||
fetch(getFetchUrl('/apps'), { credentials: 'include' }).then(res => res.json() as any as HomepageApp[]).catch(() => []),
|
||||
fetch(getFetchUrl('/main:app_store:sys/apps'), { credentials: 'include' }).then(res => res.json()).catch(() => []),
|
||||
fetch(getFetchUrl('/version'), { credentials: 'include' }).then(res => res.text()).catch(() => '')
|
||||
fetch('/apps', { credentials: 'include' }).then(res => res.json() as any as HomepageApp[]).catch(() => []),
|
||||
fetch('/main:app_store:sys/apps', { credentials: 'include' }).then(res => res.json()).catch(() => []),
|
||||
fetch('/version', { credentials: 'include' }).then(res => res.text()).catch(() => '')
|
||||
]).then(([appsData, appStoreData, version]) => {
|
||||
// console.log({ appsData, appStoreData, version })
|
||||
|
||||
setVersion(version)
|
||||
|
||||
const appz = appsData.map(app => ({
|
||||
const apps = appsData.map(app => ({
|
||||
...app,
|
||||
is_favorite: false, // Assuming initial state for all apps
|
||||
}));
|
||||
|
||||
appStoreData.forEach((appStoreApp: AppStoreApp) => {
|
||||
const existingAppIndex = appz.findIndex(a => a.package_name === appStoreApp.package);
|
||||
const existingAppIndex = apps.findIndex(a => a.package_name === appStoreApp.package);
|
||||
if (existingAppIndex === -1) {
|
||||
appz.push({
|
||||
apps.push({
|
||||
package_name: appStoreApp.package,
|
||||
path: '',
|
||||
label: appStoreApp.package,
|
||||
@ -52,19 +46,19 @@ function Homepage() {
|
||||
is_favorite: false
|
||||
});
|
||||
} else {
|
||||
appz[existingAppIndex] = {
|
||||
...appz[existingAppIndex],
|
||||
apps[existingAppIndex] = {
|
||||
...apps[existingAppIndex],
|
||||
state: appStoreApp.state
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
setApps(appz);
|
||||
setApps(apps);
|
||||
|
||||
// TODO: be less dumb about this edge case!
|
||||
for (
|
||||
let i = 0;
|
||||
i < 5 && appz.find(a => a.package_name === 'app_store' && !a.base64_icon);
|
||||
i < 5 && apps.find(a => a.package_name === 'app_store' && !a.base64_icon);
|
||||
i++
|
||||
) {
|
||||
getAppPathsAndIcons();
|
||||
@ -77,53 +71,33 @@ function Homepage() {
|
||||
}, [our]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(getFetchUrl('/our'), { credentials: 'include' })
|
||||
fetch('/our', { credentials: 'include' })
|
||||
.then(res => res.text())
|
||||
.then(data => {
|
||||
if (data.match(/^[a-zA-Z0-9\-\.]+\.[a-zA-Z]+$/)) {
|
||||
setOur(data)
|
||||
fetchHostedStatus(data)
|
||||
}
|
||||
})
|
||||
}, [our])
|
||||
|
||||
return (
|
||||
<div className={classNames("flex-col-center relative w-screen overflow-hidden special-bg-homepage min-h-screen", {
|
||||
})}>
|
||||
<h5 className={classNames('absolute flex gap-4 c', {
|
||||
'top-8 left-8 right-8': !isMobile,
|
||||
'top-2 left-2 right-2': isMobile
|
||||
})}>
|
||||
<span>{our}</span>
|
||||
<span className='bg-white/10 rounded p-1'>v{version}</span>
|
||||
<button
|
||||
className="icon ml-auto"
|
||||
onClick={() => setShowWidgetsSettings(true)}
|
||||
>
|
||||
<div>
|
||||
<h5>
|
||||
<span>Hello, {our}</span>
|
||||
<span>v{version}</span>
|
||||
<button onClick={() => setShowWidgetsSettings(true)}>
|
||||
<FaScrewdriverWrench />
|
||||
</button>
|
||||
</h5>
|
||||
{isMobile
|
||||
? <div className='flex-center gap-4 p-8 mt-8 max-w-screen'>
|
||||
<KinodeBird />
|
||||
<KinodeText />
|
||||
</div>
|
||||
: <div className={classNames("flex-col-center mx-0 gap-4 mt-8 mb-4")}>
|
||||
<h3 className='text-center'>Welcome to</h3>
|
||||
<KinodeText />
|
||||
<KinodeBird />
|
||||
</div>}
|
||||
<div>
|
||||
<KinodeBird />
|
||||
<KinodeText />
|
||||
</div>
|
||||
<AppsDock />
|
||||
<Widgets />
|
||||
<button
|
||||
className={classNames("fixed alt clear flex-center self-center z-20", {
|
||||
'bottom-2 right-2': isMobile,
|
||||
'bottom-8 right-8': !isMobile,
|
||||
})}
|
||||
onClick={() => setAllAppsExpanded(!allAppsExpanded)}
|
||||
>
|
||||
<button onClick={() => setAllAppsExpanded(!allAppsExpanded)}>
|
||||
{allAppsExpanded ? <FaChevronDown /> : <FaChevronUp />}
|
||||
<span className="ml-2">{allAppsExpanded ? 'Collapse' : 'All apps'}</span>
|
||||
<span>{allAppsExpanded ? 'Collapse' : 'All apps'}</span>
|
||||
</button>
|
||||
<AllApps expanded={allAppsExpanded} />
|
||||
{showWidgetsSettings && <WidgetsSettingsModal />}
|
||||
|
@ -1,3 +1 @@
|
||||
export const Settings = () => <div>
|
||||
Settings go here!
|
||||
</div>
|
||||
export const Settings = () => <div></div>
|
@ -17,10 +17,6 @@ export interface HomepageStore {
|
||||
get: () => HomepageStore
|
||||
set: (partial: HomepageStore | Partial<HomepageStore>) => void
|
||||
|
||||
isHosted: boolean
|
||||
setIsHosted: (isHosted: boolean) => void
|
||||
fetchHostedStatus: (our: string) => Promise<void>
|
||||
|
||||
apps: HomepageApp[]
|
||||
setApps: (apps: HomepageApp[]) => void
|
||||
showWidgetsSettings: boolean
|
||||
@ -37,19 +33,6 @@ const useHomepageStore = create<HomepageStore>()(
|
||||
setApps: (apps: HomepageApp[]) => set({ apps }),
|
||||
showWidgetsSettings: false,
|
||||
setShowWidgetsSettings: (showWidgetsSettings: boolean) => set({ showWidgetsSettings }),
|
||||
isHosted: false,
|
||||
setIsHosted: (isHosted: boolean) => set({ isHosted }),
|
||||
fetchHostedStatus: async (our: string) => {
|
||||
let hosted = false
|
||||
try {
|
||||
const res = await fetch(`https://${our.replace('.os', '')}.hosting.kinode.net/`)
|
||||
hosted = res.status === 200
|
||||
} catch (error) {
|
||||
// do nothing
|
||||
} finally {
|
||||
set({ isHosted: hosted })
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'homepage_store', // unique name
|
||||
|
@ -13,14 +13,6 @@ export interface PersistentStore {
|
||||
setWidgetSettings: (widgetSettings: PersistentStore['widgetSettings']) => void
|
||||
toggleWidgetVisibility: (package_name: string) => void
|
||||
setWidgetSize: (package_name: string, size: 'small' | 'large') => void,
|
||||
favoriteApps: {
|
||||
[key: string]: {
|
||||
favorite: boolean
|
||||
order?: number
|
||||
}
|
||||
}
|
||||
setFavoriteApps: (favoriteApps: PersistentStore['favoriteApps']) => void
|
||||
favoriteApp: (package_name: string) => void
|
||||
}
|
||||
|
||||
const usePersistentStore = create<PersistentStore>()(
|
||||
@ -29,9 +21,7 @@ const usePersistentStore = create<PersistentStore>()(
|
||||
get,
|
||||
set,
|
||||
widgetSettings: {},
|
||||
favoriteApps: {},
|
||||
setWidgetSettings: (widgetSettings: PersistentStore['widgetSettings']) => set({ widgetSettings }),
|
||||
setFavoriteApps: (favoriteApps: PersistentStore['favoriteApps']) => set({ favoriteApps }),
|
||||
toggleWidgetVisibility: (package_name: string) => {
|
||||
const { widgetSettings } = get()
|
||||
set({
|
||||
@ -56,18 +46,6 @@ const usePersistentStore = create<PersistentStore>()(
|
||||
}
|
||||
})
|
||||
},
|
||||
favoriteApp: async (package_name: string) => {
|
||||
const { favoriteApps } = get()
|
||||
set({
|
||||
favoriteApps: {
|
||||
...favoriteApps,
|
||||
[package_name]: {
|
||||
...favoriteApps[package_name],
|
||||
favorite: !favoriteApps[package_name]?.favorite
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'homepage_persistent_store', // unique name for the store
|
||||
|
@ -1,106 +0,0 @@
|
||||
export type RgbType = { r: number, g: number, b: number }
|
||||
export type HslType = { h: number, s: number, l: number }
|
||||
|
||||
export const rgbToHsl: (rgb: RgbType) => HslType = ({ r, g, b }) => {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
let max = Math.max(r, g, b);
|
||||
let min = Math.min(r, g, b);
|
||||
let d = max - min;
|
||||
let h = 0;
|
||||
if (d === 0) h = 0;
|
||||
else if (max === r) h = ((((g - b) / d) % 6) + 6) % 6; // the javascript modulo operator handles negative numbers differently than most other languages
|
||||
else if (max === g) h = (b - r) / d + 2;
|
||||
else if (max === b) h = (r - g) / d + 4;
|
||||
let l = (min + max) / 2;
|
||||
let s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
|
||||
return { h: h * 60, s, l };
|
||||
}
|
||||
|
||||
export const hslToRgb: (hsl: HslType) => RgbType = ({ h, s, l }) => {
|
||||
let c = (1 - Math.abs(2 * l - 1)) * s;
|
||||
let hp = h / 60.0;
|
||||
let x = c * (1 - Math.abs((hp % 2) - 1));
|
||||
let rgb1 = [0, 0, 0];
|
||||
if (isNaN(h)) rgb1 = [0, 0, 0];
|
||||
else if (hp <= 1) rgb1 = [c, x, 0];
|
||||
else if (hp <= 2) rgb1 = [x, c, 0];
|
||||
else if (hp <= 3) rgb1 = [0, c, x];
|
||||
else if (hp <= 4) rgb1 = [0, x, c];
|
||||
else if (hp <= 5) rgb1 = [x, 0, c];
|
||||
else if (hp <= 6) rgb1 = [c, 0, x];
|
||||
let m = l - c * 0.5;
|
||||
return {
|
||||
r: Math.round(255 * (rgb1[0] + m)),
|
||||
g: Math.round(255 * (rgb1[1] + m)),
|
||||
b: Math.round(255 * (rgb1[2] + m))
|
||||
};
|
||||
}
|
||||
|
||||
export const hexToRgb: (hex: string) => RgbType = (hex) => {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : { r: 0, g: 0, b: 0 };
|
||||
}
|
||||
|
||||
export const rgbToHex: (rgb: RgbType) => string = ({ r, g, b }) => {
|
||||
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
||||
}
|
||||
|
||||
export const generateDistinguishableColors = (numColors: number, mod?: number, sat?: number, val?: number) => {
|
||||
const colors: string[] = [];
|
||||
const saturation = sat || 0.75;
|
||||
const value = val || 0.75;
|
||||
|
||||
for (let i = 0; i < numColors; i++) {
|
||||
const hue = i / numColors * (+(mod || 1) || 1);
|
||||
const [r, g, b] = hsvToRgb(hue, saturation, value);
|
||||
const hexColor = rgbToHex2(r, g, b);
|
||||
colors.push(hexColor);
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
export const hsvToRgb = (h: number, s: number, v: number) => {
|
||||
let [r, g, b] = [0, 0, 0];
|
||||
const i = Math.floor(h * 6);
|
||||
const f = h * 6 - i;
|
||||
const p = v * (1 - s);
|
||||
const q = v * (1 - f * s);
|
||||
const t = v * (1 - (1 - f) * s);
|
||||
|
||||
switch (i % 6) {
|
||||
case 0:
|
||||
(r = v), (g = t), (b = p); // eslint-disable-line
|
||||
break;
|
||||
case 1:
|
||||
(r = q), (g = v), (b = p); // eslint-disable-line
|
||||
break;
|
||||
case 2:
|
||||
(r = p), (g = v), (b = t); // eslint-disable-line
|
||||
break;
|
||||
case 3:
|
||||
(r = p), (g = q), (b = v); // eslint-disable-line
|
||||
break;
|
||||
case 4:
|
||||
(r = t), (g = p), (b = v); // eslint-disable-line
|
||||
break;
|
||||
case 5:
|
||||
(r = v), (g = p), (b = q); // eslint-disable-line
|
||||
break;
|
||||
}
|
||||
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||
}
|
||||
|
||||
export const componentToHex = (c: number) => {
|
||||
const hex = c.toString(16);
|
||||
return hex.length === 1 ? '0' + hex : hex;
|
||||
}
|
||||
|
||||
export const rgbToHex2 = (r: number, g: number, b: number) => {
|
||||
return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b);
|
||||
}
|
@ -1 +0,0 @@
|
||||
export const isMobileCheck = () => window.innerWidth <= 600
|
@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Prepends or strips '/api/' based on the environment.
|
||||
* @param {string} path The original path.
|
||||
* @return {string} The modified path.
|
||||
*/
|
||||
export function getFetchUrl(path: string) {
|
||||
const isDevelopment = import.meta.env.DEV;
|
||||
if (isDevelopment) {
|
||||
return `/api${path}`;
|
||||
}
|
||||
return path.replace(/^\/api/, '');
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
import { defineConfig, presetIcons, presetUno, presetWind, UserConfig, transformerDirectives } from 'unocss'
|
||||
|
||||
const config = {
|
||||
presets: [presetUno(), presetWind(), presetIcons()],
|
||||
shortcuts: [
|
||||
{
|
||||
'flex-center': 'flex justify-center items-center',
|
||||
'flex-col-center': 'flex flex-col justify-center items-center',
|
||||
},
|
||||
],
|
||||
rules: [
|
||||
],
|
||||
theme: {
|
||||
colors: {
|
||||
'white': '#FFF5D9',
|
||||
'black': '#22211F',
|
||||
'orange': '#F35422',
|
||||
'transparent': 'transparent',
|
||||
'gray': '#7E7E7E',
|
||||
},
|
||||
font: {
|
||||
'sans': ['Barlow', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', '"Noto Sans"', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"', '"Noto Color Emoji"'],
|
||||
'serif': ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
|
||||
'mono': ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace'],
|
||||
'heading': ['OpenSans', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', '"Noto Sans"', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"', '"Noto Color Emoji"'],
|
||||
'display': ['Futura', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', '"Noto Sans"', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"', '"Noto Color Emoji"'],
|
||||
},
|
||||
},
|
||||
transformers: [
|
||||
transformerDirectives()
|
||||
],
|
||||
}
|
||||
|
||||
export default defineConfig(config) as UserConfig<(typeof config)['theme']>
|
Loading…
Reference in New Issue
Block a user