Initial commit
This commit is contained in:
parent
501fbdb0e1
commit
99a12e2194
30
.github/workflows/build-and-deploy.yml
vendored
Normal file
30
.github/workflows/build-and-deploy.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
name: Node.js CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
npm ci --prefix site
|
||||||
|
npm run build --prefix site
|
||||||
|
npm run deploy --prefix site
|
||||||
|
|
||||||
|
- name: Generate App List
|
||||||
|
run: |
|
||||||
|
npm ci --prefix processing
|
||||||
|
node processing
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
uses: JamesIves/github-pages-deploy-action@v4
|
||||||
|
with:
|
||||||
|
branch: gh-pages
|
||||||
|
folder: site/out
|
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# build output
|
||||||
|
dist
|
||||||
|
.next
|
||||||
|
target
|
||||||
|
packages/next/wasm/@next
|
||||||
|
out
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# logs & pids
|
||||||
|
*.log
|
||||||
|
pids
|
||||||
|
*.cpuprofile
|
||||||
|
|
||||||
|
# coverage
|
||||||
|
.nyc_output
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# test output
|
||||||
|
test/**/out*
|
||||||
|
test/**/next-env.d.ts
|
||||||
|
.DS_Store
|
||||||
|
/e2e-tests
|
||||||
|
test/tmp/**
|
||||||
|
|
||||||
|
# Editors
|
||||||
|
**/.idea
|
||||||
|
**/.#*
|
||||||
|
.nvmrc
|
||||||
|
|
||||||
|
# examples
|
||||||
|
examples/**/out
|
||||||
|
examples/**/.env*.local
|
||||||
|
|
||||||
|
pr-stats.md
|
||||||
|
test-timings.json
|
||||||
|
|
||||||
|
# Vercel
|
||||||
|
.vercel
|
||||||
|
.now
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
.swc/
|
15
README.md
Normal file
15
README.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Kasm Apps
|
||||||
|
|
||||||
|
This is a repository of the apps supported by Kasm. The apps list is automatically generated and can be used when creating new workspaces or using the 1 click installer
|
||||||
|
|
||||||
|
## Create your own app store
|
||||||
|
|
||||||
|
We have tried to make it as simple as possible for people to create their own app stores that work with Kasm, the easiest way to do that is to follow these steps:
|
||||||
|
1. Fork this repository. You can also clone it, but forking it will make it easier for people to discover your app store.
|
||||||
|
1. Enable workflows in the `Actions` tab.
|
||||||
|
1. Remove the default apps in `/apps/` and add your own.
|
||||||
|
1. Once you have made your first commit, the workflow will run and build your own App Store landing page and generate the apps.json file. Even just making a change to the README will trigger the build, this step is required before you can set up github pages.
|
||||||
|
1. Go to `Settings / Pages` and under `Branch` select `gh-pages` then hit Save.
|
||||||
|
1. Wait a few minutes for the Action to run building your page (you can see the progress in the Actions tab) and then you should be presented with your URL.
|
||||||
|
|
||||||
|
Note: If you want to use a domain / subdomain, you will need to modify `site/next.config.js` and remove the line `basePath: '/kasm-apps',`.
|
7
apps/README.md
Normal file
7
apps/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Apps directory
|
||||||
|
|
||||||
|
This directory is for storing all the files needed for your apps store, they should be stored with the following structure:
|
||||||
|
|
||||||
|
* apps/Application Name
|
||||||
|
* apps/Application Name/app.json
|
||||||
|
* apps/Application Name/application-name.png
|
1451
processing/package-lock.json
generated
Normal file
1451
processing/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
processing/package.json
Normal file
19
processing/package.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "Kasm-Apps",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Generate json file",
|
||||||
|
"main": "processjson.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"folder-hash": "^4.0.2",
|
||||||
|
"glob": "^7.1.6",
|
||||||
|
"ncu": "^0.2.1"
|
||||||
|
}
|
||||||
|
}
|
68
processing/processjson.js
Normal file
68
processing/processjson.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const glob = require("glob");
|
||||||
|
const { hashElement } = require("folder-hash");
|
||||||
|
const nextConfig = require("../site/next.config.js")
|
||||||
|
|
||||||
|
var dir = "./site/out";
|
||||||
|
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(dir + "/icons")) {
|
||||||
|
fs.mkdirSync(dir + "/icons");
|
||||||
|
}
|
||||||
|
|
||||||
|
glob("**/app.json", async function (err, files) {
|
||||||
|
if (err) {
|
||||||
|
console.log(
|
||||||
|
"cannot read the folder, something goes wrong with glob",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let apptotal = files.length;
|
||||||
|
let apps = [];
|
||||||
|
let promises = [];
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
algho: "sha1",
|
||||||
|
encoding: "hex",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
//files.forEach(async function(file) {
|
||||||
|
|
||||||
|
let folder = file.replace("/app.json", "");
|
||||||
|
|
||||||
|
let hash = await hashElement(folder, options);
|
||||||
|
let filedata = fs.readFileSync(file);
|
||||||
|
|
||||||
|
let parsed = JSON.parse(filedata);
|
||||||
|
parsed.sha = hash.hash;
|
||||||
|
console.log(parsed.name + ' added')
|
||||||
|
apps.push(parsed);
|
||||||
|
|
||||||
|
if (fs.existsSync(folder + "/" + parsed.image_src)) {
|
||||||
|
let imagedata = fs.readFileSync(folder + "/" + parsed.image_src);
|
||||||
|
fs.writeFileSync(dir + "/icons/" + parsed.image_src, imagedata);
|
||||||
|
} else {
|
||||||
|
console.error("missing file: ".folder + "/" + parsed.image_src);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = {
|
||||||
|
name: nextConfig.env.name || 'Unknown store',
|
||||||
|
appcount: apptotal,
|
||||||
|
icon: nextConfig.env.icon || null,
|
||||||
|
description: nextConfig.env.description || null,
|
||||||
|
list_url: nextConfig.env.listUrl || null,
|
||||||
|
contact_url: nextConfig.env.contactUrl || null,
|
||||||
|
modified: Date.now(),
|
||||||
|
apps: apps,
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = JSON.stringify(json);
|
||||||
|
|
||||||
|
fs.writeFileSync(dir + "/list.json", data);
|
||||||
|
});
|
1
site/.env.local
Normal file
1
site/.env.local
Normal file
@ -0,0 +1 @@
|
|||||||
|
NEXT_PUBLIC_APPURL=apps.kasmweb.com/list.json
|
34
site/README.md
Normal file
34
site/README.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
|
||||||
|
|
||||||
|
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
19
site/components/App.js
Normal file
19
site/components/App.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
function App({ Component, pageProps, app }) {
|
||||||
|
return (
|
||||||
|
<div className="w-[245px] h-[88px] transition-all relative cursor-pointer group flex p-2 items-center justify-center bg-slate-100/90 dark:bg-slate-900/90 shadow rounded hover:shadow-xl hover:bg-gradient-to-r hover:from-slate-900 hover:to-cyan-800 hover:text-white">
|
||||||
|
<div className="w-full h-full">
|
||||||
|
<div className="show-grid flex h-full items-center">
|
||||||
|
<div className="kasmcard-img flex h-full mx-4 items-center justify-center">
|
||||||
|
<img className="w-[50px] max-h-[66px]" src={ 'icons/' + app.image_src} />
|
||||||
|
</div>
|
||||||
|
<div className="kasmcard-detail settingPad">
|
||||||
|
<h5 className="text-base">{ app.friendly_name }</h5>
|
||||||
|
<p className="text-xs opacity-50">{ app.categories[0] }</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
18
site/components/Bubbles.js
Normal file
18
site/components/Bubbles.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
function Bubbles() {
|
||||||
|
return (
|
||||||
|
<ul className="bg-bubbles">
|
||||||
|
<li></li>
|
||||||
|
<li></li>
|
||||||
|
<li></li>
|
||||||
|
<li></li>
|
||||||
|
<li></li>
|
||||||
|
<li></li>
|
||||||
|
<li></li>
|
||||||
|
<li></li>
|
||||||
|
<li></li>
|
||||||
|
<li></li>
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Bubbles
|
5
site/components/footer.js
Normal file
5
site/components/footer.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default function Footer() {
|
||||||
|
return (
|
||||||
|
<footer></footer>
|
||||||
|
)
|
||||||
|
}
|
72
site/components/header.js
Normal file
72
site/components/header.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import Bubbles from '../components/Bubbles'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
export default function Header({ searchText, changeSearch }) {
|
||||||
|
|
||||||
|
const copyToClipboard = () => {
|
||||||
|
var textField = document.createElement('textarea')
|
||||||
|
textField.innerText = listUrl
|
||||||
|
document.body.appendChild(textField)
|
||||||
|
textField.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
textField.remove()
|
||||||
|
alert('URL copied to clipboard')
|
||||||
|
}
|
||||||
|
const listUrl = process.env.listUrl;
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="relative font-light overflow-hidden bg-gradient-to-tr from-slate-900 to-cyan-800 p-32 py-8 text-white flex justify-between items-center">
|
||||||
|
<Bubbles />
|
||||||
|
<div className='relative z-10'>
|
||||||
|
<div className="text-3xl">{process.env.name}</div>
|
||||||
|
<div className="text-sm uppercase w-full flex justify-between">
|
||||||
|
<span className='opacity-70'>A</span>
|
||||||
|
<span className='opacity-70'>p</span>
|
||||||
|
<span className='opacity-70'>p</span>
|
||||||
|
<span className='opacity-70'>l</span>
|
||||||
|
<span className='opacity-70'>i</span>
|
||||||
|
<span className='opacity-70'>c</span>
|
||||||
|
<span className='opacity-70'>a</span>
|
||||||
|
<span className='opacity-70'>t</span>
|
||||||
|
<span className='opacity-70'>i</span>
|
||||||
|
<span className='opacity-70'>o</span>
|
||||||
|
<span className='opacity-70'>n</span>
|
||||||
|
<span> </span>
|
||||||
|
<span className='opacity-40'>D</span>
|
||||||
|
<span className='opacity-40'>a</span>
|
||||||
|
<span className='opacity-40'>t</span>
|
||||||
|
<span className='opacity-40'>a</span>
|
||||||
|
<span className='opacity-40'>b</span>
|
||||||
|
<span className='opacity-40'>a</span>
|
||||||
|
<span className='opacity-40'>s</span>
|
||||||
|
<span className='opacity-40'>e</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav className='relative z-10 mx-12'>
|
||||||
|
<Link href="/" className={'p-4 rounded-full border border-solid' + (router.pathname == "/" ? ' border-white/30' : ' border-transparent')}>Library</Link>
|
||||||
|
<Link href="/addapp" className={'p-4 rounded-full border border-solid' + (router.pathname == "/addapp" ? ' bg-black/10 border-white/30' : ' border-transparent')}>Add App</Link>
|
||||||
|
</nav>
|
||||||
|
<div className="grow flex justify-center relative z-10">
|
||||||
|
<div className='bg-black/10 shadow border border-1 border-white/30 rounded flex w-full max-w-md'>
|
||||||
|
<input
|
||||||
|
name="search"
|
||||||
|
className='bg-transparent shadow-inner text-lg font-light w-full p-4 placeholder:text-white/40'
|
||||||
|
placeholder='Search for application'
|
||||||
|
type="text"
|
||||||
|
value={searchText}
|
||||||
|
onChange={changeSearch}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<button className='p-4 relative z-10 px-5 bg-emerald-600 m-2 rounded items-center text-white/70 flex cursor-pointer' onClick={() => { copyToClipboard() }}>
|
||||||
|
<span className="mr-3">App Store Link</span>
|
||||||
|
<svg style={{ height: '14px', fill: '#fff' }} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M224 0c-35.3 0-64 28.7-64 64V288c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H224zM64 160c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H288c35.3 0 64-28.7 64-64V384H288v64H64V224h64V160H64z" /></svg>
|
||||||
|
</button>
|
||||||
|
</header >
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
14
site/components/layout.js
Normal file
14
site/components/layout.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// components/layout.js
|
||||||
|
|
||||||
|
import Header from './header'
|
||||||
|
import Footer from './footer'
|
||||||
|
|
||||||
|
export default function Layout({ children, searchText, changeSearch }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header searchText={searchText} changeSearch={changeSearch} />
|
||||||
|
<main>{children}</main>
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
20
site/next.config.js
Normal file
20
site/next.config.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
|
||||||
|
const nextConfig = {
|
||||||
|
env: {
|
||||||
|
name: 'Kasm Technologies',
|
||||||
|
description: 'The official store for Kasm supported applications.',
|
||||||
|
icon: '/img/logo.svg',
|
||||||
|
listUrl: 'https://apps.kasmweb.com/list.json',
|
||||||
|
contactUrl: 'https://kasmweb.com/support',
|
||||||
|
},
|
||||||
|
reactStrictMode: true,
|
||||||
|
swcMinify: true,
|
||||||
|
basePath: '/kasm-apps',
|
||||||
|
trailingSlash: true,
|
||||||
|
images: {
|
||||||
|
unoptimized: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
7712
site/package-lock.json
generated
Normal file
7712
site/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
site/package.json
Normal file
26
site/package.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build && next export",
|
||||||
|
"deploy": "next build && touch out/.nojekyll",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
|
"next": "13.0.0",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-select": "^5.6.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.13",
|
||||||
|
"eslint": "8.26.0",
|
||||||
|
"eslint-config-next": "13.0.0",
|
||||||
|
"postcss": "^8.4.18",
|
||||||
|
"tailwindcss": "^3.2.1"
|
||||||
|
}
|
||||||
|
}
|
20
site/pages/_app.js
Normal file
20
site/pages/_app.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import '../styles/globals.css'
|
||||||
|
import Layout from '../components/layout'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
|
||||||
|
function MyApp({ Component, pageProps }) {
|
||||||
|
const [searchText, setSearchText] = useState('')
|
||||||
|
|
||||||
|
const changeSearch = event => {
|
||||||
|
setSearchText(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout searchText={searchText} changeSearch={changeSearch}>
|
||||||
|
<Component searchText={searchText} {...pageProps} />
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyApp
|
251
site/pages/addapp.js
Normal file
251
site/pages/addapp.js
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
import Head from 'next/head'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
import CreatableSelect from 'react-select/creatable';
|
||||||
|
|
||||||
|
export default function AddApp() {
|
||||||
|
|
||||||
|
function friendlyUrl(url) {
|
||||||
|
// make the url lowercase
|
||||||
|
var encodedUrl = url.toString().toLowerCase();
|
||||||
|
// replace & with and
|
||||||
|
encodedUrl = encodedUrl.split(/\&+/).join("-and-")
|
||||||
|
// remove invalid characters
|
||||||
|
encodedUrl = encodedUrl.split(/[^a-z0-9]/).join("-");
|
||||||
|
// remove duplicates
|
||||||
|
encodedUrl = encodedUrl.split(/-+/).join("-");
|
||||||
|
// trim leading & trailing characters
|
||||||
|
encodedUrl = encodedUrl.trim('-');
|
||||||
|
return encodedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadZip = () => {
|
||||||
|
var JSZip = require("jszip");
|
||||||
|
const zip = new JSZip()
|
||||||
|
const folder = zip.folder(application.friendly_name)
|
||||||
|
folder.file('app.json', JSON.stringify(application, null, 2))
|
||||||
|
if (icon) {
|
||||||
|
folder.file(application.image_src, icon.file)
|
||||||
|
}
|
||||||
|
zip.generateAsync({ type: "blob" })
|
||||||
|
.then(function (content) {
|
||||||
|
// Force down of the Zip file
|
||||||
|
saveAs(content, friendlyUrl(application.friendly_name) + '.zip');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [icon, setIcon] = useState(null)
|
||||||
|
const [ext, setExt] = useState('png')
|
||||||
|
|
||||||
|
const [application, setApplication] = useState({
|
||||||
|
friendly_name: null,
|
||||||
|
image_src: null,
|
||||||
|
description: null,
|
||||||
|
name: null,
|
||||||
|
cores: 2,
|
||||||
|
memory: 2768,
|
||||||
|
gpu_count: 0,
|
||||||
|
cpu_allocation_method: "Inherit",
|
||||||
|
docker_registry: "https://index.docker.io/v1/",
|
||||||
|
volume_mappings: "{}",
|
||||||
|
run_config: "{}",
|
||||||
|
exec_config: "{}",
|
||||||
|
categories: [],
|
||||||
|
require_gpu: false,
|
||||||
|
enabled: true,
|
||||||
|
restrict_to_network: false,
|
||||||
|
restrict_network_names: "[]",
|
||||||
|
allow_network_selection: false,
|
||||||
|
notes: null,
|
||||||
|
image_type: 'Container',
|
||||||
|
})
|
||||||
|
|
||||||
|
const displayApplication = () => {
|
||||||
|
return {
|
||||||
|
...application,
|
||||||
|
categories: JSON.stringify(application.categories)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const customStyles = {
|
||||||
|
control: (base, state) => ({
|
||||||
|
...base,
|
||||||
|
background: "#f1f5f9",
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
borderColor: "#94a3b8"
|
||||||
|
}),
|
||||||
|
multiValue: (styles, { data }) => {
|
||||||
|
return {
|
||||||
|
...styles,
|
||||||
|
backgroundColor: '#dde6f1',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (application && application.friendly_name) {
|
||||||
|
const updateapp = {
|
||||||
|
...application
|
||||||
|
}
|
||||||
|
updateapp.image_src = friendlyUrl(updateapp.friendly_name) + '.' + ext
|
||||||
|
setApplication(updateapp)
|
||||||
|
}
|
||||||
|
}, [ext])
|
||||||
|
|
||||||
|
const updateCategories = (items) => {
|
||||||
|
const updateapp = {
|
||||||
|
...application
|
||||||
|
}
|
||||||
|
updateapp.categories = items.map(cat => cat.value)
|
||||||
|
setApplication(updateapp)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleChange = (event) => {
|
||||||
|
const updateapp = {
|
||||||
|
...application
|
||||||
|
}
|
||||||
|
updateapp[event.target.name] = event.target.value
|
||||||
|
if (event.target.name === 'icon') {
|
||||||
|
delete updateapp.icon
|
||||||
|
setIcon({
|
||||||
|
value: event.target.value,
|
||||||
|
file: event.target.files[0]
|
||||||
|
})
|
||||||
|
setExt(event.target.value.substr(event.target.value.lastIndexOf('.') + 1))
|
||||||
|
// return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateapp.friendly_name) {
|
||||||
|
updateapp.image_src = friendlyUrl(updateapp.friendly_name) + '.' + ext
|
||||||
|
}
|
||||||
|
|
||||||
|
setApplication(updateapp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ value: 'Browser', label: 'Browser' },
|
||||||
|
{ value: 'Communication', label: 'Communication' },
|
||||||
|
{ value: 'Desktop', label: 'Desktop' },
|
||||||
|
{ value: 'Development', label: 'Development' },
|
||||||
|
{ value: 'Games', label: 'Games' },
|
||||||
|
{ value: 'Multimedia', label: 'Multimedia' },
|
||||||
|
{ value: 'Office', label: 'Office' },
|
||||||
|
{ value: 'Privacy', label: 'Privacy' },
|
||||||
|
{ value: 'Productivity', label: 'Productivity' },
|
||||||
|
{ value: 'Remote Access', label: 'Remote Access' }
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<Head>
|
||||||
|
<title>Kasm Apps</title>
|
||||||
|
<meta name="description" content="List of apps for Kasm Webspaces" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
<div className='flex flex-col lg:flex-row w-full my-20 max-w-6xl text-sm rounded-xl overflow-hidden mx-auto'>
|
||||||
|
<div className='w-full lg:w-1/2 p-16 bg-slate-300'>
|
||||||
|
<h1 className='text-2xl font-medium mb-2'>Add Application</h1>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<p className='mb-8 opacity-70'>This will help you generate the JSON file you need to upload to the App directory.</p>
|
||||||
|
|
||||||
|
<label className='mb-2 font-medium'>Icon</label>
|
||||||
|
<input type="file" name="icon" onChange={handleChange} className='mb-2 p-2 rounded-lg bg-slate-100 border border-solid border-slate-400' />
|
||||||
|
<p className='mb-6 opacity-70'>Select the image to use, image will be renamed when it's downloaded.</p>
|
||||||
|
|
||||||
|
<label className='mb-2 font-medium'>Friendly Name</label>
|
||||||
|
<input name="friendly_name" onChange={handleChange} className='mb-2 p-2 rounded-lg bg-slate-100 border border-solid border-slate-400' />
|
||||||
|
<p className='mb-6 opacity-70'>This is the name that will show for users</p>
|
||||||
|
|
||||||
|
<label className='mb-2 font-medium'>Categories</label>
|
||||||
|
<CreatableSelect
|
||||||
|
name="categories"
|
||||||
|
isMulti
|
||||||
|
options={options}
|
||||||
|
onChange={updateCategories}
|
||||||
|
styles={customStyles}
|
||||||
|
/>
|
||||||
|
<p className='mb-6 mt-2 opacity-70'>You can select from the available option or create new ones.</p>
|
||||||
|
|
||||||
|
<label className='mb-2 font-medium'>Description</label>
|
||||||
|
<input name="description" onChange={handleChange} className='mb-2 p-2 rounded-lg bg-slate-100 border border-solid border-slate-400' />
|
||||||
|
<p className='mb-6 opacity-70'>A short description about the application</p>
|
||||||
|
|
||||||
|
<label className='mb-2 font-medium'>Docker Image</label>
|
||||||
|
<input name="name" onChange={handleChange} className='mb-2 p-2 rounded-lg bg-slate-100 border border-solid border-slate-400' />
|
||||||
|
<p className='mb-6 opacity-70'>The docker image to use, i.e. <code className='text-xs p-1 px-2 rounded bg-white/40'>kasmweb/filezilla:develop</code></p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='w-full lg:w-1/2 p-16 bg-slate-100'>
|
||||||
|
<App app={application} icon={icon} />
|
||||||
|
<pre className='my-8 overflow-y-auto text-xs'>{JSON.stringify(displayApplication(), null, 2)}</pre>
|
||||||
|
<button onClick={downloadZip} className='p-4 relative z-10 px-5 bg-emerald-600 m-2 rounded items-center text-white/70 flex cursor-pointer'>Download</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function App({ app, icon }) {
|
||||||
|
|
||||||
|
const [showDescription, setShowDescription] = useState(false);
|
||||||
|
|
||||||
|
let srcBlob = null
|
||||||
|
|
||||||
|
if (icon) {
|
||||||
|
const blob = new Blob([icon.file])
|
||||||
|
srcBlob = URL.createObjectURL(blob);
|
||||||
|
app.image_src = srcBlob
|
||||||
|
}
|
||||||
|
|
||||||
|
const installButton = () => {
|
||||||
|
return <button className={"text-xs w-full p-4 py-1 rounded-lg flex justify-center items-center bg-blue-500 font-bold text-white"}>Install</button>
|
||||||
|
}
|
||||||
|
const editButton = () => {
|
||||||
|
return <div className="text-xs text-color w-full p-4 py-1 rounded-lg bg-black/5 flex justify-center items-center">Edit</div>
|
||||||
|
}
|
||||||
|
const official = () => {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const appExists = false
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"rounded-xl group w-full shadow max-w-xs relative overflow-hidden h-[100px] border border-solid flex flex-col justify-between bg-slate-300 border-slate-400/50"}>
|
||||||
|
<div className={"absolute top-0 left-0 right-0 h-[200px] transition-all" + (showDescription ? ' -translate-y-1/2' : '')}>
|
||||||
|
<div onClick={() => setShowDescription(true)} className={"h-[100px] p-4 relative overflow-hidden cursor-pointer"}>
|
||||||
|
<img className="h-[90px] group-hover:scale-150 transition-all absolute left-2 top-1" src={app.image_src} alt={app.friendly_name} />
|
||||||
|
<div className="flex-col pl-28">
|
||||||
|
<div className="font-bold">{app.friendly_name || 'Friendly Name'}</div>
|
||||||
|
<div className="text-xs mb-2 flex gap-2">{app.author || 'Unknown'} <span>{official()}</span></div>
|
||||||
|
<div className=" h-8"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 bg-slate-400/20 h-8 text-[10px] flex items-center justify-center">
|
||||||
|
{app.categories.map(cat => (
|
||||||
|
<span className="p-2 py-0 m-[1px] inline-block rounded bg-slate-300/90">{cat}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{appExists && appExists.enabled === true && appExists.available === false && (
|
||||||
|
<div className="absolute inset-0 flex justify-center items-center bg-slate-600/70 text-white"><i className="fa fa-spinner fa-spin mr-3"></i> Installing</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="h-[100px] text-xs relative p-2 pl-4 flex">
|
||||||
|
<button className="absolute right-2 top-2 bg-slate-100 rounded-full flex justify-center items-center h-6 w-6" onClick={() => setShowDescription(false)}>
|
||||||
|
<svg style={{ height: '14px'}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M310.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L160 210.7 54.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L114.7 256 9.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 301.3 265.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L205.3 256 310.6 150.6z"/></svg>
|
||||||
|
</button>
|
||||||
|
<div className="flex flex-col flex-grow"><div className="font-bold">{app.friendly_name}</div> {app.description}</div>
|
||||||
|
<div className="flex flex-col justify-end gap-1">
|
||||||
|
{editButton()}
|
||||||
|
{installButton()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
5
site/pages/api/hello.js
Normal file
5
site/pages/api/hello.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
|
|
||||||
|
export default function handler(req, res) {
|
||||||
|
res.status(200).json({ name: 'John Doe' })
|
||||||
|
}
|
60
site/pages/index.js
Normal file
60
site/pages/index.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import Head from 'next/head'
|
||||||
|
import App from '../components/App'
|
||||||
|
import styles from '../styles/Home.module.css'
|
||||||
|
|
||||||
|
export default function Home({ searchText }) {
|
||||||
|
|
||||||
|
const [apps, setApps] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('list.json')
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((apps) => {
|
||||||
|
setApps(apps)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
let filteredapps = apps && apps.apps && apps.apps.length > 0 ? [...apps.apps] : [];
|
||||||
|
const lowerSearch = searchText && searchText.toLowerCase();
|
||||||
|
if (searchText && searchText !== "") {
|
||||||
|
filteredapps = filteredapps.filter((i) => {
|
||||||
|
const category = i.categories.filter((i) =>
|
||||||
|
i.toLowerCase().includes(lowerSearch)
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
i.name.toLowerCase().includes(lowerSearch) ||
|
||||||
|
category.length > 0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<Head>
|
||||||
|
<title>Kasm Apps</title>
|
||||||
|
<meta name="description" content="List of apps for Kasm Webspaces" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
|
||||||
|
<main className="p-20">
|
||||||
|
<h1 className='flex text-2xl justify-center mb-10'>Applications: <span className=''>{apps && apps.appcount}</span></h1>
|
||||||
|
<div className="flex flex-wrap gap-1 justify-center">
|
||||||
|
{filteredapps && filteredapps.length > 0 && filteredapps.map(function (app, i) {
|
||||||
|
return <App key={app.sha} app={app} />
|
||||||
|
})}
|
||||||
|
{filteredapps && filteredapps.length === 0 && (
|
||||||
|
<p>No applications found {searchText !== '' && ('matching "' + searchText + '"')}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className={styles.grid}>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div >
|
||||||
|
)
|
||||||
|
}
|
6
site/postcss.config.js
Normal file
6
site/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
BIN
site/public/favicon.ico
Normal file
BIN
site/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
1
site/public/list.json
Normal file
1
site/public/list.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"appcount":1,"apps":[{"name":"Chromium","icon":"chromium.png","description":"Chromium is a free and open-source browser, primarily developed and maintained by Google.","image":"kasmweb/chromium:develop","cores":2,"memory":2768,"gpu_count":0,"cpu_allocation":"inherit","docker_registry":"https://index.docker.io/v1/","volume_mappings":{},"config_override":{"hostname":"kasm"},"exec_config":{"go":{"cmd":"bash -c '/dockerstartup/custom_startup.sh --go --url \"$KASM_URL\"'"},"assign":{"cmd":"bash -c '/dockerstartup/custom_startup.sh --assign --url \"$KASM_URL\"'"}},"categories":["Browser"],"sha":"13126dde5f5338398a728debe459dd1106548aef"}]}
|
4
site/public/vercel.svg
Normal file
4
site/public/vercel.svg
Normal 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 |
129
site/styles/Home.module.css
Normal file
129
site/styles/Home.module.css
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
.container {
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 4rem 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
padding: 2rem 0;
|
||||||
|
border-top: 1px solid #eaeaea;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title a {
|
||||||
|
color: #0070f3;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title a:hover,
|
||||||
|
.title a:focus,
|
||||||
|
.title a:active {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.15;
|
||||||
|
font-size: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title,
|
||||||
|
.description {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin: 4rem 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||||
|
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
text-align: left;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid #eaeaea;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: color 0.15s ease, border-color 0.15s ease;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover,
|
||||||
|
.card:focus,
|
||||||
|
.card:active {
|
||||||
|
color: #0070f3;
|
||||||
|
border-color: #0070f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 1em;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.grid {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.card,
|
||||||
|
.footer {
|
||||||
|
border-color: #222;
|
||||||
|
}
|
||||||
|
.code {
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
.logo img {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
}
|
123
site/styles/globals.css
Normal file
123
site/styles/globals.css
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700&display=swap');
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-gradient-to-tr from-slate-500 to-slate-300 min-h-[100vh] text-slate-700;
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-bubbles {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.bg-bubbles li {
|
||||||
|
position: absolute;
|
||||||
|
list-style: none;
|
||||||
|
display: block;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
bottom: -160px;
|
||||||
|
-webkit-animation: square 25s infinite;
|
||||||
|
animation: square 25s infinite;
|
||||||
|
transition-timing-function: linear;
|
||||||
|
}
|
||||||
|
.bg-bubbles li:nth-child(1) {
|
||||||
|
left: 10%;
|
||||||
|
bottom: -60px;
|
||||||
|
}
|
||||||
|
.bg-bubbles li:nth-child(2) {
|
||||||
|
left: 20%;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
-webkit-animation-delay: 2s;
|
||||||
|
animation-delay: 2s;
|
||||||
|
-webkit-animation-duration: 17s;
|
||||||
|
animation-duration: 17s;
|
||||||
|
}
|
||||||
|
.bg-bubbles li:nth-child(3) {
|
||||||
|
left: 25%;
|
||||||
|
-webkit-animation-delay: 4s;
|
||||||
|
animation-delay: 4s;
|
||||||
|
}
|
||||||
|
.bg-bubbles li:nth-child(4) {
|
||||||
|
left: 40%;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
-webkit-animation-duration: 22s;
|
||||||
|
animation-duration: 22s;
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
.bg-bubbles li:nth-child(5) {
|
||||||
|
left: 70%;
|
||||||
|
bottom: -20px;
|
||||||
|
}
|
||||||
|
.bg-bubbles li:nth-child(6) {
|
||||||
|
left: 80%;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
-webkit-animation-delay: 3s;
|
||||||
|
animation-delay: 3s;
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
}
|
||||||
|
.bg-bubbles li:nth-child(7) {
|
||||||
|
left: 32%;
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
-webkit-animation-delay: 7s;
|
||||||
|
animation-delay: 7s;
|
||||||
|
}
|
||||||
|
.bg-bubbles li:nth-child(8) {
|
||||||
|
left: 55%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
-webkit-animation-delay: 15s;
|
||||||
|
animation-delay: 15s;
|
||||||
|
-webkit-animation-duration: 40s;
|
||||||
|
animation-duration: 40s;
|
||||||
|
}
|
||||||
|
.bg-bubbles li:nth-child(9) {
|
||||||
|
left: 25%;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
-webkit-animation-delay: 2s;
|
||||||
|
animation-delay: 2s;
|
||||||
|
-webkit-animation-duration: 40s;
|
||||||
|
animation-duration: 40s;
|
||||||
|
background-color: rgba(255, 255, 255, 0.17);
|
||||||
|
border: 1px solid rgba(255,255,255,0.22);
|
||||||
|
}
|
||||||
|
.bg-bubbles li:nth-child(10) {
|
||||||
|
left: 90%;
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
-webkit-animation-delay: 11s;
|
||||||
|
animation-delay: 11s;
|
||||||
|
}
|
||||||
|
@-webkit-keyframes square {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(-400px) rotate(600deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes square {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(-400px) rotate(600deg);
|
||||||
|
}
|
||||||
|
}
|
19
site/tailwind.config.js
Normal file
19
site/tailwind.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./pages/**/*.{js,ts,jsx,tsx}",
|
||||||
|
"./components/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
app: {
|
||||||
|
900: '#5f4c7c',
|
||||||
|
800: '#9178bd',
|
||||||
|
700: '#b199da'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
2661
site/yarn.lock
Normal file
2661
site/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user