#186 cp svelte components into electron. WARNING: doesnt work like the tauri build yet

This commit is contained in:
neil 2023-02-03 14:02:49 +08:00
parent e4af99443d
commit f56598b6ea
81 changed files with 3496 additions and 315 deletions

View file

@ -11,5 +11,6 @@ node_modules
pnpm-lock.yaml
package-lock.json
yarn.lock
src-tauri/target/*
coverage/*
build/*
dist/*

View file

@ -12,12 +12,7 @@ pnpm-lock.yaml
package-lock.json
yarn.lock
build
/src-tauri/src/*
/src-tauri/icons/*
/src-tauri/target/**/*
/src-tauri/build/*
/src-tauri/Cargo.lock
/src-tauri/Cargo.toml
src-tauri
coverage/*
**/*.plist
build/*
dist/*

View file

@ -10,10 +10,7 @@ Once you've created a project and installed dependencies with `npm install` (or
```bash
# use if you need interaction with the rust handlers
pnpm tauri dev
# or if ui dev only
pnpm run dev -- --open
pnpm dev
```
## Building

View file

@ -6,13 +6,17 @@
"author": "tea.xyz",
"main": "src/electron.cjs",
"scripts": {
"dev": "cross-env NODE_ENV=dev npm run dev:all",
"dev:all": "concurrently -n=svelte,electron -c='#ff3e00',blue \"npm run dev:svelte\" \"npm run dev:electron\"",
"dev:svelte": "vite dev",
"dev:electron": "electron src/electron.cjs",
"pack": "electron-builder --dir --config electron-builder.config.cjs",
"dist": "pnpm build && electron-builder --config electron-builder.config.cjs",
"package": "pnpm build && electron-builder --config electron-builder.config.cjs",
"dev:package": "pnpm build && electron-builder --config electron-builder.config.cjs --dir",
"electron": "concurrently --kill-others \"vite dev\" \"electron src/electron.cjs\"",
"dev": "vite dev --port 8080",
"build": "vite build",
"olddev": "vite dev",
"build": "vite build && cp build/app.html build/index.html",
"preview": "vite preview",
"unit:test": "vitest",
"coverage": "vitest run --coverage",
@ -37,7 +41,8 @@
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"autoprefixer": "^10.4.13",
"concurrently": "^6.5.1",
"concurrently": "^7.6.0",
"cross-env": "^7.0.3",
"electron": "22.1.0",
"electron-builder": "^23.6.0",
"electron-reloader": "^1.2.3",
@ -57,7 +62,7 @@
"tslib": "^2.3.1",
"typescript": "^4.7.4",
"vite": "^4.0.0",
"vitest": "^0.27.1"
"vitest": "^0.28.3"
},
"type": "module",
"dependencies": {
@ -67,14 +72,17 @@
"@vitest/coverage-c8": "^0.27.1",
"bcryptjs": "^2.4.3",
"buffer": "^6.0.3",
"electron-context-menu": "^3.6.1",
"electron-log": "^4.4.8",
"electron-serve": "^1.1.0",
"electron-vite": "^1.0.18",
"electron-window-state": "^5.0.3",
"fuse.js": "^6.6.2",
"lodash": "^4.17.21",
"lorem-ipsum": "^2.0.8",
"svelte-markdown": "^0.2.3",
"svelte-watch-resize": "^1.0.3",
"upath": "^2.0.1",
"url-join": "^5.0.0"
},
"pnpm": {

View file

@ -0,0 +1,10 @@
import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
webServer: {
command: 'npm run build && npm run preview',
port: 4173
}
};
export default config;

View file

@ -0,0 +1,12 @@
const { theme, plugins } = require('@tea/ui/tailwind.config.cjs');
module.exports = {
plugins: {
tailwindcss: {
content: ['./src/**/*.{html,svelte,ts,js}', '../ui/src/**/*.{html,svelte,ts,js}'],
theme,
plugins: [...plugins]
},
autoprefixer: {}
}
};

View file

@ -0,0 +1,4 @@
import matchers from '@testing-library/jest-dom/matchers';
import { expect } from 'vitest';
expect.extend(matchers);

View file

@ -1,59 +1,43 @@
:root {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Open Sans', 'Helvetica Neue', sans-serif;
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'pp-neue-machina';
src: url('/fonts/PPNeueMachina-InktrapLight.woff');
}
.text-primary {
color: #ff3e00;
@font-face {
font-family: 'sono';
src: url('/fonts/Sono-Light.woff2');
}
.button {
padding: 10px 10px;
display: inline-block;
background-color: #ff3e00;
color: white;
text-decoration: none;
border: none;
html {
background-color: #1a1a1a;
color: #fff;
user-select: none;
cursor: default;
}
/* The sidebar menu */
.sidenav {
height: 100%; /* Full-height: remove this if you want "auto" height */
width: 160px; /* Set the width of the sidebar */
position: fixed; /* Fixed Sidebar (stay in place on scroll) */
z-index: 1; /* Stay on top */
top: 0; /* Stay at the top */
left: 0;
background-color: #ff3e00;
overflow-x: hidden; /* Disable horizontal scroll */
padding-top: 20px;
}
.sidenav a {
padding: 6px 8px 6px 16px;
text-decoration: none;
font-size: 20px;
font-weight: bold;
color: white;
display: block;
}
.sidenav a:hover {
background-color: #f1f1f1;
color: $primary;
}
.main {
margin-left: 160px; /* Same as the width of the sidebar */
padding: 0px 10px;
}
/* On smaller screens, where height is less than 450px, change the style of the sidebar (less padding and a smaller font size) */
@media screen and (max-height: 450px) {
.sidenav {
padding-top: 15px;
}
.sidenav a {
font-size: 18px;
@layer base {
html {
font-family: sono, sans-serif;
}
}
.text-primary,
header,
h1,
h2,
h3,
h4,
h5,
h6,
button,
.click-copy {
font-family: 'pp-neue-machina' !important;
}
.pk-version {
font-family: 'sono';
}

9
modules/desktop/src/app.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Locals {}
// interface PageData {}
// interface Error {}
// interface Platform {}
}

View file

@ -2,14 +2,11 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
const ipc = require('electron').ipcRenderer;
</script>
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body>
<div id="svelte">%sveltekit.body%</div>
<div>%sveltekit.body%</div>
</body>
</html>

View file

@ -0,0 +1,9 @@
<script lang="ts">
import '$appcss';
import Placeholder from '$components/Placeholder/Placeholder.svelte';
export let arg1: string;
</script>
<Placeholder label="Badges" />
<h1>{arg1 || 'tes'}</h1>

View file

@ -0,0 +1,5 @@
<script lang="ts">
import '$appcss';
</script>
<section class="border-gray h-56 border bg-black" />

View file

@ -0,0 +1,24 @@
<script lang="ts">
import PanelHeader from '@tea/ui/PanelHeader/PanelHeader.svelte';
import MiniPackageCard from '@tea/ui/MiniPackageCard/MiniPackageCard.svelte';
import type { Category } from '$libs/types';
import { onMount } from 'svelte';
import { getCategorizedPackages } from '@api';
let categories: Category[] = [];
onMount(async () => {
categories = await getCategorizedPackages();
});
</script>
{#each categories as category}
<PanelHeader ctaLabel={category.cta_label} ctaLink={'#'} title={category.label} />
<ul class="border-gray grid grid-cols-3 border border-r-0 bg-black">
{#each category.packages as pkg}
<div class="border-gray border border-t-0 border-l-0 p-4">
<MiniPackageCard {pkg} ctaLabel="DETAILS" link={`/packages/${pkg.slug}`} />
</div>
{/each}
</ul>
{/each}

View file

@ -0,0 +1,27 @@
<script lang="ts">
import '$appcss';
import Button from '@tea/ui/Button/Button.svelte';
let copyButtonText = 'COPY';
const copyValue = `sh <(curl https://tea.xyz)`;
const onCopy = () => {
copyButtonText = 'COPIED!';
navigator.clipboard.writeText(copyValue);
};
</script>
<section class="border-gray mt-4 border bg-black">
<header class="flex flex-col items-center py-8">
<figure>
<img alt="tea" src="/images/tea-icon.png" class="rounded-md" />
</figure>
<p class="text-primary">tea.cli ver. 0.6.0</p>
</header>
<footer class="border-gray flex h-20 border-t text-white">
<input class="flex-grow bg-black pl-4" disabled value="sh <(curl tea.xyz)>" />
<Button class="w-16 border-0 border-l-2 text-sm" onClick={onCopy}>{copyButtonText}</Button>
<Button class="w-56 border-0 border-l-2 text-sm" onClick={() => console.log('cli')}
>OPEN IN TERMINAL</Button
>
</footer>
</section>

View file

@ -0,0 +1,24 @@
<script lang="ts">
import '$appcss';
import type { AirtablePost } from '@tea/ui/types';
import Posts from '@tea/ui/Posts/Posts.svelte';
import PanelHeader from '@tea/ui/PanelHeader/PanelHeader.svelte';
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
import { postsStore } from '$libs/stores';
export let title = 'Workshops';
export let ctaLabel = 'View all';
let courses: AirtablePost[] = [];
postsStore.subscribeByTag('course', (posts) => (courses = posts));
</script>
<PanelHeader {title} {ctaLabel} ctaLink="/" />
{#if courses.length}
<Posts posts={courses} linkTarget="_blank" />
{:else}
<section class="border-gray h-64 border bg-black p-4">
<Preloader />
</section>
{/if}

View file

@ -0,0 +1,31 @@
<script lang="ts">
import '$appcss';
import { postsStore } from '$libs/stores';
import type { Course } from '$libs/types';
import Gallery from '@tea/ui/Gallery/Gallery.svelte';
let courses: Course[] = [];
postsStore.subscribeByTag('featured_course', (posts) => {
courses = posts.map((post) => {
return {
title: post.title,
sub_title: post.sub_title,
banner_image_url: post.thumb_image_url,
link: post.link
} as Course;
});
});
</script>
<Gallery
title="FEATURED COURSES"
items={courses.map((course) => ({
title: course.title,
subTitle: course.sub_title,
imageUrl: course.banner_image_url,
link: course.link
}))}
linkTarget="_blank"
/>

View file

@ -0,0 +1,33 @@
<script lang="ts">
import '$appcss';
import { onMount } from 'svelte';
import type { Package } from '@tea/ui/types';
import Gallery from '@tea/ui/Gallery/Gallery.svelte';
import {
featuredPackages as featuredPackagesStore,
initializeFeaturedPackages
} from '$libs/stores';
let featuredPackages: Package[] = [];
featuredPackagesStore.subscribe((v) => {
featuredPackages = v;
});
onMount(() => {
if (!featuredPackages.length) {
initializeFeaturedPackages();
}
});
</script>
<Gallery
title="FEATURED PACKAGES"
items={featuredPackages.map((pkg) => ({
title: pkg.full_name,
subTitle: pkg.maintainer || '',
imageUrl: pkg.thumb_image_url,
link: `/packages/${pkg.slug}`
}))}
/>

View file

@ -0,0 +1,85 @@
<script lang="ts">
import Button from '@tea/ui/Button/Button.svelte';
</script>
<footer class="font-machina relative h-auto w-full bg-black">
<section class="p-4 px-16 py-16">
<h3 class="text-primary mb-5 text-2xl">QUICK LINKS</h3>
<menu class="flex gap-4">
<div class="border-gray flex-grow border border-l-0 border-r-0">
<a href="/">
<Button>
<div class="text-primary flex justify-between hover:text-black">
<div class="uppercase">About the tea store</div>
<div>&#8594</div>
</div>
</Button>
</a>
</div>
<div class="border-gray flex-grow border border-l-0 border-r-0">
<a href="/">
<Button>
<div class="text-primary flex justify-between hover:text-black">
<div class="uppercase">REPORT A PROBLEM</div>
<div>&#8594</div>
</div>
</Button>
</a>
</div>
<div class="border-gray flex-grow border border-l-0 border-r-0">
<a href="https://tea.xyz" target="_blank" rel="noreferrer">
<Button>
<div class="text-primary flex justify-between hover:text-black">
<div class="uppercase">VISIT TEA.XYZ</div>
<div>&#8594</div>
</div>
</Button>
</a>
</div>
</menu>
</section>
<section class="border-gray h-16 border border-r-0 p-4 px-16">
<div class="text-gray flex gap-4 text-xs">
<a
href="https://tea.xyz/terms-of-use/"
target="_blank"
rel="noreferrer"
class="hover:text-white"
>
TERMS & SERVICES
</a>
<a
href="https://tea.xyz/privacy-policy/"
target="_blank"
rel="noreferrer"
class="hover:text-white"
>
PRIVACY POLICY
</a>
</div>
</section>
</footer>
<style>
h3 {
font-family: 'pp-neue-machina', sans-serif;
color: #00ffd0;
}
p,
.nav-item {
font-family: 'sono', sans-serif;
color: #ffffff;
}
.list-group-item {
font-family: 'pp-neue-machina', sans-serif;
text-transform: uppercase;
transition: 0.1s ease-in;
}
.list-group-item:hover {
padding-left: 1vw;
}
</style>

View file

@ -0,0 +1,45 @@
<script lang="ts">
import '$appcss';
import ArticleCard from '@tea/ui/ArticleCard/ArticleCard.svelte';
const doStuff = () => {
console.log('do stuff!');
};
</script>
<header class="border-gray text-primary border bg-black p-4">GETTING STARTED WITH TEA</header>
<section class="grid grid-cols-3 bg-black">
<div class="border-gray border p-4">
<ArticleCard
content={{
title: 'installing tea',
copy: "It's time to take your first sip! Click below to visit our tea-cli documentation page.",
img_url: '/images/bored-ape.png',
cta_label: 'Get Started',
link: '/cli'
}}
/>
</div>
<div class="border-gray border p-4">
<ArticleCard
content={{
title: 'authenticating',
copy: 'Using tea without authenticating is like playing a video game without the DLC. Join us today!',
img_url: '/images/bored-ape.png',
cta_label: 'Get Started',
link: ''
}}
onClick={doStuff}
/>
</div>
<div class="border-gray border p-4">
<ArticleCard
content={{
title: 'give us a star',
copy: 'Revolutions are built on the will of the people. Show your support for a more equitable internet.',
img_url: '/images/bored-ape.png',
cta_label: 'Get Started'
}}
/>
</div>
</section>

View file

@ -0,0 +1,38 @@
<script lang="ts">
import '$appcss';
import type { GUIPackage } from '$libs/types';
import { PackageStates } from '$libs/types';
import PanelHeader from '@tea/ui/PanelHeader/PanelHeader.svelte';
import { packagesStore } from '$libs/stores';
import MiniPackageCard from '@tea/ui/MiniPackageCard/MiniPackageCard.svelte';
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
let packages: GUIPackage[] = [];
packagesStore.subscribe((v) => {
packages = v.filter((p) => p.state === PackageStates.INSTALLED);
});
</script>
<PanelHeader title="My installs" ctaLabel="Check for updates >" ctaLink="#" />
<ul class="border-gray grid grid-cols-3 border border-r-0 bg-black">
{#if packages.length > 0}
{#each packages as pkg}
<div class="border-gray border border-t-0 border-l-0 p-4">
<MiniPackageCard
{pkg}
ctaLabel="DETAILS"
onClickCTA={async () => {
console.log('do something with:', pkg.full_name);
}}
/>
</div>
{/each}
{:else}
{#each Array(12) as _}
<section class="h-50 border-gray border p-4">
<Preloader />
</section>
{/each}
{/if}
</ul>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import '$appcss';
import { postsStore } from '$libs/stores';
import type { AirtablePost } from '@tea/ui/types';
import Posts from '@tea/ui/Posts/Posts.svelte';
import PanelHeader from '@tea/ui/PanelHeader/PanelHeader.svelte';
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
let news: AirtablePost[] = [];
postsStore.subscribeByTag('news', (posts) => (news = posts));
</script>
<PanelHeader title="Open-source News" ctaLabel="Read more articles >" ctaLink="/" />
{#if news.length}
<Posts posts={news} linkTarget="_blank" />
{:else}
<section class="border-gray h-64 border bg-black p-4">
<Preloader />
</section>
{/if}

View file

@ -0,0 +1,52 @@
<script lang="ts">
import '$appcss';
import '@tea/ui/icons/icons.css';
import type { Package, Bottle } from '@tea/ui/types';
import Button from '@tea/ui/Button/Button.svelte';
import StarRating from '@tea/ui/StarRating/StarRating.svelte';
import { onMount } from 'svelte';
import { getPackageBottles } from '@api';
export let pkg: Package;
let bottles: Bottle[] = [];
let packageRating = 0;
let copyButtonText = 'COPY';
const copyValue = `sh <(curl tea.xyz ) +${pkg.full_name}`;
const onCopy = () => {
copyButtonText = 'COPIED!';
navigator.clipboard.writeText(copyValue);
};
onMount(async () => {
try {
bottles = await getPackageBottles(pkg.full_name);
} catch (err) {
console.error(err);
}
});
</script>
<section class="border-gray mt-4 border bg-black">
<header class="flex p-2">
<figure class="grow-1 w-1/3">
<img width={260} src={pkg.thumb_image_url} alt={pkg.full_name} />
</figure>
<article class="w-2/3 p-4 pt-8">
<h3 class="text-primary text-3xl">{pkg.full_name}</h3>
<h3>&#x2022; {pkg.maintainer || ''}{pkg.maintainer ? ' |' : ''} {bottles.length} bottles</h3>
<div class="mt-4">
<StarRating maxRating={5} rating={packageRating} />
</div>
<p class="font-sono mt-4 text-sm">{pkg.desc}</p>
</article>
</header>
<footer class="border-gray flex h-20 border-t text-white">
<input class="click-copy flex-grow bg-black pl-4" disabled value={copyValue} />
<Button class="w-16 border-0 border-l-2 text-sm" onClick={onCopy}>{copyButtonText}</Button>
<Button class="w-56 border-0 border-l-2 text-sm" onClick={() => console.log('cli')}
>OPEN IN TERMINAL</Button
>
</footer>
</section>

View file

@ -0,0 +1,8 @@
<script lang="ts">
import type { Package } from '@tea/ui/types';
export let pkg: Package;
</script>
<section class="h-64 w-full">
<h1>{pkg.full_name}</h1>
</section>

View file

@ -0,0 +1,56 @@
<script lang="ts">
import '$appcss';
import { afterUpdate } from 'svelte';
import ReviewCard from '@tea/ui/ReviewCard/ReviewCard.svelte';
import type { Review } from '@tea/ui/types';
export let reviews: Review[];
export let showLimit = 9;
let showMore = false;
const getColReviews = (n: number) => {
const showReviews = reviews.filter((_item, i) => (i - n) % 3 === 0);
return showMore ? showReviews : showReviews.slice(0, showLimit / 3);
};
let col1: Review[] = [];
let col2: Review[] = [];
let col3: Review[] = [];
afterUpdate(() => {
col1 = getColReviews(0);
col2 = getColReviews(1);
col3 = getColReviews(2);
});
// TODO: problem with reviews with differing heights
// ideally they should work like metro-ui to not have extreme height diff between columns
</script>
<header class="border-gray text-primary border bg-black p-4">REVIEWS ({reviews.length})</header>
<section class="font-machina flex flex-row flex-wrap bg-black">
<div class="border-gray w-1/3 border-0 border-l-2 border-b-2 p-4">
{#each col1 as review}
<ReviewCard {review} />
<div class="mt-4" />
{/each}
</div>
<div class="border-gray w-1/3 border-0 border-l-2 border-b-2 p-4">
{#each col2 as review}
<ReviewCard {review} />
<div class="mt-4" />
{/each}
</div>
<div class="border-gray w-1/3 border-0 border-x-2 border-b-2 p-4">
{#each col3 as review}
<ReviewCard {review} />
<div class="mt-4" />
{/each}
</div>
</section>
{#if showLimit <= reviews.length && showMore === false}
<footer class="border-gray border bg-black p-4">
<button on:click={() => (showMore = true)}>SHOW MORE</button>
</footer>
{/if}

View file

@ -0,0 +1,64 @@
<script lang="ts">
import '$appcss';
import type { GUIPackage } from '$libs/types';
import { PackageStates } from '$libs/types';
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
import PackageCard from '@tea/ui/PackageCard/PackageCard.svelte';
import { onMount } from 'svelte';
// TODO: replace with getting foundation essentials
import { getTopPackages } from '$libs/api/mock';
import { installPackage } from '@api';
export let title = 'Packages';
let packages: GUIPackage[] = [];
const getCTALabel = (state: PackageStates): string => {
return {
[PackageStates.AVAILABLE]: 'INSTALL',
[PackageStates.INSTALLED]: 'INSTALLED',
[PackageStates.INSTALLING]: 'INSTALLING',
[PackageStates.UNINSTALLED]: 'RE-INSTALL'
}[state];
};
onMount(async () => {
if (!packages.length) {
packages = await getTopPackages();
}
});
</script>
<header class="border-gray text-primary flex items-center justify-between border bg-black p-4">
<span>{title}</span>
<a href="/packages" class="font-sono text-sm underline">View all packages</a>
</header>
<ul class="grid grid-cols-3 bg-black">
{#if packages.length > 0}
{#each packages as pkg}
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
<PackageCard
{pkg}
link={`/packages/${pkg.slug}`}
ctaLabel={getCTALabel(pkg.state)}
onClickCTA={async () => {
try {
pkg.state = PackageStates.INSTALLING;
await installPackage(pkg.full_name);
pkg.state = PackageStates.INSTALLED;
} catch (error) {
console.error(error);
}
}}
/>
</div>
{/each}
{:else}
{#each Array(9) as _}
<section class="h-50 border-gray border p-4">
<Preloader />
</section>
{/each}
{/if}
</ul>

View file

@ -0,0 +1,14 @@
<script lang="ts">
export let coverUrl = '';
let clazz = '';
export { clazz as class };
</script>
<figure class={`font-machina relative mb-8 h-32 w-full uppercase ${clazz}`}>
{#if coverUrl}
<img src={coverUrl} class="absolute z-0 h-32 w-full object-cover" alt="cover" />
{/if}
<div class="text-primary absolute bottom-0 z-10 text-6xl leading-[32px]">
<slot />
</div>
</figure>

View file

@ -0,0 +1,24 @@
<script lang="ts">
export let label = '';
</script>
<section class="bg-gray p-8">
<header>{label}</header>
<slot />
</section>
<style>
section {
position: relative;
min-height: 240px;
height: 100%;
width: 100%;
min-width: 100%;
/* background-color: #ccc; */
display: flex;
}
header {
color: rgb(50, 48, 48);
font-size: 3em;
}
</style>

View file

@ -0,0 +1,34 @@
<script lang="ts">
import '$appcss';
const openGithub = () => {
// open('https://github.com/teaxyz');
};
</script>
<div class="card social-box" style="width: 100%; float:right;">
<header class="border-gray text-primary border-b pt-7 pb-7 pl-5">PRE-FLIGHT</header>
<div class="listbox-item border-gray border-b p-6">
<a href="/cli">
<p>Install Tea</p>
</a>
</div>
<div class="listbox-item border-gray border-b p-6">
<div>
<p>Authenticate</p>
</div>
</div>
<div class="listbox-item p-6">
<button on:click={openGithub}>Give tea a star</button>
</div>
</div>
<style>
.card {
border: 2px solid #949494;
background-color: #1a1a1a;
}
.listbox-item {
height: 75px;
}
</style>

View file

@ -0,0 +1,36 @@
<script lang="ts">
import '$appcss';
import { authStore } from '$libs/stores';
import type { Developer } from '@tea/ui/types';
let user: Developer | null = null;
const authPage = `http://localhost:3000/v1/auth/user?device_id=${authStore.deviceId}`; // https://api.tea.xyz/v1/auth/user?device_id=device_id
authStore.subscribe((u) => (user = u));
</script>
{#if user}
<section class="border-gray border-2 bg-black p-2">
<div class="profile_banner border-gray container flex border bg-black">
<img class="w-1/5" src={user.avatar_url || '/images/bored-ape.png'} alt="profile" />
<div class="flex w-4/5 items-center p-5">
<div class="w-1/2 pl-5">
<p class="text-gray uppercase">Authenticated with GitHub</p>
<p />
<p class="text-primary text-4xl">@{user.login}</p>
</div>
<div class="border-gray h-full border-l" />
<div class="w-1/2 pl-10">
<p class="text-gray uppercase leading-loose">
Country: <span>{user?.country}</span><br />Wallet:
{#if user.wallet}
<span>{user.wallet}</span>
{:else}
<a class="text-green underline" href="/">Connect Now</a>
{/if}
</p>
</div>
</div>
</div>
</section>
{/if}

View file

@ -0,0 +1,103 @@
<script lang="ts">
import '$appcss';
import { packagesStore } from '$libs/stores';
import SortingButtons from './SortingButtons.svelte';
import type { GUIPackage } from '$libs/types';
import { PackageStates } from '$libs/types';
import PackageCard from '@tea/ui/PackageCard/PackageCard.svelte';
import SearchInput from '@tea/ui/SearchInput/SearchInput.svelte';
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
import { installPackage } from '@api';
let packages: GUIPackage[] = [];
let sortBy = 'popularity';
let sortDirection: 'asc' | 'desc' = 'desc';
const searchLimit = 10;
const setPackages = (pkgs: GUIPackage[], isSearch?: boolean) => {
packages = isSearch
? pkgs
: pkgs.sort((a, b) => {
if (sortBy === 'popularity') {
const aPop = +a.dl_count + a.installs;
const bPop = +b.dl_count + b.installs;
return sortDirection === 'asc' ? aPop - bPop : bPop - aPop;
} else {
// most recent
const aDate = new Date(a.last_modified);
const bDate = new Date(b.last_modified);
return sortDirection === 'asc' ? +aDate - +bDate : +bDate - +aDate;
}
});
};
const onSearch = async (term: string) => {
if (term !== '' && term.length > 1) {
const matchingPackages: GUIPackage[] = await packagesStore.search(term, searchLimit);
setPackages(matchingPackages, true);
} else {
setPackages(packagesStore.packages, false);
}
};
const onSort = (opt: string, dir: 'asc' | 'desc') => {
sortBy = opt;
sortDirection = dir;
setPackages(packages);
};
const getCTALabel = (state: PackageStates): string => {
return {
[PackageStates.AVAILABLE]: 'INSTALL',
[PackageStates.INSTALLED]: 'INSTALLED',
[PackageStates.INSTALLING]: 'INSTALLING',
[PackageStates.UNINSTALLED]: 'RE-INSTALL'
}[state];
};
packagesStore.subscribe(setPackages);
</script>
<div class="border-gray border bg-black">
<section class="flex items-center justify-between">
<div>
<SearchInput size="medium" {onSearch} />
</div>
<div class="pr-4">
<section class="border-gray h-12 w-48 border">
<SortingButtons {onSort} />
</section>
</div>
</section>
<ul class="grid grid-cols-3">
{#if packages.length > 0}
{#each packages as pkg}
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
<PackageCard
{pkg}
link={`/packages/${pkg.slug}`}
ctaLabel={getCTALabel(pkg.state)}
onClickCTA={async () => {
try {
pkg.state = PackageStates.INSTALLING;
await installPackage(pkg.full_name);
pkg.state = PackageStates.INSTALLED;
} catch (error) {
console.error(error);
}
}}
/>
</div>
{/each}
{:else}
{#each Array(12) as _}
<section class="h-50 border-gray border p-4">
<Preloader />
</section>
{/each}
{/if}
</ul>
</div>

View file

@ -0,0 +1,132 @@
<script lang="ts">
import '$appcss';
export let onSort: (opt: string, dir: 'asc' | 'desc') => void;
let sortBy = 'popularity';
let sortDirection: 'asc' | 'desc' = 'desc';
const sortOptions = ['popularity', 'most recent'];
const setSortBy = (opt: string) => {
sortBy = opt;
if (onSort) {
onSort(sortBy, sortDirection);
}
};
const setSortDir = (opt: string, dir: 'asc' | 'desc') => {
sortDirection = dir;
setSortBy(opt);
};
</script>
<section class="sorting-container font-machina bg-black text-white">
<div class="dropdown">
<div class="dropdown-title">SORT ORDER</div>
<ul class="dropdown-content column flex">
{#each sortOptions as option}
<li class="flex items-center">
<button
class={`sort-btn uppercase ${sortBy === option ? 'active' : ''}`}
on:click={() => setSortBy(option)}
>
{option}
</button>
<div class="direction-arrows">
<button
on:click={() => setSortDir(option, 'asc')}
class={sortBy === option && sortDirection === 'asc' ? 'active' : ''}>&uarr;</button
>
<button
on:click={() => setSortDir(option, 'desc')}
class={sortBy === option && sortDirection === 'desc' ? 'active' : ''}>&darr;</button
>
</div>
</li>
{/each}
</ul>
</div>
</section>
<style>
.direction-arrows {
float: right;
}
.direction-arrows button {
opacity: 0.3;
}
.direction-arrows button.active {
opacity: 1;
}
.sorting-container {
display: inline-block;
text-align: center;
text-decoration: none;
text-transform: uppercase;
max-width: 240px;
width: 100%;
height: 100%;
min-height: 34px;
transition: 0.1s linear;
}
.dropdown {
width: 100%;
height: auto;
position: relative;
display: inline-block;
cursor: pointer;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #1a1a1a;
width: 100%;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
z-index: 1;
color: white;
list-style: none;
padding: 0px;
}
.dropdown-content li {
position: relative;
padding: 0px 10px;
height: 40px;
width: 100%;
line-height: 40px;
}
.dropdown-content li .sort-btn {
height: 100%;
width: calc(100% - 40px);
opacity: 0.6;
}
.dropdown-content li .sort-btn.active {
font-weight: bold;
opacity: 1;
}
.dropdown-content li .direction-arrows {
position: absolute;
right: 10px;
top: 0px;
}
.dropdown-content li:hover {
background: #00ffd0;
color: black;
}
.dropdown:hover .dropdown-content {
display: block;
}
.dropdown-title {
height: 50px;
line-height: 50px;
padding-left: 10px;
}
</style>

View file

@ -0,0 +1,163 @@
<script lang="ts">
import { searchStore } from '$libs/stores';
import type { GUIPackage } from '$libs/types';
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
import PackageCard from '@tea/ui/PackageCard/PackageCard.svelte';
import { PackageStates } from '$libs/types';
import Posts from '@tea/ui/Posts/Posts.svelte';
import { installPackage } from '@api';
import type { AirtablePost } from '@tea/ui/types';
let term: string;
let packages: GUIPackage[] = [];
let articles: AirtablePost[] = []; // news, blogs, etc
let workshops: AirtablePost[] = []; // workshops, course
let loading = true;
searchStore.subscribe((v) => {
term = v;
});
searchStore.packagesSearch.subscribe((pkgs) => {
packages = pkgs;
});
searchStore.postsSearch.subscribe((posts) => {
let partialArticles: AirtablePost[] = [];
let partialWorkshops: AirtablePost[] = [];
for (let post of posts) {
if (post.tags.includes('news')) {
partialArticles.push(post);
}
if (post.tags.includes('course') || post.tags.includes('featured_course')) {
partialWorkshops.push(post);
}
}
articles = partialArticles;
workshops = partialWorkshops;
});
searchStore.searching.subscribe((v) => (loading = v));
const getCTALabel = (state: PackageStates): string => {
return {
[PackageStates.AVAILABLE]: 'INSTALL',
[PackageStates.INSTALLED]: 'INSTALLED',
[PackageStates.INSTALLING]: 'INSTALLING',
[PackageStates.UNINSTALLED]: 'RE-INSTALL'
}[state];
};
const onClose = () => {
term = '';
};
</script>
<section class={term ? 'show' : ''}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<figure on:click={onClose} />
<div class="border-gray z-20 border bg-black">
<header class="flex justify-between p-4">
<div class="text-primary text-2xl">Showing search results for `{term}`</div>
<button on:click={onClose}>&#x2715</button>
</header>
<menu class="bg-accent flex h-8 w-full gap-4 px-4 text-xs">
<button>packages ({packages.length})</button>
<button>articles ({articles.length})</button>
<button>workshops ({workshops.length})</button>
</menu>
<header class="text-primary p-4 text-lg">
Top Package Results ({packages.length})
</header>
<ul class="grid grid-cols-3">
{#if packages.length > 0}
{#each packages as pkg}
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
<PackageCard
{pkg}
link={`/packages/${pkg.slug}`}
ctaLabel={getCTALabel(pkg.state)}
onClickCTA={async () => {
try {
pkg.state = PackageStates.INSTALLING;
await installPackage(pkg.full_name);
pkg.state = PackageStates.INSTALLED;
} catch (error) {
console.error(error);
}
}}
/>
</div>
{/each}
{:else if loading}
{#each Array(12) as _}
<section class="h-50 border-gray border p-4">
<Preloader />
</section>
{/each}
{/if}
</ul>
<header class="text-primary p-4 text-lg">
Top Article Results ({articles.length})
</header>
{#if articles.length}
<Posts posts={articles} linkTarget="_blank" />
{:else if loading}
<section class="border-gray h-64 border bg-black p-4">
<Preloader />
</section>
{/if}
<header class="text-primary p-4 text-lg">
Top Workshop Results ({workshops.length})
</header>
{#if workshops.length}
<Posts posts={workshops} linkTarget="_blank" />
{:else if loading}
<section class="border-gray h-64 border bg-black p-4">
<Preloader />
</section>
{/if}
</div>
</section>
<style>
section {
position: fixed;
top: 80px;
left: 0px;
right: 0;
background: rgba(0, 0, 0, 0.7);
transition: opacity 0.3s ease-in-out;
opacity: 0%;
overflow: hidden;
height: 0px;
}
section.show {
padding: 36px;
z-index: 10;
}
section > figure {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
section.show {
opacity: 100%;
height: 100%;
}
section > div {
position: relative;
height: 0%;
transition: height 0.6s ease-in-out;
overflow-y: scroll;
}
section.show > div {
height: 90%;
}
</style>

View file

@ -0,0 +1,63 @@
<script lang="ts">
import '$appcss';
import type { GUIPackage } from '$libs/types';
import type { Package } from '@tea/ui/types';
import { PackageStates } from '$libs/types';
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
import PackageCard from '@tea/ui/PackageCard/PackageCard.svelte';
import { onMount } from 'svelte';
import { installPackage } from '@api';
import { packagesStore } from '$libs/stores';
export let pkg: Package;
let packages: GUIPackage[] = [];
const getCTALabel = (state: PackageStates): string => {
return {
[PackageStates.AVAILABLE]: 'INSTALL',
[PackageStates.INSTALLED]: 'INSTALLED',
[PackageStates.INSTALLING]: 'INSTALLING',
[PackageStates.UNINSTALLED]: 'RE-INSTALL'
}[state];
};
onMount(async () => {
if (!packages.length) {
const matches = await packagesStore.search(pkg.desc, 4);
packages = matches.filter((mp) => mp.full_name !== pkg.full_name).slice(0, 3);
}
});
</script>
<header class="border-gray text-primary flex items-center justify-between border bg-black p-4">
<span>MORE LIKE THIS</span>
</header>
<ul class="grid grid-cols-3 bg-black">
{#if packages.length > 0}
{#each packages as pkg}
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
<PackageCard
{pkg}
link={`/packages/${pkg.slug}`}
ctaLabel={getCTALabel(pkg.state)}
onClickCTA={async () => {
try {
pkg.state = PackageStates.INSTALLING;
await installPackage(pkg.full_name);
pkg.state = PackageStates.INSTALLED;
} catch (error) {
console.error(error);
}
}}
/>
</div>
{/each}
{:else}
{#each Array(9) as _}
<section class="h-50 border-gray border p-4">
<Preloader />
</section>
{/each}
{/if}
</ul>

View file

@ -0,0 +1,36 @@
<script lang="ts">
// import { authStore } from '$libs/stores';
import type { Developer } from '@tea/ui/types';
// import { baseUrl } from '$libs/v1Client';
let user: Developer | null = null;
// const deviceId = authStore.deviceIdStore;
const openGithub = () => {
// open(`${baseUrl}/auth/user?device_id=${$deviceId}`);
try {
// authStore.pollSession();
} catch (error) {
console.error(error);
}
};
// authStore.subscribe((u) => (user = u));
</script>
{#if user}
<a href="/profile">
<section class="flex">
<img width="40" height="40" src={user.avatar_url || '/images/bored-ape.png'} alt="profile" />
<div class="text-gray p-2">@{user.login}</div>
</section>
</a>
{:else}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<section class="flex" on:click={openGithub}>
<figure class="p-2">
<img width="28" height="28" src="/images/github.png" alt="profile" />
</figure>
<div class="text-gray p-2">Login</div>
</section>
{/if}

View file

@ -0,0 +1,75 @@
<script lang="ts">
import { page } from '$app/stores';
import { beforeUpdate } from 'svelte';
import { searchStore } from '$libs/stores';
import SearchInput from '@tea/ui/SearchInput/SearchInput.svelte';
import { navStore } from '$libs/stores';
import ProfileNavButton from './ProfileNavButton.svelte';
let { nextPath, prevPath } = navStore;
const onSearch = (term: string) => {
searchStore.search(term);
};
let currentPath: string;
beforeUpdate(async () => {
currentPath = $page.url.pathname;
});
</script>
<header class="border-gray flex w-full border border-l-0 border-r-0">
<a href="/">
<img width="40" height="40" src="/images/tea-icon.png" alt="tea" />
</a>
<ul class="text-gray flex h-10 gap-4 pl-4 align-middle leading-10">
<button on:click={navStore.back} class={$prevPath ? 'active' : ''}>&#8592</button>
<button on:click={navStore.next} class={$nextPath ? 'active' : ''}>&#8594</button>
</ul>
<SearchInput
class="flex-grow border border-none py-4"
size="small"
placeholder="search the tea store"
{onSearch}
/>
<ul class="text-gray flex gap-4 pr-4 pt-2 align-middle">
<button class="icon-filter hover:text-white" />
<button class="icon-share hover:text-white" />
<button class="icon-star-empty hover:text-white" />
</ul>
<ProfileNavButton />
</header>
<menu
class="border-gray text-gray flex h-10 gap-4 border border-l-0 border-r-0 border-t-0 pl-4 align-middle leading-10"
>
<a href="/cli" class={currentPath === '/cli' ? 'active' : ''}>install teaCli</a>
<a href="/documentation" class={currentPath === '/documentation' ? 'active' : ''}>documentation</a
>
<a href="/packages" class={currentPath === '/packages' ? 'active' : ''}>packages</a>
<a href="https://github.com/teaxyz" target="_blank" rel="noreferrer">
<i class="icon-star-empty" /> Github (5.2k)
</a>
</menu>
<style>
@tailwind base;
@tailwind components;
@tailwind utilities;
menu > a:hover {
color: white;
}
menu a.active {
color: white;
}
ul button {
pointer-events: none;
}
ul button.active {
color: white;
pointer-events: all;
}
</style>

View file

@ -1,67 +1,103 @@
// Modules to control application life and create native browser window
const { app, BrowserWindow, ipcMain, shell, dialog } = require('electron');
const windowStateManager = require('electron-window-state');
const { app, BrowserWindow, ipcMain } = require('electron');
const contextMenu = require('electron-context-menu');
const serve = require('electron-serve');
const path = require('path');
try {
require('electron-reloader')(module);
} catch (e) {
console.error(e);
}
const serveURL = serve({ directory: '.' });
const port = process.env.PORT || 3000;
const dev = !app.isPackaged;
let mainWindow;
let deeplink;
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('electron-fiddle', process.execPath, [
path.resolve(process.argv[1])
]);
}
} else {
app.setAsDefaultProtocolClient('electron-fiddle');
}
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
// Create mainWindow, load the rest of the app, etc...
app.whenReady().then(() => {
createWindow();
});
app.on('open-url', (event, url) => {
dialog.showErrorBox('Welcome Back', `You arrived from: ${url}`);
deeplink = url;
});
}
function createWindow() {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
let windowState = windowStateManager({
defaultWidth: 800,
defaultHeight: 600
});
// const path = deeplink.replace('electron-fiddle://', '');
mainWindow.loadFile('index.html');
const mainWindow = new BrowserWindow({
backgroundColor: 'whitesmoke',
autoHideMenuBar: true,
trafficLightPosition: {
x: 17,
y: 32
},
minHeight: 450,
minWidth: 500,
webPreferences: {
enableRemoteModule: true,
contextIsolation: true,
nodeIntegration: true,
spellcheck: false,
devTools: dev,
preload: path.join(app.getAppPath(), 'preload.cjs')
},
x: windowState.x,
y: windowState.y,
width: windowState.width,
height: windowState.height
});
windowState.manage(mainWindow);
mainWindow.once('ready-to-show', () => {
mainWindow.show();
mainWindow.focus();
});
mainWindow.on('close', () => {
windowState.saveState(mainWindow);
});
return mainWindow;
}
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
contextMenu({
showLookUpSelection: false,
showSearchWithGoogle: false,
showCopyImage: false,
prepend: (defaultActions, params, browserWindow) => [
{
label: 'Make App 💻'
}
]
});
function loadVite(port) {
mainWindow.loadURL(`http://localhost:${port}`).catch((e) => {
console.log('Error loading URL, retrying', e);
setTimeout(() => {
loadVite(port);
}, 200);
});
}
function createMainWindow() {
mainWindow = createWindow();
mainWindow.once('close', () => {
mainWindow = null;
});
if (dev) loadVite(port);
else serveURL(mainWindow);
}
app.once('ready', createMainWindow);
app.on('activate', () => {
if (!mainWindow) {
createMainWindow();
}
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
// Handle window controls via IPC
ipcMain.on('shell:open', () => {
const pageDirectory = __dirname.replace('app.asar', 'app.asar.unpacked');
const pagePath = path.join('file://', pageDirectory, 'index.html');
shell.openExternal(pagePath);
ipcMain.on('to-main', (event, count) => {
return mainWindow.webContents.send('from-main', `next count is ${count + 1}`);
});

View file

@ -1,11 +0,0 @@
<script>
let count = 0;
const increment = () => {
count += 1;
};
</script>
<button class="button" on:click={increment}>
Clicks: {count}
</button>

View file

@ -1,10 +0,0 @@
<!-- Based on: https://www.w3schools.com/howto/howto_css_fixed_sidebar.asp -->
<script>
import '../app.css';
</script>
<div class="sidenav">
<a href="/page1">Page 1</a>
<a href="/page2">Page 2</a>
<a href="/page3">Page 3</a>
</div>

View file

@ -0,0 +1,49 @@
import { getPkgBottles } from '../teaDir';
describe('teaDir module', () => {
it('should getPkgBottles from nested Dir object/s', () => {
const results = getPkgBottles({
name: 'kkos',
path: '/Users/x/.tea/github.com/kkos',
children: [
{ name: '.DS_Store', path: '/Users/x/.tea/github.com/kkos/.DS_Store' },
{
name: 'oniguruma',
path: '/Users/x/.tea/github.com/kkos/oniguruma',
children: [
{ name: '.DS_Store', path: '/Users/x/.tea/github.com/kkos/oniguruma/.DS_Store' },
{
path: '/Users/x/.tea/github.com/kkos/oniguruma/v6',
name: 'v6',
children: [
{ name: '.DS_Store', path: '/Users/x/.tea/github.com/kkos/oniguruma/v6/.DS_Store' }
]
},
{
name: 'v*',
path: '/Users/x/.tea/github.com/kkos/oniguruma/v*',
children: []
},
{
name: 'v6.9.8',
path: '/Users/x/.tea/github.com/kkos/oniguruma/v6.9.8',
children: []
},
{
name: 'v6.9',
path: '/Users/x/.tea/github.com/kkos/oniguruma/v6.9',
children: []
}
]
}
]
});
expect(results).toEqual([
'github.com/kkos/oniguruma/v*',
'github.com/kkos/oniguruma/v6',
'github.com/kkos/oniguruma/v6.9',
'github.com/kkos/oniguruma/v6.9.8'
]);
});
});

View file

@ -0,0 +1,359 @@
/**
* primarily used to make this desktop app work in the website preview setting in the CI/CD
* may contain fake/mock data
*
* TODO:
* * make cors work with api.tea.xyz/v1
*/
import type { Package, Review, AirtablePost, Bottle } from '@tea/ui/types';
import type { GUIPackage, Course, Category } from '../types';
import { PackageStates } from '../types';
import { loremIpsum } from 'lorem-ipsum';
import _ from 'lodash';
const packages: Package[] = [
{
slug: 'mesonbuild_com',
homepage: 'https://mesonbuild.com',
name: 'mesonbuild.com',
version: '0.63.3',
last_modified: '2022-10-06T15:45:08.000Z',
full_name: 'mesonbuild.com',
dl_count: 270745,
thumb_image_name: 'mesonbuild_com_option 1.jpg ',
maintainer: '',
desc: 'Fast and user friendly build system',
thumb_image_url: 'https://tea.xyz/Images/packages/mesonbuild_com.jpg',
installs: 0
},
{
slug: 'pixman_org',
homepage: 'http://www.pixman.org/',
maintainer: 'freedesktop',
name: 'pixman.org',
version: '0.40.0',
last_modified: '2022-09-26T19:37:47.000Z',
full_name: 'pixman.org',
dl_count: 0,
thumb_image_name: 'pixman_org_option 1.jpg ',
desc: 'Pixman is a library that provides low-level pixel manipulation features such as image compositing and trapezoid rasterization.',
thumb_image_url: 'https://tea.xyz/Images/packages/pixman_org.jpg',
installs: 0
},
{
slug: 'freedesktop_org_pkg_config',
homepage: 'https://freedesktop.org',
maintainer: 'freedesktop.org',
name: 'pkg-config',
version: '0.29.2',
last_modified: '2022-10-20T01:32:15.000Z',
full_name: 'freedesktop.org/pkg-config',
dl_count: 2661501,
thumb_image_name: 'freedecktop_org_pkg_config option 1.jpg ',
desc: 'Manage compile and link flags for libraries',
thumb_image_url: 'https://tea.xyz/Images/packages/freedesktop_org_pkg_config.jpg',
installs: 0
},
{
slug: 'gnu_org_gettext',
homepage: 'https://gnu.org',
maintainer: 'gnu.org',
name: 'gettext',
version: '0.21.1',
last_modified: '2022-10-20T01:23:46.000Z',
full_name: 'gnu.org/gettext',
dl_count: 3715970,
thumb_image_name: 'gnu_org_gettext_option 1.jpg ',
desc: 'GNU internationalization (i18n) and localization (l10n) library',
thumb_image_url: 'https://tea.xyz/Images/packages/gnu_org_gettext.jpg',
installs: 0
},
{
slug: 'ipfs_tech',
homepage: 'https://ipfs.tech',
name: 'ipfs.tech',
version: '0.16.0',
last_modified: '2022-10-19T21:36:52.000Z',
full_name: 'ipfs.tech',
dl_count: 14457,
thumb_image_name: 'ipfs_tech_option 2.jpg ',
maintainer: '',
desc: 'Peer-to-peer hypermedia protocol',
thumb_image_url: 'https://tea.xyz/Images/packages/ipfs_tech.jpg',
installs: 0
},
{
slug: 'nixos_org_patchelf',
homepage: 'https://nixos.org',
maintainer: 'nixos.org',
name: 'patchelf',
version: '0.15.0',
last_modified: '2022-09-27T04:50:44.000Z',
full_name: 'nixos.org/patchelf',
dl_count: 0,
thumb_image_name: 'nixos_org_patchelf_option 1.jpg ',
desc: 'PatchELF is a simple utility for modifying existing ELF executables and libraries.',
thumb_image_url: 'https://tea.xyz/Images/packages/nixos_org_patchelf.jpg',
installs: 0
},
{
slug: 'tea_xyz',
homepage: 'https://tea.xyz',
maintainer: 'tea.xyz',
name: 'tea.xyz',
version: '0.8.6',
last_modified: '2022-10-19T19:13:51.000Z',
full_name: 'tea.xyz',
dl_count: 0,
thumb_image_name: 'tea_xyz_option 2.jpg ',
desc: 'Website of tea.xyz',
thumb_image_url: 'https://tea.xyz/Images/packages/tea_xyz.jpg',
installs: 0
},
{
slug: 'charm_sh_gum',
homepage: 'https://charm.sh',
maintainer: 'charm.sh',
name: 'gum',
version: '0.8.0',
last_modified: '2022-10-21T02:15:16.000Z',
full_name: 'charm.sh/gum',
dl_count: 0,
thumb_image_name: 'charm_sh_gum.jpg ',
desc: '',
thumb_image_url: 'https://tea.xyz/Images/packages/charm_sh_gum.jpg',
installs: 0
},
{
slug: 'pyyaml_org',
homepage: 'https://pyyaml.org',
name: 'pyyaml.org',
version: '0.2.5',
last_modified: '2022-10-03T15:35:14.000Z',
full_name: 'pyyaml.org',
dl_count: 107505,
thumb_image_name: 'pyyaml_org_option 1.jpg ',
maintainer: '',
desc: 'YAML framework for Python',
thumb_image_url: 'https://tea.xyz/Images/packages/pyyaml_org.jpg',
installs: 0
},
{
slug: 'tea_xyz_gx_cc',
homepage: 'https://tea.xyz',
maintainer: 'tea.xyz',
name: 'cc',
version: '0.1.0',
last_modified: '2022-10-19T16:47:44.000Z',
full_name: 'tea.xyz/gx/cc',
dl_count: 0,
thumb_image_name: 'tea_xyz_gx.jpg ',
desc: '',
thumb_image_url: 'https://tea.xyz/Images/packages/tea_xyz_gx_cc.jpg',
installs: 0
}
];
export async function getPackages(): Promise<GUIPackage[]> {
await delay(2000);
return packages.map((pkg) => {
return {
...pkg,
state: PackageStates.AVAILABLE
};
});
}
export async function getFeaturedPackages(): Promise<Package[]> {
await delay(2000);
return packages.slice(0, 4);
}
export async function getPackageReviews(full_name: string): Promise<Review[]> {
console.log(`generating reviews for ${full_name}`);
const reviewCount = _.random(9, 21);
const reviews: Review[] = [];
for (let i = 0; i < reviewCount; i++) {
const title = loremIpsum({
count: _.random(2, 5),
format: 'plain',
paragraphLowerBound: 3,
paragraphUpperBound: 7,
random: Math.random,
sentenceLowerBound: 5,
sentenceUpperBound: 15,
units: 'words'
});
const comment = loremIpsum({
count: 2,
format: 'plain',
paragraphLowerBound: 3,
paragraphUpperBound: 7,
random: Math.random,
sentenceLowerBound: 5,
sentenceUpperBound: 15,
units: 'sentences'
});
const rating = _.random(0, 5);
reviews.push({
title,
comment,
rating
});
}
await delay(2000);
return reviews;
}
export async function installPackage(full_name: string) {
console.log('installing: ', full_name);
await delay(10000);
}
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function getFeaturedCourses(): Promise<Course[]> {
const mockCourses: Course[] = [
{
title: 'Developing With Tea',
sub_title: 'by Mxcl',
link: '#',
banner_image_url: 'https://tea.xyz/Images/packages/mesonbuild_com.jpg'
},
{
title: 'Brewing Tea',
sub_title: 'by Mxcl',
link: '#',
banner_image_url: 'https://tea.xyz/Images/packages/tea_xyz_gx_cc.jpg'
},
{
title: 'Harvesting Tea',
sub_title: 'by Mxcl',
link: '#',
banner_image_url: 'https://tea.xyz/Images/packages/ipfs_tech.jpg'
}
];
return mockCourses;
}
export async function getTopPackages(): Promise<GUIPackage[]> {
await delay(500);
return packages.slice(0, 9).map((pkg) => {
return {
...pkg,
state: PackageStates.AVAILABLE
};
});
}
export async function getAllPosts(type: string): Promise<AirtablePost[]> {
console.log('filter by type:', type);
const posts: AirtablePost[] = [
{
airtable_record_id: 'a',
link: 'https://google.com',
title: 'Tea Inc releases game changing api!',
sub_title: 'lorem ipsum dolor sit amet',
short_description: 'lorem ipsum dolor sit amet',
thumb_image_url: '/images/bored-ape.png',
thumb_image_name: 'borred-api.png',
created_at: new Date(),
updated_at: new Date(),
published_at: new Date(),
tags: ['news']
},
{
airtable_record_id: 'b',
link: 'https://google.com',
title: 'Bored Ape not bored anymore',
sub_title: 'lorem ipsum dolor sit amet',
short_description: 'lorem ipsum dolor sit amet',
thumb_image_url: '/images/bored-ape.png',
thumb_image_name: 'borred-api.png',
created_at: new Date(),
updated_at: new Date(),
published_at: new Date(),
tags: ['news']
},
{
airtable_record_id: 'c',
link: 'https://google.com',
title: 'Markdown can be executed! hoohah!',
sub_title: 'lorem ipsum dolor sit amet',
short_description: 'lorem ipsum dolor sit amet',
thumb_image_url: '/images/bored-ape.png',
thumb_image_name: 'borred-api.png',
created_at: new Date(),
updated_at: new Date(),
published_at: new Date(),
tags: ['news']
}
];
return posts;
}
export async function getCategorizedPackages(): Promise<Category[]> {
const mockPackages = packages.slice(0, 9).map((pkg) => ({
...pkg,
state: PackageStates.AVAILABLE
}));
return [
{
label: 'framework essentials',
cta_label: 'View all essentials >',
packages: mockPackages
},
{
label: 'star-struck heavyweights',
cta_label: 'View all star-strucks >',
packages: mockPackages
},
{
label: 'simply delightful',
cta_label: 'View all delightful packages >',
packages: mockPackages
}
];
}
export async function getDeviceAuth(deviceId: string): Promise<any> {
// const data = await get<any>(`/auth/device/${deviceId}`);
return {
status: 'SUCCESS',
user: {
developer_id: 'xxx',
name: 'Neil paul Molina',
login: 'getneil',
avatar_url: 'https://avatars.githubusercontent.com/u/7913978?v=4',
created_at: 'xxx',
updated_at: 'xxx',
country: 'germany',
wallet: 'wallet'
},
key: 'xxx'
};
}
export async function getPackageBottles(name: string): Promise<Bottle[]> {
return [
{ name, platform: 'darwin', arch: 'aarch64', version: '3.39.4' },
{ name, platform: 'darwin', arch: 'aarch64', version: '3.40.0' },
{ name, platform: 'darwin', arch: 'x86-64', version: '3.39.4' },
{ name, platform: 'darwin', arch: 'x86-64', version: '3.40.0' },
{ name, platform: 'linux', arch: 'aarch64', version: '3.39.4' },
{ name, platform: 'linux', arch: 'aarch64', version: '3.40.0' },
{ name, platform: 'linux', arch: 'x86-64', version: '3.39.4' },
{ name, platform: 'linux', arch: 'x86-64', version: '3.40.0' }
];
}
export async function registerDevice(): Promise<string> {
return 'uuid1234';
}

View file

@ -0,0 +1,32 @@
export async function installPackageCommand(full_name: string) {
return new Promise((resolve, reject) => {
// const teaInstallCommand = new Command('tea-install', [`+${full_name}`, 'true']);
// teaInstallCommand.on('error', reject);
// const handleLineOutput = async (line: string | { code: number }) => {
// const c = await child;
// if (
// (typeof line === 'string' && line.includes('installed:')) ||
// (typeof line !== 'string' && line?.code === 0)
// ) {
// c.kill();
// resolve(c.pid);
// } else if (typeof line !== 'string' && line?.code === 1) {
// reject();
// }
// };
// teaInstallCommand.stdout.on('data', handleLineOutput);
// teaInstallCommand.stderr.on('data', handleLineOutput);
// teaInstallCommand.on('close', (line: string) => {
// console.log('command closed!');
// handleLineOutput(line || '');
// });
// teaInstallCommand.on('error', (line: string) => {
// console.log('command error!', line);
// handleLineOutput(line || '');
// });
// const child = teaInstallCommand.spawn();
setTimeout(resolve, 10000);
});
}

View file

@ -0,0 +1,144 @@
import { writable } from 'svelte/store';
import Fuse from 'fuse.js';
import type { Package, Review, AirtablePost } from '@tea/ui/types';
import type { GUIPackage } from '$libs/types';
import { getFeaturedPackages, getPackageReviews, getAllPosts } from '@api';
import initAuthStore from './stores/auth';
import initNavStore from './stores/nav';
import initPackagesStore from './stores/pkgs';
export const featuredPackages = writable<Package[]>([]);
export const packagesStore = initPackagesStore();
export const initializeFeaturedPackages = async () => {
console.log('intialize featured packages');
const packages = await getFeaturedPackages();
featuredPackages.set(packages);
};
interface PackagesReview {
[full_name: string]: Review[];
}
function initPackagesReviewStore() {
const { update, subscribe } = writable<PackagesReview>({});
let packagesReviews: PackagesReview = {};
subscribe((v) => (packagesReviews = v));
const getSetPackageReviews = async (full_name: string) => {
if (full_name && !packagesReviews[full_name]) {
packagesReviews[full_name] = [];
const reviews = await getPackageReviews(full_name);
update((v) => {
return {
...v,
[full_name]: reviews
};
});
}
};
return {
subscribe: (full_name: string, reset: (reviews: Review[]) => void) => {
getSetPackageReviews(full_name);
return subscribe((value) => {
if (value[full_name]) {
reset(value[full_name]);
}
});
}
};
}
export const packagesReviewStore = initPackagesReviewStore();
function initPosts() {
let initialized = false;
const { subscribe, set } = writable<AirtablePost[]>([]);
const posts: AirtablePost[] = [];
let postsIndex: Fuse<AirtablePost>;
if (!initialized) {
initialized = true;
getAllPosts().then(set);
}
subscribe((v) => {
posts.push(...v);
postsIndex = new Fuse(posts, {
keys: ['title', 'sub_title', 'short_description', 'tags']
});
});
return {
subscribe,
search: async (term: string, limit = 10) => {
const res = postsIndex.search(term, { limit });
const matchingPosts: AirtablePost[] = res.map((v) => v.item);
return matchingPosts;
},
subscribeByTag: (tag: string, cb: (posts: AirtablePost[]) => void) => {
subscribe((newPosts: AirtablePost[]) => {
const filteredPosts = newPosts.filter((post) => post.tags.includes(tag));
cb(filteredPosts);
});
}
};
}
export const postsStore = initPosts();
function initSearchStore() {
const searching = writable<boolean>(false);
const { subscribe, set } = writable<string>('');
const packagesSearch = writable<GUIPackage[]>([]);
const postsSearch = writable<AirtablePost[]>([]);
// TODO:
// should use algolia if user is somehow online
const packagesFound: GUIPackage[] = [];
let term = '';
subscribe((v) => (term = v));
packagesSearch.subscribe((v) => packagesFound.push(...v));
return {
term,
searching,
packagesSearch,
postsSearch,
packagesFound,
subscribe,
search: async (term: string) => {
searching.set(true);
try {
if (term) {
const [resultPkgs, resultPosts] = await Promise.all([
packagesStore.search(term, 5),
postsStore.search(term, 10)
]);
packagesSearch.set(resultPkgs);
postsSearch.set(resultPosts);
} else {
packagesSearch.set([]);
postsSearch.set([]);
}
set(term);
} finally {
searching.set(false);
}
}
};
}
export const searchStore = initSearchStore();
export const authStore = initAuthStore();
export const navStore = initNavStore();

View file

@ -0,0 +1,131 @@
import { writable } from 'svelte/store';
// import { app } from 'electron';
// import { join } from 'upath';
// import fs from 'fs';
import { getDeviceAuth, registerDevice } from '@api';
import type { Developer } from '@tea/ui/types';
const basePath = '.tea/tea.xyz/gui';
export interface Session {
device_id?: string;
key?: string;
user?: Developer;
}
export let session: Session | null = null;
export const getSession = async (): Promise<Session | null> => {
// await app.whenReady();
// if (session && session?.user) return session;
// const homePath = app.getPath('home');
// const sessionFilePath = await join(homePath, basePath, 'tmp.dat');
// try {
// const encryptedData = fs.readFileSync(sessionFilePath, 'utf-8');
// session = JSON.parse(encryptedData || '{}') as Session;
// return session;
// } catch (error) {
// return null;
// }
return null;
};
export default function initAuthStore() {
const sessionStore = writable<Session>({});
let pollLoop = 0;
const deviceIdStore = writable<string>('');
let deviceId = '';
initSession().then((sess) => {
if (sess) {
session = sess;
sessionStore.set(sess);
deviceIdStore.set(sess.device_id!);
deviceId = sess.device_id!;
}
});
let timer: NodeJS.Timer | null;
async function updateSession(data: Session) {
const localSession = {
device_id: deviceId,
key: data.key,
user: data.user
};
saveLocallySessionData(localSession);
sessionStore.set(localSession);
}
async function pollSession() {
if (!timer) {
timer = setInterval(async () => {
pollLoop++;
try {
const data = await getDeviceAuth(deviceId);
console.log('dd', deviceId, data);
if (data.status === 'SUCCESS') {
updateSession({
key: data.key,
user: data.user
});
timer && clearInterval(timer);
timer = null;
}
} catch (error) {
console.error(error);
}
if (pollLoop > 20 && timer) {
clearInterval(timer);
pollLoop = 0;
timer = null;
}
}, 2000);
}
}
return {
deviceId,
deviceIdStore,
subscribe: (cb: (u: Developer) => void) => {
return sessionStore.subscribe((v) => v?.user && cb(v.user));
},
pollSession
};
}
const initSession = async (): Promise<Session | void> => {
// const homePath = app.getPath('home');
// try {
// await fs.mkdirSync(join(homePath, basePath));
// } catch (error) {
// console.error(error);
// }
// const session = await getLocalSessionData();
// return session;
};
const getLocalSessionData = async (): Promise<Session | void> => {
let data: Session;
try {
const session = await getSession();
if (!session) throw new Error('no session');
data = session;
} catch (error) {
console.error('register device:', error);
const deviceId = await registerDevice();
data = {
device_id: deviceId
};
await saveLocallySessionData(data);
}
return data;
};
const saveLocallySessionData = async (data: Session) => {
// const homePath = app.getPath('home');
// const sessionFilePath = await join(homePath, basePath, 'tmp.dat');
// // TODO: encrypt first
// await fs.writeFileSync(sessionFilePath, JSON.stringify(data), 'utf-8');
};

View file

@ -0,0 +1,55 @@
import { writable } from 'svelte/store';
import { goto } from '$app/navigation';
export default function initNavStore() {
const historyStore = writable<string[]>(['/']);
let history = ['/'];
historyStore.subscribe((v) => (history = v));
const prevPathStore = writable<string>('');
const nextPathStore = writable<string>('');
let currentIndex = 0; // if non next/back click
let isMovingNext = false;
let isMovingBack = false;
return {
historyStore,
prevPath: prevPathStore,
nextPath: nextPathStore,
next: () => {
if (currentIndex < history.length - 1) {
isMovingNext = true;
goto(history[currentIndex + 1]);
prevPathStore.set(history[currentIndex]);
currentIndex++;
if (currentIndex >= history.length - 1) nextPathStore.set('');
}
},
back: () => {
if (currentIndex > 0) {
isMovingBack = true;
goto(history[currentIndex - 1]);
nextPathStore.set(history[currentIndex]);
currentIndex--;
if (currentIndex === 0) prevPathStore.set('');
}
},
setNewPath: (newNextPath: string, newPrevPath: string) => {
const oldCurrentPath = history[currentIndex];
const isNavArrows = isMovingBack || isMovingNext;
if (!isNavArrows && newNextPath !== oldCurrentPath) {
historyStore.update((history) => {
const cleanHistory = history.filter((_v, i) => i <= currentIndex);
currentIndex = cleanHistory.length;
prevPathStore.set(cleanHistory[currentIndex - 1]);
return [...cleanHistory, newNextPath];
});
}
isMovingNext = false;
isMovingBack = false;
}
};
}

View file

@ -0,0 +1,36 @@
import { writable } from 'svelte/store';
import type { GUIPackage } from '../types';
import { getPackages } from '@api';
import Fuse from 'fuse.js';
export default function initPackagesStore() {
let initialized = false;
const { subscribe, set } = writable<GUIPackage[]>([]);
const packages: GUIPackage[] = [];
let packagesIndex: Fuse<GUIPackage>;
if (!initialized) {
initialized = true;
getPackages().then((pkgs) => {
set(pkgs);
packagesIndex = new Fuse(pkgs, {
keys: ['name', 'full_name', 'desc']
});
});
}
subscribe((v) => packages.push(...v));
return {
packages,
subscribe,
search: async (term: string, limit = 5): Promise<GUIPackage[]> => {
if (!term || !packagesIndex) return [];
// TODO: if online, use algolia else use Fuse
const res = packagesIndex.search(term, { limit });
const matchingPackages: GUIPackage[] = res.map((v) => v.item);
return matchingPackages;
}
};
}

View file

@ -0,0 +1,61 @@
import { app } from 'electron';
import fs from 'fs';
import { join } from 'upath';
type Dir = {
name: string;
path: string;
children?: Dir[];
};
export async function getInstalledPackages() {
// const homePath = app.getPath('home');
// const packageFolders = (await readDir('.tea/', {
// dir: BaseDirectory.Home,
// recursive: true
// })) as Dir[];
// const pkgs = packageFolders
// .filter((p) => p.name !== 'tea.xyz')
// .map(getPkgBottles)
// .filter((pkgBottles) => pkgBottles.length)
// .map((pkgBottles) => {
// const versions = pkgBottles.map((v) => v.split('/v')[1]);
// const full_name = pkgBottles[0].split('/v')[0];
// const isSemverVersion = versions.filter((v) => semverTest.test(v));
// const isNotAsterisk = versions.filter((v) => v !== '*');
// const version =
// (isSemverVersion.length && isSemverVersion[0]) ||
// (isNotAsterisk.length && isNotAsterisk[0]) ||
// '*';
// return {
// version,
// full_name
// };
// });
// return pkgs;
return [];
}
const semverTest =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g;
export const getPkgBottles = (packageDir: Dir): string[] => {
const bottles: string[] = [];
const pkg = packageDir.path.split('.tea/')[1];
const version = pkg.split('/v')[1];
const isVersion = semverTest.test(version) || !isNaN(+version) || version === '*';
if (version && isVersion) {
bottles.push(pkg);
} else if (packageDir?.children?.length) {
const childBottles = packageDir.children
.map(getPkgBottles)
.reduce((arr, bottles) => [...arr, ...bottles], []);
bottles.push(...childBottles);
}
return bottles.filter((b) => b !== undefined).sort(); // ie: ["gohugo.io/v*", "gohugo.io/v0", "gohugo.io/v0.108", "gohugo.io/v0.108.0"]
};

View file

@ -0,0 +1,44 @@
// as much possible add types here that are unique to @tea/gui use only
// else
// please use the package @tea/ui/src/types.ts
// things that go there are shared types/shapes like ie: Package
import type { Package, Developer } from '@tea/ui/types';
export enum PackageStates {
AVAILABLE,
INSTALLED,
INSTALLING,
UNINSTALLED
}
export type GUIPackage = Package & {
state: PackageStates;
installed_version?: string;
};
export type Course = {
title: string;
sub_title: string;
banner_image_url: string;
link: string;
};
export type Category = {
label: string;
cta_label: string;
packages: GUIPackage[];
};
export enum AuthStatus {
UNKNOWN = 'UNKNOWN',
PENDING = 'PENDING',
SUCCESS = 'SUCCESS',
FAILED = 'FAILED'
}
export type DeviceAuth = {
status: AuthStatus;
user: Developer;
key: string;
};

View file

@ -0,0 +1,63 @@
import { net, app } from 'electron';
import type { Session } from '$libs/stores/auth';
import bcrypt from 'bcryptjs';
import { getSession } from '$libs/stores/auth';
import urlJoin from 'url-join';
export const baseUrl = 'https://api.tea.xyz/v1';
export async function get<T>(path: string, query?: { [key: string]: string }) {
console.log(`GET /api/${path}`);
await app.isReady(); // wait for electrong dont remove
const [session] = await Promise.all([getSession()]);
const headers =
session?.device_id && session?.user
? await getHeaders(`GET/${path}`, session)
: { Authorization: 'public ' };
return new Promise((resolve, reject) => {
const url = urlJoin(baseUrl, path);
const req = net.request({
method: 'GET',
url
});
for (const k in headers) {
const v = headers[k as keyof typeof headers];
if (v) req.setHeader(k, v);
}
const buffer: Buffer[] = [];
req.on('response', (res) => {
res.on('error', reject);
res.on('data', (b) => buffer.push(b));
res.on('end', () => {
const bodyRaw = Buffer.concat(buffer);
const body = JSON.parse(bodyRaw.toString());
resolve(body);
});
});
req.on('error', reject);
});
}
async function getHeaders(path: string, session: Session) {
const unixMs = new Date().getTime();
const unixHexSecs = Math.round(unixMs / 1000).toString(16); // hex
const deviceId = session.device_id?.split('-')[0];
const preHash = [unixHexSecs, session.key, deviceId, path].join('');
const Authorization = bcrypt.hashSync(preHash, 10);
return {
Authorization,
['tea-ts']: unixMs.toString(),
['tea-uid']: session.user?.developer_id,
['tea-gui_id']: session.device_id
};
}

View file

@ -0,0 +1,13 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electron', {
send: (channel, data) => {
ipcRenderer.send(channel, data);
},
sendSync: (channel, data) => {
ipcRenderer.sendSync(channel, data);
},
receive: (channel, func) => {
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
});

View file

@ -1 +0,0 @@
export const prerender = true;

View file

@ -1,10 +1,87 @@
<script>
import '../app.css';
import SideBar from '$lib/SideBar.svelte';
<script lang="ts">
import '$appcss';
// import { navigating } from '$app/stores';
// import { afterNavigate } from '$app/navigation';
import TopBar from '$components/TopBar/TopBar.svelte';
import FooterLinks from '$components/FooterLinks/FooterLinks.svelte';
import { navStore } from '$libs/stores';
import SearchPopupResults from '$components/SearchPopupResults/SearchPopupResults.svelte';
let view: HTMLElement;
// $: if ($navigating) view.scrollTop = 0;
// afterNavigate(({ from, to }) => {
// if (to && to?.route.id && from && from?.url) {
// const nextPath = to.url.href.replace(to.url.origin, '');
// const fromPath = from?.url.href.replace(from.url.origin, '');
// navStore.setNewPath(nextPath, fromPath || '/');
// }
// });
</script>
<SideBar />
<div class="main">
<div id="main-layout" class="w-full">
<TopBar />
<section class="relative pt-24" bind:this={view}>
<figure />
<div class="content">
<slot />
</div>
<footer class="border-gray mt-8 w-full border border-r-0 bg-black">
<FooterLinks />
</footer>
<!-- <SearchPopupResults /> -->
</section>
</div>
<style>
#main-layout {
height: 100vh;
}
section {
height: calc(100vh - 82px);
overflow-y: scroll;
box-sizing: border-box;
}
figure {
z-index: 0;
position: fixed;
top: 220px;
left: 0px;
right: 0px;
bottom: 0px;
background-image: url('/images/gui-background-grid.svg');
}
@media screen and (min-width: 1440px) {
figure {
background-size: cover;
background-repeat: repeat-y;
}
.content {
padding: 0vw 3.6vw !important;
}
}
@media screen and (max-width: 1440px) {
figure {
background-size: contain;
background-repeat: repeat;
}
.content {
padding: 0vw 3.33vw;
}
}
slot {
z-index: 1;
}
div {
position: relative;
}
footer {
height: 100px;
}
</style>

View file

@ -0,0 +1,2 @@
export const ssr = false;
export const prerender = false;

View file

@ -1,49 +1,51 @@
<script>
import Counter from '$lib/Counter.svelte';
<!-- home / discover / welcome page -->
<script lang="ts">
import '$appcss';
import PageHeader from '$components/PageHeader/PageHeader.svelte';
import EssentialWorkshops from '$components/EssentialWorkshops/EssentialWorkshops.svelte';
import Packages from '$components/Packages/Packages.svelte';
import News from '$components/News/News.svelte';
import HeaderCard from '@tea/ui/HeaderCard/HeaderCard.svelte';
import ListAction from '@tea/ui/ListAction/ListAction.svelte';
</script>
<svelte:head>
<title>Svelte Template</title>
</svelte:head>
<main>
<h1 class="text-primary">Hello world!</h1>
<Counter />
<p>
Visit <a href="https://svelte.dev">svelte.dev</a> to learn how to build Svelte apps.
<i class="fas fa-heart fa-fw" />
</p>
</main>
<style lang="css">
main {
text-align: center;
padding: 1em;
margin: 0 auto;
}
h1 {
text-transform: uppercase;
font-size: 4rem;
font-weight: 100;
line-height: 1.1;
margin: 4rem auto;
max-width: 14rem;
}
p {
max-width: 14rem;
margin: 2rem auto;
line-height: 1.35;
}
@media (min-width: 480px) {
h1 {
max-width: none;
}
p {
max-width: none;
}
}
</style>
<div>
<PageHeader coverUrl="/images/headers/header_bg_1.png">Discover</PageHeader>
<section class="mt-8 mb-8">
<Packages title="FOUNDATION ESSENTIALS" />
</section>
<PageHeader coverUrl="/images/headers/header_bg_1.png">ASSET TYPE</PageHeader>
<section class="mt-8 mb-8 flex gap-4">
<div>
<HeaderCard
title="Browser Packages"
imgUrl="/images/bored-ape.png"
ctaUrl="/packages"
ctaLabel="Browse packages >"
articleTitle="What are packages?"
description="Collections of files aggregated to form larger frameworks & functions. Think Python or Node.js."
/>
<ListAction title="Top packages this week" mainCtaTitle="VIEW ALL PACKAGES" />
</div>
<div>
<HeaderCard
title="Browse Scripts"
imgUrl="/images/bored-ape.png"
ctaUrl="/packages"
ctaLabel="Browse scripts >"
articleTitle="What are scripts?"
description="Invisible applications that chain packages together in order to perform cool actions on your computer."
/>
<ListAction title="Top scripts this week" mainCtaTitle="VIEW ALL SCRIPTS" />
</div>
</section>
<PageHeader coverUrl="/images/headers/header_bg_1.png">TUTORIALS</PageHeader>
<section class="mt-8 mb-8">
<EssentialWorkshops title="WORKSHOPS TO GET STARTED" ctaLabel="Read more articles >" />
</section>
<PageHeader coverUrl="/images/headers/header_bg_1.png">OPEN-SOURCE NEWS</PageHeader>
<section class="mt-8">
<News />
</section>
</div>

View file

@ -0,0 +1,18 @@
<script>
import '$appcss';
import PageHeader from '$components/PageHeader/PageHeader.svelte';
import CliBanner from '$components/CliBanner/CliBanner.svelte';
import BigBlackSpace from '$components/BigBlackSpace/BigBlackSpace.svelte';
</script>
<div>
<PageHeader>INSTALL TEA</PageHeader>
<section>
<CliBanner />
</section>
<section class="mt-8">
<BigBlackSpace />
</section>
</div>

View file

@ -0,0 +1,18 @@
<script>
import '$appcss';
import PageHeader from '$components/PageHeader/PageHeader.svelte';
import FeaturedCourses from '$components/FeaturedCourses/FeaturedCourses.svelte';
import EssentialWorkshops from '$components/EssentialWorkshops/EssentialWorkshops.svelte';
</script>
<div>
<PageHeader>Documentation</PageHeader>
<section>
<FeaturedCourses />
</section>
<section class="mt-8">
<EssentialWorkshops />
</section>
</div>

View file

@ -0,0 +1,10 @@
<script>
import '$appcss';
import PageHeader from '$components/PageHeader/PageHeader.svelte';
import SearchPackages from '$components/SearchPackages/SearchPackages.svelte';
</script>
<div>
<PageHeader>Packages</PageHeader>
<SearchPackages />
</div>

View file

@ -0,0 +1,105 @@
<script lang="ts">
import '$appcss';
import { onMount } from 'svelte';
import PageHeader from '$components/PageHeader/PageHeader.svelte';
import { packagesReviewStore } from '$libs/stores';
import PackageBanner from '$components/PackageBanner/PackageBanner.svelte';
import type { Review, Bottle } from '@tea/ui/types';
import SuggestedPackages from '$components/SuggestedPackages/SuggestedPackages.svelte';
import Tabs from '@tea/ui/Tabs/Tabs.svelte';
import type { Tab } from '@tea/ui/types';
import Bottles from '@tea/ui/Bottles/Bottles.svelte';
import { getPackageBottles } from '@api';
import PackageMetas from '@tea/ui/PackageMetas/PackageMetas.svelte';
import Markdown from '@tea/ui/Markdown/Markdown.svelte';
import PackageSnippets from '@tea/ui/PackageSnippets/PackageSnippets.svelte';
/** @type {import('./$types').PageData} */
export let data;
import { packagesStore, featuredPackages } from '$libs/stores';
import type { Package } from '@tea/ui/types';
let pkg: Package;
let reviews: Review[];
let bottles: Bottle[] = [];
let versions: string[] = [];
let tabs: Tab[] = [];
const setPkg = (pkgs: Package[]) => {
const foundPackage = pkgs.find(({ slug }) => slug === data?.slug) as Package;
if (!pkg && foundPackage) {
pkg = foundPackage;
tabs.push({
label: 'details',
component: Markdown,
props: { pkg }
});
}
if (!reviews && pkg) {
packagesReviewStore.subscribe(pkg.full_name, (updatedReviews) => {
reviews = updatedReviews;
});
}
};
packagesStore.subscribe(setPkg);
featuredPackages.subscribe(setPkg);
onMount(async () => {
try {
const newBottles = await getPackageBottles(pkg.full_name);
const newVersion = newBottles.map((b) => b.version);
versions = [...new Set(newVersion)];
bottles.push(...newBottles);
tabs = [
...tabs,
{
label: `versions (${versions.length || 0})`,
component: Bottles,
props: {
bottles
}
}
];
} catch (err) {
console.error(err);
}
});
</script>
<div>
<PageHeader coverUrl={pkg.thumb_image_url}>{pkg.full_name}</PageHeader>
<section>
<PackageBanner {pkg} />
</section>
<section class="mt-8 flex gap-8">
<div class="w-2/3">
<Tabs class="bg-black" {tabs} />
</div>
<div class="w-1/3">
<PackageMetas />
</div>
</section>
<PageHeader class="mt-8" coverUrl="/images/headers/header_bg_1.png">SNIPPETS</PageHeader>
<section class="mt-8">
<PackageSnippets />
</section>
<!-- <section class="mt-8">
<PackageReviews reviews={reviews || []} />
</section> -->
{#if pkg}
<PageHeader class="mt-8" coverUrl="/images/headers/header_bg_1.png"
>YOU MAY ALSO LIKE...</PageHeader
>
<section class="mt-8">
<SuggestedPackages {pkg} />
</section>
{/if}
</div>

View file

@ -0,0 +1,11 @@
import type { LoadEvent } from '@sveltejs/kit';
/** @type {import('./$types').PageLoad} */
export function load({ params }: LoadEvent) {
// TODO: search package details here
return {
title: `${params.slug}`,
content: '',
slug: params.slug
};
}

View file

@ -1,12 +0,0 @@
<svelte:head>
<title>Page 1</title>
</svelte:head>
<h1 class="pagetitle">Page 1</h1>
<p>This is the page 1</p>
<div style="display: flex; justify-content: center;">
<a class="button" href="/">Home</a>
<a class="button" style="margin-left: 2px;" href="/page2">Page 2</a>
<a class="button" style="margin-left: 2px;" href="/page3">Page 3</a>
</div>

View file

@ -1,12 +0,0 @@
<svelte:head>
<title>Page 2</title>
</svelte:head>
<h1 class="pagetitle">Page 2</h1>
<p>This is the page 2</p>
<div style="display: flex; justify-content: center;">
<a class="button" style="margin-left: 2px;" href="/">Home</a>
<a class="button" style="margin-left: 2px;" href="/page1">Page 1</a>
<a class="button" style="margin-left: 2px;" href="/page3">Page 3</a>
</div>

View file

@ -1,12 +0,0 @@
<svelte:head>
<title>Page 3</title>
</svelte:head>
<h1 class="pagetitle">Page 3</h1>
<p>This is the page 3</p>
<div style="display: flex; justify-content: center;">
<a class="button" href="/">Home</a>
<a class="button" style="margin-left: 2px;" href="/page1">Page 1</a>
<a class="button" style="margin-left: 2px;" href="/page2">Page 2</a>
</div>

View file

@ -0,0 +1,28 @@
<script>
import '$appcss';
import PageHeader from '$components/PageHeader/PageHeader.svelte';
import ProfileBanner from '$components/ProfileBanner/ProfileBanner.svelte';
import Preflight from '$components/Preflight/Preflight.svelte';
import Badges from '$components/Badges/Badges.svelte';
import InstalledPackages from '$components/InstalledPackages/InstalledPackages.svelte';
</script>
<div>
<PageHeader>PROFILE</PageHeader>
<section>
<ProfileBanner />
</section>
<section class="mt-8 grid grid-cols-2 gap-8">
<div>
<Preflight />
</div>
<div>
<Badges />
</div>
</section>
<section class="mt-8">
<InstalledPackages />
</section>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12L19 6.41Z"/></svg>

After

Width:  |  Height:  |  Size: 232 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M12 19H5l.007-7H7v5h5v2Zm6.992-7H17V7h-5V5h7l-.008 7Z"/></svg>

After

Width:  |  Height:  |  Size: 176 B

View file

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="961.504" height="157.742" viewBox="0 0 961.504 157.742">
<g id="Group_1224" data-name="Group 1224" transform="translate(-159.75 -8720.324)">
<path id="Rectangle_616" data-name="Rectangle 616" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(160 8720.574)" fill="#949494"/>
<path id="Rectangle_617" data-name="Rectangle 617" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(240.084 8720.574)" fill="#949494"/>
<path id="Rectangle_618" data-name="Rectangle 618" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(320.168 8720.574)" fill="#949494"/>
<path id="Rectangle_619" data-name="Rectangle 619" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(400.251 8720.574)" fill="#949494"/>
<path id="Rectangle_620" data-name="Rectangle 620" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(480.334 8720.574)" fill="#949494"/>
<path id="Rectangle_621" data-name="Rectangle 621" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(560.418 8720.574)" fill="#949494"/>
<path id="Rectangle_622" data-name="Rectangle 622" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(640.502 8720.574)" fill="#949494"/>
<path id="Rectangle_2829" data-name="Rectangle 2829" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(880.754 8720.574)" fill="#949494"/>
<path id="Rectangle_623" data-name="Rectangle 623" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(720.586 8720.574)" fill="#949494"/>
<path id="Rectangle_2827" data-name="Rectangle 2827" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(960.838 8720.574)" fill="#949494"/>
<path id="Rectangle_624" data-name="Rectangle 624" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(800.669 8720.574)" fill="#949494"/>
<path id="Rectangle_2825" data-name="Rectangle 2825" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(1040.92 8720.574)" fill="#949494"/>
<path id="Rectangle_616-2" data-name="Rectangle 616" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(160 8799.358)" fill="#949494"/>
<path id="Rectangle_617-2" data-name="Rectangle 617" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(240.084 8799.358)" fill="#949494"/>
<path id="Rectangle_618-2" data-name="Rectangle 618" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(320.168 8799.358)" fill="#949494"/>
<path id="Rectangle_619-2" data-name="Rectangle 619" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(400.251 8799.358)" fill="#949494"/>
<path id="Rectangle_620-2" data-name="Rectangle 620" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(480.334 8799.358)" fill="#949494"/>
<path id="Rectangle_621-2" data-name="Rectangle 621" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(560.418 8799.358)" fill="#949494"/>
<path id="Rectangle_622-2" data-name="Rectangle 622" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(640.502 8799.358)" fill="#949494"/>
<path id="Rectangle_2830" data-name="Rectangle 2830" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(880.754 8799.358)" fill="#949494"/>
<path id="Rectangle_623-2" data-name="Rectangle 623" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(720.586 8799.358)" fill="#949494"/>
<path id="Rectangle_2828" data-name="Rectangle 2828" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(960.838 8799.358)" fill="#949494"/>
<path id="Rectangle_624-2" data-name="Rectangle 624" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(800.669 8799.358)" fill="#949494"/>
<path id="Rectangle_2826" data-name="Rectangle 2826" d="M-.25-.25H80.334V78.708H-.25Zm80.084.5H.25V78.208H79.834Z" transform="translate(1040.92 8799.358)" fill="#949494"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M20 14H4v-4h16"/></svg>

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -1,14 +1,28 @@
import preprocess from 'svelte-preprocess';
import adapter from '@sveltejs/adapter-static';
//import node from "@sveltejs/adapter-node";
import preprocess from 'svelte-preprocess';
//const dev = process.env.NODE_ENV == "development"
export default {
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [
preprocess({
postcss: true
})
],
kit: {
//adapter: node()
adapter: adapter()
},
preprocess: preprocess()
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: 'app.html'
}),
alias: {
'@tea/ui/*': '../ui/src/*'
}
// ssr: false,
// hydrate the <div id="svelte"> element in src/app.html
// target: '#svelte'
}
};
export default config;

View file

@ -0,0 +1,6 @@
import { theme, plugins } from '@tea/ui/tailwind.config.cjs';
module.exports = {
content: ['./src/**/*.{html,svelte,ts,js}', '../ui/src/**/*.{html,svelte,ts,js}'],
theme,
plugins: [...plugins]
};

View file

@ -0,0 +1,26 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"types": ["vitest/globals", "@testing-library/jest-dom"],
"paths": {
"$appcss": ["src/app.css"],
"$libs/*": ["src/libs/*"],
"@api": ["src/libs/api/mock.ts"],
"$components/*": ["src/components/*"],
"@tea/ui/*": ["../ui/src/*"]
}
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

View file

@ -1,11 +1,39 @@
import { sveltekit } from '@sveltejs/kit/vite';
import type { UserConfig } from 'vite';
import path from 'path';
/** @type {import('vite').UserConfig} */
const config = {
// const isMock = process.env.BUILD_FOR === 'preview';
const config: UserConfig = {
plugins: [sveltekit()],
resolve: {
alias: {
'@tea/ui/*': path.resolve('../ui/src/*'),
// this dynamic-ish static importing is followed by the svelte build
// but for vscode editing intellisense tsconfig.json is being used
// TODO: replace it with correct api
'@api': path.resolve('src/libs/api/mock.ts'),
$components: path.resolve('./src/components'),
$libs: path.resolve('./src/libs'),
$appcss: path.resolve('./src/app.css')
}
},
server: {
port: 3000
port: 3000,
fs: {
allow: ['..']
}
},
test: {
// Jest like globals
globals: true,
environment: 'jsdom',
include: ['src/**/*.{test,spec}.ts'],
// Extend jest-dom matchers
setupFiles: ['./setupTest.js'],
coverage: {
provider: 'c8'
}
}
};

View file

@ -71,7 +71,8 @@ export type Tab = {
| Array<string>
| Array<number>
| Array<Date>
| Array<Record<string, unknown>>;
| Array<Record<string, unknown>>
| Package;
};
component: ComponentType;
};

File diff suppressed because it is too large Load diff