✨ feat: Add qwik-react (#103)
This adds the following pages: The landing page (/) The pricing page (/pricing) The contact page (/contact) The changelog page (/changelog) Terms Of Service page (/terms) Privacy Policy (/privacy)
4
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["dbaeumer.vscode-eslint", "unifiedjs.vscode-mdx"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
12
.vscode/settings.json
vendored
@@ -1,3 +1,11 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||
}
|
||||
"material-icon-theme.activeIconPack": "qwik",
|
||||
"emmet.includeLanguages": {
|
||||
"typescriptreact": "html"
|
||||
},
|
||||
"emmet.preferences": {
|
||||
// to ensure closing tags are used (e.g. <img/> not just <img> like in HTML)
|
||||
// https://github.com/microsoft/vscode/commit/083bf9020407ea5a91199eb1f0b373859df8d600#diff-88456bc9b7caa2f8126aea0107b4671db0f094961aaf39a7c689f890e23aaaba
|
||||
"output.selfClosingStyle": "xhtml"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@nestri/docs",
|
||||
"name": "docs",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbo --port 3001",
|
||||
"developer": "next dev --turbo --port 3001",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
|
||||
33
apps/nexus/.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# prod
|
||||
dist/
|
||||
|
||||
# dev
|
||||
.yarn/
|
||||
!.yarn/releases
|
||||
.vscode/*
|
||||
!.vscode/launch.json
|
||||
!.vscode/*.code-snippets
|
||||
.idea/workspace.xml
|
||||
.idea/usage.statistics.xml
|
||||
.idea/shelf
|
||||
|
||||
# deps
|
||||
node_modules/
|
||||
.wrangler
|
||||
|
||||
# env
|
||||
.env
|
||||
.env.production
|
||||
.dev.vars
|
||||
|
||||
# logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
12
apps/nexus/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Nexus
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```
|
||||
npm run deploy
|
||||
```
|
||||
BIN
apps/nexus/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
14
apps/nexus/assets/favicon.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="48.672001"
|
||||
height="36.804001"
|
||||
viewBox="0 0 12.8778 9.7377253"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="layer1">
|
||||
<path
|
||||
d="m 2.093439,1.7855532 h 8.690922 V 2.2639978 H 2.093439 Z m 0,2.8440874 h 8.690922 V 5.1080848 H 2.093439 Z m 0,2.8440866 h 8.690922 V 7.952172 H 2.093439 Z"
|
||||
style="font-size:12px;fill:#ff4f01;fill-opacity:1;fill-rule:evenodd;stroke:#ff4f01;stroke-width:1.66201;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 590 B |
BIN
apps/nexus/bun.lockb
Normal file
23
apps/nexus/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@nestri/nexus",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"description": "Nestri's core",
|
||||
"scripts": {
|
||||
"dev": "wrangler dev src/index.ts",
|
||||
"deploy": "wrangler deploy --minify src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cf-wasm/resvg": "^0.1.21",
|
||||
"@cloudflare/workers-types": "^4.20240529.0",
|
||||
"@jsquash/avif": "^1.3.0",
|
||||
"@jsquash/jpeg": "^1.4.0",
|
||||
"@jsquash/resize": "^2.0.0",
|
||||
"@nestri/cache": "workspace:*",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"wrangler": "^3.72.2"
|
||||
}
|
||||
}
|
||||
38
apps/nexus/src/image/avatar.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Resvg } from "@cf-wasm/resvg";
|
||||
import { generateGradient, createAvatarSvg } from '../utils';
|
||||
import { kvCaches } from "@nestri/cache"
|
||||
|
||||
const cacheOptions = {
|
||||
key: "nexus",
|
||||
namespace: "avatar-cache"
|
||||
};
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
const middleware = kvCaches(cacheOptions);
|
||||
|
||||
app.get('/:id', middleware, async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const [name, fileType] = id.split('.');
|
||||
const size = parseInt(c.req.query("size") || "200");
|
||||
|
||||
const validImageTypes = ["png"] //['jpg', 'png', 'webp', 'avif'];
|
||||
if (!validImageTypes.includes(fileType)) {
|
||||
return c.text('Invalid image type', 400);
|
||||
}
|
||||
|
||||
const gradient = generateGradient(name || Math.random() + "");
|
||||
// console.log(`gradient: ${JSON.stringify(gradient)}`)
|
||||
const svg = createAvatarSvg(size, gradient, fileType);
|
||||
const resvg = await Resvg.create(svg.toString());
|
||||
const pngData = resvg.render()
|
||||
const pngBuffer = pngData.asPng()
|
||||
|
||||
return c.newResponse(pngBuffer, 200, {
|
||||
"Content-Type": `image/${fileType}`,
|
||||
'Cache-Control': 'public, max-age=31536000, immutable'
|
||||
})
|
||||
})
|
||||
|
||||
export default app
|
||||
72
apps/nexus/src/image/banner.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Hono } from 'hono'
|
||||
import { kvCaches } from "@nestri/cache"
|
||||
import resize, { initResize } from '@jsquash/resize';
|
||||
import decodeJpeg, { init as initDecodeJpeg } from '@jsquash/jpeg/decode';
|
||||
import encodeAvif, { init as initEncodeAvif } from '@jsquash/avif/encode.js';
|
||||
import JPEG_DEC_WASM from "@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm";
|
||||
import RESIZE_WASM from "@jsquash/resize/lib/resize/pkg/squoosh_resize_bg.wasm";
|
||||
import AVIF_ENC_WASM from "@jsquash/avif/codec/enc/avif_enc.wasm";
|
||||
|
||||
const cacheOptions = {
|
||||
key: "nexus",
|
||||
namespace: "cover-cache"
|
||||
};
|
||||
|
||||
const middleware = kvCaches(cacheOptions);
|
||||
|
||||
const decodeImage = async (buffer: ArrayBuffer) => {
|
||||
await initDecodeJpeg(JPEG_DEC_WASM);
|
||||
return decodeJpeg(buffer);
|
||||
}
|
||||
|
||||
const resizeImage = async (image: { width: number; height: number }, width: number, height: number) => {
|
||||
await initResize(RESIZE_WASM);
|
||||
// Resize image with respect to aspect ratio
|
||||
const aspectRatio = image.width / image.height;
|
||||
const newWidth = width;
|
||||
const newHeight = width / aspectRatio;
|
||||
return resize(image, { width: newWidth, height: newHeight, fitMethod: "stretch" });
|
||||
}
|
||||
|
||||
const encodeImage = async (image: { width: number; height: number }, format: string) => {
|
||||
if (format === 'avif') {
|
||||
await initEncodeAvif(AVIF_ENC_WASM);
|
||||
return encodeAvif(image);
|
||||
}
|
||||
throw new Error(`Unsupported image format: ${format}`);
|
||||
}
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.notFound((c) => c.json({ message: 'Not Found', ok: false }, 404))
|
||||
|
||||
app.get('/:id', middleware, async (c) => {
|
||||
const [gameId, imageType] = c.req.param("id").split('.');
|
||||
const width = parseInt(c.req.query("width") || "600");
|
||||
//We don't even use this, but let us keep it for future use
|
||||
const height = parseInt(c.req.query("height") || "900");
|
||||
if (!gameId || !imageType) {
|
||||
return c.text("Invalid image parameters", 400)
|
||||
}
|
||||
//Support Avif only because of it's small size
|
||||
const validImageTypes = ["avif"] //['jpg', 'png', 'webp', 'avif'];
|
||||
if (!validImageTypes.includes(imageType)) {
|
||||
return c.text('Invalid image type', 400);
|
||||
}
|
||||
|
||||
const imageUrl = `https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${gameId}/header.jpg`;
|
||||
const image = await fetch(imageUrl);
|
||||
if (!image.ok) {
|
||||
return c.text('Image not found', 404);
|
||||
}
|
||||
const imageBuffer = await image.arrayBuffer();
|
||||
const imageData = await decodeImage(imageBuffer);
|
||||
const resizedImage = await resizeImage(imageData, width, height);
|
||||
const resizedImageBuffer = await encodeImage(resizedImage, imageType);
|
||||
return c.newResponse(resizedImageBuffer, 200, {
|
||||
"Content-Type": `image/${imageType}`,
|
||||
'Cache-Control': 'public, max-age=31536000, immutable'
|
||||
})
|
||||
})
|
||||
|
||||
export default app
|
||||
72
apps/nexus/src/image/cover.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Hono } from 'hono'
|
||||
import { kvCaches } from "@nestri/cache"
|
||||
import resize, { initResize } from '@jsquash/resize';
|
||||
import decodeJpeg, { init as initDecodeJpeg } from '@jsquash/jpeg/decode';
|
||||
import encodeAvif, { init as initEncodeAvif } from '@jsquash/avif/encode.js';
|
||||
import JPEG_DEC_WASM from "@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm";
|
||||
import RESIZE_WASM from "@jsquash/resize/lib/resize/pkg/squoosh_resize_bg.wasm";
|
||||
import AVIF_ENC_WASM from "@jsquash/avif/codec/enc/avif_enc.wasm";
|
||||
|
||||
const cacheOptions = {
|
||||
key: "nexus",
|
||||
namespace: "cover-cache"
|
||||
};
|
||||
|
||||
const middleware = kvCaches(cacheOptions);
|
||||
|
||||
const decodeImage = async (buffer: ArrayBuffer) => {
|
||||
await initDecodeJpeg(JPEG_DEC_WASM);
|
||||
return decodeJpeg(buffer);
|
||||
}
|
||||
|
||||
const resizeImage = async (image: { width: number; height: number }, width: number, height: number) => {
|
||||
await initResize(RESIZE_WASM);
|
||||
// Resize image with respect to aspect ratio
|
||||
const aspectRatio = image.width / image.height;
|
||||
const newWidth = width;
|
||||
const newHeight = width / aspectRatio;
|
||||
return resize(image, { width: newWidth, height: newHeight, fitMethod: "stretch" });
|
||||
}
|
||||
|
||||
const encodeImage = async (image: { width: number; height: number }, format: string) => {
|
||||
if (format === 'avif') {
|
||||
await initEncodeAvif(AVIF_ENC_WASM);
|
||||
return encodeAvif(image);
|
||||
}
|
||||
throw new Error(`Unsupported image format: ${format}`);
|
||||
}
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.notFound((c) => c.json({ message: 'Not Found', ok: false }, 404))
|
||||
|
||||
app.get('/:id', middleware, async (c) => {
|
||||
const [gameId, imageType] = c.req.param("id").split('.');
|
||||
const width = parseInt(c.req.query("width") || "600");
|
||||
//We don't even use this, but let us keep it for future use
|
||||
const height = parseInt(c.req.query("height") || "900");
|
||||
if (!gameId || !imageType) {
|
||||
return c.text("Invalid image parameters", 400)
|
||||
}
|
||||
//Support Avif only because of it's small size
|
||||
const validImageTypes = ["avif"] //['jpg', 'png', 'webp', 'avif'];
|
||||
if (!validImageTypes.includes(imageType)) {
|
||||
return c.text('Invalid image type', 400);
|
||||
}
|
||||
|
||||
const imageUrl = `https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${gameId}/library_600x900_2x.jpg`;
|
||||
const image = await fetch(imageUrl);
|
||||
if (!image.ok) {
|
||||
return c.text('Image not found', 404);
|
||||
}
|
||||
const imageBuffer = await image.arrayBuffer();
|
||||
const imageData = await decodeImage(imageBuffer);
|
||||
const resizedImage = await resizeImage(imageData, width, height);
|
||||
const resizedImageBuffer = await encodeImage(resizedImage, imageType);
|
||||
return c.newResponse(resizedImageBuffer, 200, {
|
||||
"Content-Type": `image/${imageType}`,
|
||||
'Cache-Control': 'public, max-age=31536000, immutable'
|
||||
})
|
||||
})
|
||||
|
||||
export default app
|
||||
39
apps/nexus/src/image/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Hono } from 'hono'
|
||||
import { cors } from 'hono/cors';
|
||||
import Cover from './cover'
|
||||
import Avatar from './avatar'
|
||||
import Banner from './banner'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.get('/', (c) => {
|
||||
return c.text('Hello There! 👋🏾')
|
||||
})
|
||||
|
||||
app.notFound((c) => c.json({ message: 'Not Found', ok: false }, 404))
|
||||
|
||||
app.use(
|
||||
'/*',
|
||||
cors({
|
||||
origin: (origin, c) => {
|
||||
const allowedOriginPatterns = [
|
||||
/^https:\/\/.*\.nestri\.pages\.dev$/,
|
||||
/^https:\/\/.*\.nestri\.io$/,
|
||||
/^http:\/\/localhost:\d+$/ // For local development
|
||||
];
|
||||
|
||||
return allowedOriginPatterns.some(pattern => pattern.test(origin))
|
||||
? origin
|
||||
: 'https://nexus.nestri.io'
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
app.route('/cover', Cover)
|
||||
|
||||
app.route('/avatar', Avatar)
|
||||
|
||||
app.route("/banner", Banner)
|
||||
|
||||
export default app
|
||||
18
apps/nexus/src/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Hono } from 'hono'
|
||||
import Image from "./image"
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.get('/', (c) => {
|
||||
return c.text('Hello There! 👋🏾')
|
||||
})
|
||||
|
||||
app.get('/favicon.ico', (c) => {
|
||||
return c.newResponse(null, 302, {
|
||||
Location: 'https://nestri.pages.dev/favicon.svg'
|
||||
})
|
||||
})
|
||||
|
||||
app.route("/image", Image)
|
||||
|
||||
export default app
|
||||
4
apps/nexus/src/types/api.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '*wasm' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
33
apps/nexus/src/utils/create-avatar.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
export const createAvatarSvg = (size: number, gradient: { fromColor: string, toColor: string }, fileType?: string, text?: string) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stopColor={gradient.fromColor} />
|
||||
<stop offset="100%" stopColor={gradient.toColor} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect fill="url(#gradient)" x="0" y="0" width={size} height={size} />
|
||||
{fileType === "svg" && text && (
|
||||
<text
|
||||
x="50%"
|
||||
y="50%"
|
||||
alignmentBaseline="central"
|
||||
dominantBaseline="central"
|
||||
textAnchor="middle"
|
||||
fill="#fff"
|
||||
fontFamily="sans-serif"
|
||||
fontSize={(size * 0.9) / text.length}
|
||||
>
|
||||
{text}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
24
apps/nexus/src/utils/gradient.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import color from "tinycolor2";
|
||||
|
||||
function fnv1a(str: string) {
|
||||
let hash = 0x811c9dc5;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash ^= str.charCodeAt(i);
|
||||
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
||||
}
|
||||
return hash >>> 0;
|
||||
}
|
||||
|
||||
export function generateGradient(username: string) {
|
||||
const hash = fnv1a(username);
|
||||
const hue1 = hash % 360;
|
||||
const hue2 = (hash >> 16) % 360;
|
||||
|
||||
const c1 = color({ h: hue1, s: 0.8, l: 0.6 });
|
||||
const c2 = color({ h: hue2, s: 0.8, l: 0.5 });
|
||||
|
||||
return {
|
||||
fromColor: c1.toHexString(),
|
||||
toColor: c2.toHexString(),
|
||||
};
|
||||
}
|
||||
2
apps/nexus/src/utils/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './gradient'
|
||||
export * from './create-avatar'
|
||||
17
apps/nexus/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"ESNext"
|
||||
],
|
||||
"types": [
|
||||
"@cloudflare/workers-types/2023-07-01"
|
||||
],
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx"
|
||||
},
|
||||
}
|
||||
26
apps/nexus/wrangler.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
name = "nexus"
|
||||
compatibility_date = "2024-08-14"
|
||||
send_metrics = false
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "nexus"
|
||||
id = "e21527f9f1ed4adbaca1fa23d7f147c9"
|
||||
|
||||
# [vars]
|
||||
# MY_VAR = "my-variable"
|
||||
|
||||
# [[kv_namespaces]]
|
||||
# binding = "MY_KV_NAMESPACE"
|
||||
# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
# [[r2_buckets]]
|
||||
# binding = "MY_BUCKET"
|
||||
# bucket_name = "my-bucket"
|
||||
|
||||
# [[d1_databases]]
|
||||
# binding = "DB"
|
||||
# database_name = "my-database"
|
||||
# database_id = ""
|
||||
|
||||
# [ai]
|
||||
# binding = "AI"
|
||||
@@ -1,11 +0,0 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@nestri/eslint-config/qwik.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
//Find some way to use the lint tsconfig
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
};
|
||||
16
apps/www/.eslintrc.js
Normal file
@@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
"@nestri/eslint-config/qwik.js",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ["./tsconfig.json"],
|
||||
ecmaVersion: 2021,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestri/www",
|
||||
"description": "Website for Nestri",
|
||||
"name": "@nestri/web",
|
||||
"description": "Your games. Your rules.",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
@@ -31,16 +31,22 @@
|
||||
"devDependencies": {
|
||||
"@builder.io/qwik": "^1.8.0",
|
||||
"@builder.io/qwik-city": "^1.8.0",
|
||||
"@builder.io/qwik-react": "0.5.0",
|
||||
"@nestri/eslint-config": "workspace:*",
|
||||
"@nestri/typescript-config": "workspace:*",
|
||||
"@nestri/ui": "workspace:*",
|
||||
"@types/eslint": "8.56.10",
|
||||
"@types/node": "20.14.11",
|
||||
"@types/node": "^22.5.1",
|
||||
"@types/react": "^18.2.28",
|
||||
"@types/react-dom": "^18.2.13",
|
||||
"@typescript-eslint/eslint-plugin": "7.16.1",
|
||||
"@typescript-eslint/parser": "7.16.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-qwik": "^1.8.0",
|
||||
"framer-motion": "^11.3.24",
|
||||
"prettier": "3.3.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"typescript": "5.4.5",
|
||||
"undici": "*",
|
||||
"vite": "5.3.5",
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = require("@nestri/ui/postcss");
|
||||
module.exports = require("@nestri/ui/postcss.config");
|
||||
BIN
apps/www/public/changelog/v0.0.3/game-play.png
Normal file
|
After Width: | Height: | Size: 6.9 MiB |
BIN
apps/www/public/changelog/v0.0.3/gameplay-right.png
Normal file
|
After Width: | Height: | Size: 7.0 MiB |
BIN
apps/www/public/changelog/v0.0.3/gameplay.avifs
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
apps/www/public/changelog/v0.0.3/header.avif
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
apps/www/public/changelog/v0.0.3/header.png
Normal file
|
After Width: | Height: | Size: 4.5 MiB |
BIN
apps/www/public/changelog/v0.0.3/nestrivalheim.mp4
Normal file
BIN
apps/www/public/changelog/v0.0.3/new-site.png
Normal file
|
After Width: | Height: | Size: 5.5 MiB |
BIN
apps/www/public/changelog/v0.0.3/new-website-design.avif
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
apps/www/public/changelog/v0.0.3/new-website-design.png
Normal file
|
After Width: | Height: | Size: 5.3 MiB |
BIN
apps/www/public/changelog/v0.0.3/new-website.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
apps/www/public/changelog/v0.0.3/steam-integration.png
Normal file
|
After Width: | Height: | Size: 6.3 MiB |
BIN
apps/www/public/logo.webp
Normal file
|
After Width: | Height: | Size: 242 B |
@@ -26,7 +26,7 @@ export const RouterHead = component$(() => {
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/seo/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
{/**@ts-ignore */}
|
||||
<link rel="mask-icon" href="/seo/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<link rel="mask-icon" href="/seo/safari-pinned-tab.svg" color="#ffede5" />
|
||||
<link rel="shortcut icon" href="/seo/favicon.ico" />
|
||||
<meta name="msapplication-TileColor" content="#ffede5" />
|
||||
<meta name="msapplication-config" content="/seo/browserconfig.xml" />
|
||||
|
||||
@@ -8,6 +8,7 @@ import { RouterHead } from "@/components/router-head";
|
||||
import { isDev } from "@builder.io/qwik/build";
|
||||
|
||||
import "@nestri/ui/globals.css";
|
||||
import { Fonts } from "@nestri/ui";
|
||||
|
||||
export default component$(() => {
|
||||
/**
|
||||
@@ -18,21 +19,28 @@ export default component$(() => {
|
||||
*/
|
||||
|
||||
return (
|
||||
<QwikCityProvider>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
{!isDev && (
|
||||
<link
|
||||
rel="manifest"
|
||||
href={`${import.meta.env.BASE_URL}manifest.json`}
|
||||
/>
|
||||
)}
|
||||
<RouterHead />
|
||||
</head>
|
||||
<body lang="en">
|
||||
<RouterOutlet />
|
||||
{!isDev && <ServiceWorkerRegister />}
|
||||
</body>
|
||||
</QwikCityProvider>
|
||||
<Fonts>
|
||||
<QwikCityProvider>
|
||||
<head>
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fafafa" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0a0a0a" />
|
||||
<meta charset="utf-8" />
|
||||
{!isDev && (
|
||||
<link
|
||||
rel="manifest"
|
||||
href={`${import.meta.env.BASE_URL}manifest.json`}
|
||||
/>
|
||||
)}
|
||||
<RouterHead />
|
||||
</head>
|
||||
<body
|
||||
class="bg-gray-50 text-primary-950 dark:bg-gray-950 dark:text-primary-50 font-body flex min-h-[100dvh] flex-col overflow-x-hidden antialiased"
|
||||
lang="en">
|
||||
<RouterOutlet />
|
||||
{/* {!isDev && <ServiceWorkerRegister />} */}
|
||||
<ServiceWorkerRegister />
|
||||
</body>
|
||||
</QwikCityProvider>
|
||||
</Fonts>
|
||||
);
|
||||
});
|
||||
|
||||
185
apps/www/src/routes/(legal)/privacy/index.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
/* eslint-disable qwik/no-react-props */
|
||||
import { Title, Text} from "@nestri/ui/react";
|
||||
import { buttonVariants, cn } from "@nestri/ui/design";
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { Link } from "@builder.io/qwik-city";
|
||||
|
||||
export default component$(() => {
|
||||
|
||||
return (
|
||||
<div class="w-screen relative" >
|
||||
{/**Gradient to hide the ending of the checkered bg at the bottom*/}
|
||||
{/* <div class="absolute inset-0 dark:[background:radial-gradient(60.1852%_65%_at_50%_52%,rgba(255,255,255,0)_41.4414%,theme(colors.gray.950,0.7)_102%)] [background:radial-gradient(60.1852%_65%_at_50%_52%,rgba(255,255,255,0)_41.4414%,theme(colors.gray.50,0.7)_102%)] h-screen w-screen overflow-hidden max-w-[100vw] top-0 left-0 right-0 select-none" /> */}
|
||||
<nav class="w-full h-[70px] lg:flex hidden sticky top-0 z-50 py-4 justify-center items-center" >
|
||||
<div class="w-full left-1/2 relative -translate-x-[40%]">
|
||||
<Link href="/" class={cn(buttonVariants.outlined({ intent: "neutral", size: "md" }), "w-max")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-[20px] -rotate-90" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"><path stroke-linejoin="round" d="m17 9.5l-5-5l-5 5" /><path d="M12 4.5v10c0 1.667-1 5-5 5" opacity=".5" /></g></svg>
|
||||
{/* <svg xmlns="http://www.w3.org/2000/svg" class="size-[20px]" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10" opacity=".5" /><path stroke-linecap="round" stroke-linejoin="round" d="m15.5 9l-3 3l3 3m-4-6l-3 3l3 3" /></g></svg> */}
|
||||
Go Back
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
<section class="px-4 relative lg:-top-[70px]" >
|
||||
|
||||
<div class="mx-auto select-text max-w-xl py-8 [&_h1]:text-3xl flex relative gap-4 w-full flex-col" >
|
||||
<Title className="py-4 text-4xl" >
|
||||
Nestri's Privacy Policy
|
||||
</Title>
|
||||
|
||||
<Text className="py-2 dark:text-primary-50/80 text-primary-950/80" >
|
||||
<strong>Last updated on: </strong>
|
||||
1st July 2024
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
Welcome to Nestri. Thank you for using our service. We value you and we know privacy is important to you. It's important to us, too.
|
||||
<br />
|
||||
<br />
|
||||
This Privacy Policy describes how we collect, use, disclose, and protect the personal data we collect through our website, products, services, and applications that link to this Privacy Policy (collectively, the "Services").
|
||||
|
||||
</Text>
|
||||
|
||||
<Title>Information We Collect</Title>
|
||||
|
||||
<Text>We may collect personal information directly from you or automatically when you use our Services. The types of personal information we may collect include:
|
||||
<br />
|
||||
|
||||
<ul class="list-disc mx-8 list-item" >
|
||||
<li>
|
||||
<strong>Contact Information:</strong>
|
||||
such as name and email address.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Account Credentials:</strong>
|
||||
such as usernames and passwords.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Limited Payment Processing Information:</strong>
|
||||
such as whether the transaction happened, its status, type and amount, as well as what payment scheme or operator you’ve used.
|
||||
We <strong>DO NOT</strong> collect information about your bank card number, bank account number, cardholder’s name.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Usage Information:</strong>
|
||||
such as your IP address, browser type, operating system, and device information.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Cookies and Similar Technologies:</strong>
|
||||
to collect information about your interactions with our Services.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Additional Information: </strong>
|
||||
such as age, gender, games played, activity across our products, games "installed," crash reports, technical data about your device (including internet speed, IP, location, mobile type, hardware details), and games on Steam (from Steam).
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</Text>
|
||||
|
||||
<Title>How We Use Your Information</Title>
|
||||
|
||||
<Text>We may use the personal information we collect for the following purposes:
|
||||
<br />
|
||||
|
||||
<ul class="list-disc mx-8 list-item" >
|
||||
<li>
|
||||
<strong>To Provide and Maintain Our Services:</strong>
|
||||
ensuring they function correctly and securely.
|
||||
</li>
|
||||
<li>
|
||||
<strong>To Process and Fulfill Your Requests:</strong>
|
||||
such as subscription management and customer support.
|
||||
</li>
|
||||
<li>
|
||||
<strong>To Communicate with You:</strong>
|
||||
including responding to your inquiries and providing customer support.
|
||||
</li>
|
||||
<li>
|
||||
<strong>To Personalize Your Experience and Improve Our Services:</strong>
|
||||
based on your usage and preferences.
|
||||
</li>
|
||||
<li>
|
||||
<strong>To Send You Marketing Communications and Promotional Offers:</strong>
|
||||
if you have opted in to receive them.
|
||||
</li>
|
||||
<li>
|
||||
<strong>To Detect, Prevent, and Investigate Piracy and Other Illegal Activities:</strong>
|
||||
ensuring the integrity of our platform.
|
||||
</li>
|
||||
<li>
|
||||
<strong>To Comply with Legal Obligations and Enforce Our Terms and Policies:</strong>
|
||||
including our Terms of Service.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</Text>
|
||||
|
||||
<Title>Data Sharing and Disclosure</Title>
|
||||
|
||||
<Text>We may share your personal information with third parties in the following circumstances:
|
||||
<br />
|
||||
|
||||
<ul class="list-disc mx-8 list-item" >
|
||||
<li>
|
||||
<strong>With Our Affiliates and Subsidiaries:</strong>
|
||||
for the purposes described in this Privacy Policy.
|
||||
</li>
|
||||
<li>
|
||||
<strong>With Third Parties for Marketing Purposes:</strong>
|
||||
if you have consented to such sharing.
|
||||
</li>
|
||||
<li>
|
||||
<strong>In Connection with a Merger, Acquisition, or Sale:</strong>
|
||||
of all or a portion of our assets.
|
||||
</li>
|
||||
<li>
|
||||
<strong>With Service Providers:</strong>
|
||||
who assist us in operating our business and providing our Services.
|
||||
</li>
|
||||
</ul>
|
||||
</Text>
|
||||
|
||||
<Title>Your Rights and Choices</Title>
|
||||
|
||||
<Text>
|
||||
You have certain rights regarding your personal information, including the right to access, correct, or delete your information. You may also have the right to object to certain processing activities and to withdraw your consent where applicable.
|
||||
</Text>
|
||||
|
||||
<Title>Data Transfers</Title>
|
||||
|
||||
<Text>If we transfer your personal information outside of the European Economic Area (EEA), we will ensure that adequate safeguards are in place to protect your information, such as standard contractual clauses approved by the European Commission.</Text>
|
||||
|
||||
<Title>Security Measures</Title>
|
||||
|
||||
<Text>We implement appropriate technical and organizational measures to protect your personal information against unauthorized access, disclosure, alteration, or destruction.</Text>
|
||||
|
||||
<Title>Children's Privacy</Title>
|
||||
|
||||
<Text>Our Services are not directed to children under the age of 13, and we do not knowingly collect personal information from children under this age. If you are a parent or guardian and believe that your child has provided us with personal information, please contact us, and we will take steps to delete such information. Children merit specific protection with regard to their personal data. If we decide to knowingly collect personal data from a child under 13, we will ask for consent from the holder of parental responsibility over the child.</Text>
|
||||
|
||||
<Title>Changes to This Privacy Policy</Title>
|
||||
|
||||
<Text>We may update this Privacy Policy from time to time to reflect changes in our practices or applicable law. We will notify you of any material changes by posting the updated Privacy Policy on our website.</Text>
|
||||
|
||||
<Title>Contact Us</Title>
|
||||
|
||||
<Text>
|
||||
If you have any questions or concerns about our Privacy Policy or our data practices, you may contact us at
|
||||
<Link
|
||||
href="mailto:support@nestri.io"
|
||||
class={buttonVariants.link()}>
|
||||
support@nestri.io
|
||||
</Link>.
|
||||
</Text>
|
||||
|
||||
<Title>Legal Basis for Processing (for Users in the EEA)</Title>
|
||||
|
||||
<Text>If you are located in the European Economic Area (EEA), our legal basis for collecting and using the personal information described in this Privacy Policy will depend on the personal information concerned and the specific context in which we collect it. We will only process your personal information if we have a valid legal basis for doing so under applicable data protection law.</Text>
|
||||
|
||||
<Text align="center" className="pt-3">
|
||||
💖 Thank you for trusting Nestri with your data and gaming experience.💖
|
||||
<br />
|
||||
We are committed to safeguarding your personal information and ensuring your privacy. </Text>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
184
apps/www/src/routes/(legal)/terms/index.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
/* eslint-disable qwik/no-react-props */
|
||||
import { Title, Text } from "@nestri/ui/react";
|
||||
import { buttonVariants, cn } from "@nestri/ui/design";
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { Link } from "@builder.io/qwik-city";
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
<div class="w-screen relative" >
|
||||
{/**Gradient to hide the ending of the checkered bg at the bottom*/}
|
||||
{/* <div class="absolute inset-0 dark:[background:radial-gradient(60.1852%_65%_at_50%_52%,rgba(255,255,255,0)_41.4414%,theme(colors.gray.950,0.7)_102%)] [background:radial-gradient(60.1852%_65%_at_50%_52%,rgba(255,255,255,0)_41.4414%,theme(colors.gray.50,0.7)_102%)] h-screen w-screen overflow-hidden max-w-[100vw] top-0 left-0 right-0 select-none" /> */}
|
||||
<nav class="w-full h-[70px] lg:flex hidden sticky top-0 z-50 py-4 justify-center items-center" >
|
||||
<div class="w-full left-1/2 relative -translate-x-[40%]">
|
||||
<Link href="/" class={cn(buttonVariants.outlined({ intent: "neutral", size: "md" }), "w-max")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-[20px] -rotate-90" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"><path stroke-linejoin="round" d="m17 9.5l-5-5l-5 5" /><path d="M12 4.5v10c0 1.667-1 5-5 5" opacity=".5" /></g></svg>
|
||||
{/* <svg xmlns="http://www.w3.org/2000/svg" class="size-[20px]" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10" opacity=".5" /><path stroke-linecap="round" stroke-linejoin="round" d="m15.5 9l-3 3l3 3m-4-6l-3 3l3 3" /></g></svg> */}
|
||||
Go Back
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
<section class="px-4 relative lg:-top-[70px]" >
|
||||
<div class="mx-auto select-text max-w-xl overflow-x-hidden py-8 [&_h1]:text-3xl flex relative gap-4 w-full flex-col" >
|
||||
<Title className="py-4 text-4xl" >
|
||||
Nestri's Terms of Service
|
||||
</Title>
|
||||
|
||||
<Text className="py-2 dark:text-primary-50/80 text-primary-950/80" >
|
||||
<strong>Last updated on: </strong>
|
||||
1st July 2024
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
Welcome to Nestri and thank you for using our services. We are an innovative cloud gaming platform that offers both self-hosted and hosted versions for gamers without GPUs.
|
||||
<br />
|
||||
<br />
|
||||
By using Nestri, you agree to these Terms of Service ("Terms"). If you have any questions, feel free to contact us.
|
||||
</Text>
|
||||
|
||||
<Title>Who We Are</Title>
|
||||
|
||||
<Text>Nestri is an open-source cloud gaming platform that lets you play games on your own terms — invite friends to join your gaming sessions, share your game library, and take even more control by hosting your own gaming server. Our hosted version is perfect for those who need GPU support, providing seamless gaming experiences for everyone.</Text>
|
||||
|
||||
<Title>Privacy and Security</Title>
|
||||
|
||||
<Text>We take your privacy and security very seriously. We adhere to stringent data protection laws and ensure that all data, including games downloaded from Steam on behalf of the user, is encrypted. We also collect and log IP addresses to avoid abuse and ensure the security of our services. For more details, please review our
|
||||
<Link
|
||||
href="/privacy"
|
||||
class={buttonVariants.link()}>
|
||||
Privacy Policy</Link>.
|
||||
</Text>
|
||||
|
||||
<Title>Acceptance of Terms</Title>
|
||||
|
||||
<Text>By using Nestri, you agree to be bound by these Terms. If you're using Nestri on behalf of an organization, you agree to these Terms on behalf of that organization.</Text>
|
||||
|
||||
<Title>Your Games</Title>
|
||||
|
||||
<Text>You can run the games you own on Nestri. Please check the list of video games available on Nestri before purchasing a subscription. Sorry, but we will not provide a refund if a game you want to play is not available.
|
||||
<br />
|
||||
<br />
|
||||
<Link
|
||||
href="/"
|
||||
class={buttonVariants.link()}>
|
||||
Check the list here</Link>.
|
||||
</Text>
|
||||
|
||||
<Title>Game Ownership and Piracy</Title>
|
||||
|
||||
<Text>Nestri strictly prohibits the use of our platform for piracy. We are not liable if you are caught pirating using any of our products. Only run games that you legally own.</Text>
|
||||
|
||||
<Title>Family Sharing</Title>
|
||||
|
||||
<Text>Nestri allows family sharing by enabling your friends to access your Steam account from their Nestri account. Please take care of who you share your Nestri membership with. Note that no two people can play a game simultaneously as per Steam's requirements.</Text>
|
||||
|
||||
<Title>Cloud Saves</Title>
|
||||
|
||||
<Text>We use a custom cloud save provider to ensure your game progress is preserved for all the games you play. If you exit a game properly or close the stream for more than 15 minutes, we will automatically save your progress. However, this is limiting, and you may need to contact us to retrieve your game progress, if you plan on migrating to another service.</Text>
|
||||
|
||||
<Title>Your Responsibilities</Title>
|
||||
|
||||
<Text>Your use of our services must comply with these Terms and applicable Nestri policies, as well as applicable laws. You are solely responsible for the development, content, and use of the games you play on Nestri and assume all risks associated with them, including intellectual property or other legal claims.</Text>
|
||||
|
||||
<Title>Prohibited Activities</Title>
|
||||
|
||||
<Text>To maintain a great service, we require you to comply with certain limitations.
|
||||
<br />
|
||||
You may not:
|
||||
<br />
|
||||
|
||||
<ul class="list-disc mx-8 list-item" >
|
||||
<li>Violate any laws, regulations, ordinances, or directives.</li>
|
||||
<li>Engage in any threatening, abusive, harassing, defamatory, or tortious conduct.</li>
|
||||
<li>Harass or abuse Nestri personnel or representatives.</li>
|
||||
<li>Use our services to support malware, phishing, spam, pirating, or similar activities.</li>
|
||||
<li>Interfere with the proper functioning of our services.</li>
|
||||
<li>Engage in any conduct that inhibits anyone else's use or enjoyment of our services.</li>
|
||||
<li>Circumvent storage space limits or pricing.</li>
|
||||
<li>Use the Nestri system inconsistently with its intended purpose.</li>
|
||||
<li>Upload, transmit, or distribute any computer viruses, worms, or any software intended to damage or alter a computer system or data.</li>
|
||||
<li>Send advertising, promotional materials, junk mail, spam, chain letters, pyramid schemes, or any other form of duplicative or unsolicited messages.</li>
|
||||
<li>Harvest, collect, or assemble information or data regarding other users without their consent.</li>
|
||||
<li>Attempt to gain unauthorized access to Nestri or other computer systems or networks connected to or used together with Nestri.</li>
|
||||
<li>Use automated scripts to produce multiple accounts or to generate automated searches, requests, or queries.</li>
|
||||
</ul>
|
||||
|
||||
</Text>
|
||||
|
||||
<Title>Our Rights</Title>
|
||||
|
||||
<Text>We reserve the right to change, eliminate, or restrict access to our services at any time. We may modify, suspend, or terminate a user account if you stop paying for our service or violate any of our Terms or policies. Nestri is not liable for any damages resulting from these actions.</Text>
|
||||
|
||||
<Title>Beta Services</Title>
|
||||
|
||||
<Text>We may release products and features still in testing and evaluation (“Beta Services”). Beta Services are confidential until official launch. By using Beta Services, you agree not to disclose any information about them without our permission.</Text>
|
||||
|
||||
<Title>Other Sites and Services</Title>
|
||||
|
||||
<Text>Nestri may contain links to websites, services, and advertisements that we neither own nor control. We do not endorse or assume responsibility for any third-party sites, information, materials, products, or services.</Text>
|
||||
|
||||
<Title>Children</Title>
|
||||
|
||||
<Text>Nestri is only for users 13 years old and older. If we become aware that a child under 13 has created an account, we will terminate that account.</Text>
|
||||
|
||||
<Title>Account Creation and Access</Title>
|
||||
|
||||
<Text>When you create a Nestri account, you will be required to set your account credentials, including an email address and a password. You are responsible for maintaining and safeguarding your account credentials. Nestri is under no obligation to provide you access to your account or your files if you are unable to provide the appropriate account credentials.</Text>
|
||||
|
||||
<Title>Service Cancellation and Account Deletion</Title>
|
||||
|
||||
<Text>You can cancel your Nestri service and delete your account at any time by signing in and deleting all your games and game progress stored on our system. If assistance is needed, please contact us. When you cancel your service, we will no longer bill you, except for past due amounts. Your canceled account information will remain accessible unless you delete your account.</Text>
|
||||
|
||||
<Title>Disclaimers</Title>
|
||||
|
||||
<Text>Nestri is provided "as is" without any warranties, express or implied. Except where otherwise prohibited by law, Nestri disclaims all warranties and conditions of merchantability, fitness for a particular purpose, and non-infringement.</Text>
|
||||
|
||||
<Title>Limitation of Liability</Title>
|
||||
|
||||
<Text>To the fullest extent allowed by law, Nestri shall not be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, or any loss of data, use, goodwill, or other intangible losses resulting from (A) your access to, use of, inability to access, or inability to use Nestri; (B) any third-party conduct or content on Nestri; or (C) any unauthorized access, use, or alteration of your content.</Text>
|
||||
|
||||
<Title>Arbitration and Opt-Out</Title>
|
||||
|
||||
<Text>We aim to resolve disputes fairly and quickly. If you have any issues with Nestri, please contact us, and we'll work with you in good faith to resolve the matter. If we can't solve the dispute informally, you and Nestri agree to resolve any claim through final and binding arbitration. You may opt out of the arbitration agreement by notifying us within 90 days of agreeing to these Terms.</Text>
|
||||
|
||||
<Title>Class Action and Trial Waiver</Title>
|
||||
|
||||
<Text>You and Nestri agree that each party may bring disputes against the other only in an individual capacity and not on behalf of any class of people. You and Nestri agree to waive the right to a trial by jury for all disputes.</Text>
|
||||
|
||||
<Title>Indemnification</Title>
|
||||
|
||||
<Text>You agree to indemnify, defend, and hold harmless Nestri from and against all liabilities, damages, and costs arising out of any claim by a third party against Nestri regarding (a) games and game progress stored with us by you, (b) your domains, or (c) your use of Nestri in violation of these terms.</Text>
|
||||
|
||||
<Title>Governing Law and Jurisdiction</Title>
|
||||
|
||||
<Text>These Terms will be governed by the laws applicable in your jurisdiction. For claims not subject to arbitration, we each agree to submit to the personal jurisdiction of the courts located in your jurisdiction.</Text>
|
||||
|
||||
<Title>Entire Agreement</Title>
|
||||
|
||||
<Text>These Terms and our Privacy Policy constitute the entire agreement between you and Nestri. If any provision of these Terms is found to be unenforceable, the remaining provisions will remain in full force and effect.</Text>
|
||||
|
||||
<Title>No Waiver</Title>
|
||||
|
||||
<Text>No waiver of any provision of these Terms shall be a further or continuing waiver of that term. Nestri's failure to assert any right or provision under these Terms does not constitute a waiver of that right or provision.</Text>
|
||||
|
||||
<Title>Modification</Title>
|
||||
|
||||
<Text>These Terms may be modified from time to time. The date of the most recent revisions will always be available on our website. If we make changes that we believe will substantially alter your rights, we will notify you. If you continue to use Nestri after modifications of the Terms, you agree to accept such modifications.</Text>
|
||||
|
||||
<Title>Contact</Title>
|
||||
|
||||
<Text>We welcome all questions, concerns, and feedback you might have about these terms. If you have suggestions for us, let us know at
|
||||
<Link
|
||||
href="mailto:support@nestri.io"
|
||||
class={buttonVariants.link()}>
|
||||
support@nestri.io
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
|
||||
<Text className="pt-3">💖 Thank you for choosing Nestri for your cloud gaming needs! 💖</Text>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
89
apps/www/src/routes/changelog/index.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { Link } from "@builder.io/qwik-city";
|
||||
import { NavBar, Footer } from "@nestri/ui";
|
||||
import { MotionComponent, transition, TitleSection } from "@nestri/ui/react";
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
<>
|
||||
<NavBar />
|
||||
<TitleSection client:load title="Changelog" description="All the latest updates, improvements, and fixes to Nestri." />
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={transition}
|
||||
client:load
|
||||
class="flex items-center justify-center w-full"
|
||||
as="div"
|
||||
>
|
||||
<div class="px-2" >
|
||||
<section class="flex flex-col gap-4 overflow-hidden mx-auto w-full text-left max-w-xl pt-20 pb-4">
|
||||
<div class="w-full justify-between flex">
|
||||
<h2 class="relative pl-8 font-medium font-title text-base before:absolute before:left-0 before:top-1 before:w-4 before:h-4 before:bg-primary-500 before:rounded-full after:absolute after:left-0.5 after:top-1.5 after:w-3 after:h-3 after:bg-gray-50 after:dark:bg-gray-950 after:rounded-full">
|
||||
v0.0.3
|
||||
</h2>
|
||||
<p class="text-sm pr-2.5">August 2024</p>
|
||||
</div>
|
||||
<div class="pt-2 pb-4 pr-2 pl-4 md:pl-8 h-max gap-4 flex flex-col relative before:absolute before:bottom-2 before:top-0 before:left-[7px] before:w-0.5 before:bg-gradient-to-b before:rounded-[2px] before:from-primary-500 before:to-transparent" >
|
||||
<div class="flex flex-row flex-wrap gap-2.5">
|
||||
<div class="aspect-auto h-max rounded-2xl overflow-hidden shadow-sm flex relative basis-full after:absolute after:inset-0 after:z-[3] bg-gray-200/70 select-none border-gray-300/70 dark:border-gray-700/70 p-5 border dark:bg-gray-800/70 dark:text-primary-50/70 text-primary-950/70">
|
||||
<div class="text-base backdrop-blur-sm font-title font-medium text-white z-[2] w-full h-full text-center p-4 justify-center items-center flex flex-col">
|
||||
<p class="text-2xl">Fresh new look, Intel & AMD GPU support and we finally launched 🥳</p>
|
||||
</div>
|
||||
<div class="absolute inset-0 z-0">
|
||||
<img draggable={false} src="/changelog/v0.0.3/header.avif" alt="Nestri Logo" height={328} width={328} class="w-full h-full object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="aspect-square h-max rounded-2xl overflow-hidden shadow-sm flex relative basis-full after:absolute after:inset-0 after:z-[3] bg-gray-200/70 select-none border-gray-300/70 dark:border-gray-700/70 p-5 border dark:bg-gray-800/70 dark:text-primary-50/70 text-primary-950/70">
|
||||
<div class="text-lg font-title font-medium text-white z-[2] w-full h-full text-center p-4 justify-end flex flex-col">
|
||||
<p class="m-4 backdrop-blur-sm" >Fresh new logo and branding 💅🏾</p>
|
||||
</div>
|
||||
<div class="absolute inset-0 z-0">
|
||||
<img draggable={false} src="/changelog/v0.0.3/new-website-design.avif" alt="Nestri Logo" height={328} width={328} class="w-full h-full object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-max aspect-auto rounded-2xl overflow-hidden shadow-sm flex relative basis-full after:absolute after:pointer-events-none after:inset-0 after:z-[3] bg-gray-200/70 select-none border-gray-300/70 dark:border-gray-700/70 p-5 border dark:bg-gray-800/70 dark:text-primary-50/70 text-primary-950/70">
|
||||
<div class="justify-center items-center flex w-full">
|
||||
<p class="text-xl font-title text-center font-medium">Updated our <Link class="underline" href="/terms">Terms of Service</Link> <br class="hidden md:block" /> and our <Link class="underline" href="/privacy">Privacy Policy</Link></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-max md:aspect-square aspect-auto rounded-2xl overflow-hidden shadow-sm flex relative sm:basis-[calc(50%-5px)] basis-full after:absolute after:pointer-events-none after:inset-0 after:z-[3] bg-gray-200/70 select-none border-gray-300/70 dark:border-gray-700/70 p-5 border dark:bg-gray-800/70 dark:text-primary-50/70 text-primary-950/70">
|
||||
<div class="justify-center items-center flex w-full">
|
||||
<p class="text-xl font-title text-center font-medium">Added support for Intel & AMD GPUs. Courtesy of{" "}<Link class="underline" href="https://github.com/DatCaptainHorse">@DatHorse</Link></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-max aspect-square rounded-2xl overflow-hidden shadow-sm flex relative sm:basis-[calc(50%-5px)] basis-full after:absolute after:pointer-events-none after:inset-0 after:z-[3] bg-gray-200/70 select-none border-gray-300/70 dark:border-gray-700/70 p-5 border dark:bg-gray-800/70 dark:text-primary-50/70 text-primary-950/70">
|
||||
<div class="absolute inset-0 z-0">
|
||||
<img draggable={false} src="/changelog/v0.0.3/gameplay.avifs" alt="Nestri Logo" height={328} width={328} class="w-full h-full object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-max aspect-auto rounded-2xl overflow-hidden shadow-sm flex relative basis-full after:absolute after:inset-0 after:z-[3] bg-gray-200/70 select-none border-gray-300/70 dark:border-gray-700/70 p-5 border dark:bg-gray-800/70 dark:text-primary-50/70 text-primary-950/70">
|
||||
<div class="justify-center items-center flex w-full">
|
||||
<p class="text-lg font-title text-center font-medium">+ Lots of quality of life improvements! 🤞🏽</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section >
|
||||
<section class="flex flex-col gap-4 overflow-hidden mx-auto w-full text-left max-w-xl pt-2 pb-4">
|
||||
<div class="w-full justify-between flex">
|
||||
<h2 class="relative pl-8 font-medium font-title text-base before:absolute before:left-0 before:top-1 before:w-4 before:h-4 before:bg-primary-500 before:rounded-full after:absolute after:left-0.5 after:top-1.5 after:w-3 after:h-3 after:bg-gray-50 after:dark:bg-gray-950 after:rounded-full">
|
||||
v0.0.2
|
||||
</h2>
|
||||
<p class="text-sm pr-2.5">June 2024</p>
|
||||
</div>
|
||||
<div class="pt-2 pb-4 pr-2 pl-4 md:pl-8 h-max relative before:absolute before:bottom-2 before:top-0 before:left-[7px] before:w-0.5 before:bg-gradient-to-b before:rounded-[2px] before:from-primary-500 before:to-transparent" >
|
||||
<div class="flex flex-row flex-wrap gap-2.5">
|
||||
<div class="h-max justify-center aspect-auto rounded-2xl overflow-hidden shadow-sm flex relative basis-full after:absolute after:inset-0 after:z-[3] bg-gray-200/70 select-none border-gray-300/70 dark:border-gray-700/70 p-5 border dark:bg-gray-800/70 dark:text-primary-50/70 text-primary-950/70">
|
||||
<p class="text-lg font-title text-center font-medium">Nestri has been long overdue for a changelog. <br class="hidden md:block" /> Welcome to our changelog!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div >
|
||||
</MotionComponent>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
})
|
||||
130
apps/www/src/routes/contact/index.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { component$ } from "@builder.io/qwik"
|
||||
import { NavBar, Footer } from "@nestri/ui";
|
||||
import { buttonVariants, cn } from "@nestri/ui/design";
|
||||
import { MotionComponent, transition, TitleSection } from "@nestri/ui/react";
|
||||
|
||||
|
||||
const feedback = [
|
||||
{
|
||||
rating: 5,
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M15.9989 29.9978C25.3333 29.9978 29.9978 23.7303 29.9978 15.9989C29.9978 8.26751 25.3333 2 15.9989 2C6.66443 2 2 8.26751 2 15.9989C2 23.7303 6.66443 29.9978 15.9989 29.9978Z" fill="#FFB02E" />
|
||||
<path d="M16 25C7 25 7 16 7 16H25C25 16 25 25 16 25Z" fill="#BB1D80" />
|
||||
<path d="M8 16.5V16H24V16.5C24 17.0523 23.5523 17.5 23 17.5H9C8.44772 17.5 8 17.0523 8 16.5Z" fill="white" />
|
||||
<path d="M3.18104 9.75037C5.19703 12.0771 7.8791 13.096 9.25386 13.4894C9.81699 13.6506 10.4079 13.4889 10.8249 13.0776C12.0184 11.9005 14.4238 9.19933 14.938 6.11531C15.656 1.80872 10.256 0.495856 8.07985 4.04542C2.98933 1.65437 0.296489 6.42127 3.18104 9.75037Z" fill="#F70A8D" />
|
||||
<path d="M28.8172 9.75198C26.8022 12.0775 24.1215 13.0961 22.7473 13.4894C22.1841 13.6506 21.5932 13.4889 21.1762 13.0776C19.9831 11.9008 17.579 9.20094 17.0651 6.11839C16.3474 1.81356 21.7452 0.50123 23.9204 4.04935C29.0089 1.65928 31.7006 6.42423 28.8172 9.75198Z" fill="#F70A8D" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
rating: 4,
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M15.9989 29.9978C25.3333 29.9978 29.9978 23.7303 29.9978 15.9989C29.9978 8.26751 25.3333 2 15.9989 2C6.66443 2 2 8.26751 2 15.9989C2 23.7303 6.66443 29.9978 15.9989 29.9978Z" fill="#FFB02E" />
|
||||
<path d="M11.8395 18.4566C11.5433 17.9987 10.9333 17.8621 10.4695 18.1523C10.0013 18.4453 9.85933 19.0624 10.1523 19.5305L10.1531 19.5317L10.1538 19.5328L10.1554 19.5354L10.1589 19.5409L10.1676 19.5543C10.1741 19.5642 10.1819 19.5759 10.1913 19.5895C10.2099 19.6165 10.2343 19.6507 10.2648 19.6909C10.3259 19.7713 10.4117 19.8761 10.5249 19.9967C10.7514 20.2382 11.0879 20.5432 11.5554 20.8423C12.4995 21.4464 13.9369 22 16 22C18.0632 22 19.5005 21.4464 20.4446 20.8423C20.9121 20.5432 21.2486 20.2382 21.4752 19.9967C21.5883 19.8761 21.6741 19.7713 21.7352 19.6909C21.7657 19.6507 21.7902 19.6165 21.8088 19.5895C21.8181 19.5759 21.826 19.5642 21.8324 19.5543L21.8411 19.5409L21.8447 19.5354L21.8462 19.5328L21.847 19.5317L21.8477 19.5305C22.1409 19.0625 21.9987 18.4453 21.5305 18.1523C21.0667 17.8621 20.4567 17.9987 20.1606 18.4566C20.1582 18.4599 20.1523 18.4683 20.1426 18.481C20.1206 18.51 20.0793 18.5614 20.0166 18.6283C19.8913 18.7619 19.6806 18.9568 19.3667 19.1577C18.7478 19.5536 17.6852 20 16 20C14.3148 20 13.2522 19.5536 12.6333 19.1577C12.3194 18.9568 12.1088 18.7619 11.9834 18.6283C11.9207 18.5614 11.8794 18.51 11.8574 18.481C11.8477 18.4683 11.8418 18.4599 11.8395 18.4566Z" fill="#402A32" />
|
||||
<path d="M11 5H5C2.79086 5 1 6.79086 1 9V9.67376C1 12.3252 2.49802 14.749 4.8695 15.9348L5.80534 16.4027C7.20729 17.1036 8.84913 17.1967 10.3252 16.6696C13.1112 15.6746 15 13.0253 15 10.067V9C15 6.79086 13.2091 5 11 5Z" fill="#321B41" />
|
||||
<path d="M20.5 5H27.5C29.433 5 31 6.567 31 8.5V9.67376C31 12.3252 29.502 14.749 27.1305 15.9348L26.1947 16.4027C24.7927 17.1036 23.1509 17.1967 21.6748 16.6696C18.8888 15.6746 17 13.0253 17 10.067V8.5C17 6.567 18.567 5 20.5 5Z" fill="#321B41" />
|
||||
<path d="M14 6.35418C13.2671 5.52375 12.1947 5 11 5H5C2.79086 5 1 6.79086 1 9V9.67376C1 12.3252 2.49802 14.749 4.8695 15.9348L5.80534 16.4027C7.20729 17.1036 8.84912 17.1967 10.3252 16.6696C13.1112 15.6746 15 13.0253 15 10.067V10C15 9.44772 15.4477 9 16 9C16.5523 9 17 9.44772 17 10V10.067C17 13.0253 18.8888 15.6746 21.6748 16.6696C23.1509 17.1967 24.7927 17.1036 26.1947 16.4027L27.1305 15.9348C29.502 14.749 31 12.3252 31 9.67376V8.5C31 6.567 29.433 5 27.5 5H20.5C19.3759 5 18.3756 5.52992 17.7352 6.35357V6.35077C17.7206 6.36537 17.704 6.3824 17.6855 6.40132C17.4936 6.59759 17.1031 6.9969 16.7395 7H14.9957C14.6321 6.9969 14.2418 6.59771 14.0499 6.40145C14.0314 6.38253 14.0146 6.36538 14 6.35077V6.35418ZM11 6C12.6569 6 14 7.34315 14 9V10.067C14 12.5975 12.3817 14.8732 9.9889 15.7278C8.76683 16.1643 7.40816 16.086 6.25255 15.5082L5.31672 15.0403C3.28401 14.024 2 11.9464 2 9.67376V9C2 7.34315 3.34315 6 5 6H11ZM18 8.5C18 7.11929 19.1193 6 20.5 6H27.5C28.8807 6 30 7.11929 30 8.5V9.67376C30 11.9464 28.716 14.024 26.6833 15.0403L25.7474 15.5082C24.5918 16.086 23.2332 16.1643 22.0111 15.7278C19.6183 14.8732 18 12.5975 18 10.067V8.5Z" fill="#8D65C5" />
|
||||
<path d="M12.3891 10.2678C12.9749 9.68199 12.8166 8.57395 12.0356 7.7929C11.2545 7.01186 10.1465 6.85356 9.56069 7.43935C8.9749 8.02514 9.13319 9.13318 9.91424 9.91422C10.6953 10.6953 11.8033 10.8536 12.3891 10.2678Z" fill="white" />
|
||||
<path d="M28.3891 10.2678C28.9749 9.68199 28.8166 8.57395 28.0356 7.7929C27.2545 7.01186 26.1465 6.85356 25.5607 7.43935C24.9749 8.02514 25.1332 9.13318 25.9142 9.91422C26.6953 10.6953 27.8033 10.8536 28.3891 10.2678Z" fill="white" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
rating: 3,
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M15.9989 29.9978C25.3333 29.9978 29.9978 23.7303 29.9978 15.9989C29.9978 8.26751 25.3333 2 15.9989 2C6.66443 2 2 8.26751 2 15.9989C2 23.7303 6.66443 29.9978 15.9989 29.9978Z" fill="#FFB02E" />
|
||||
<path d="M6 13.5C6 13.331 6.00932 13.1642 6.02746 13H10.0313L12.332 13.9227L14.4639 13H14.9725C14.9907 13.1642 15 13.331 15 13.5C15 15.9853 12.9853 18 10.5 18C8.01472 18 6 15.9853 6 13.5Z" fill="white" />
|
||||
<path d="M17 13.5C17 13.331 17.0093 13.1642 17.0275 13H21.0407L23.2816 13.7124L25.448 13H25.9725C25.9907 13.1642 26 13.331 26 13.5C26 15.9853 23.9853 18 21.5 18C19.0147 18 17 15.9853 17 13.5Z" fill="white" />
|
||||
<path d="M10 13.25C10 13.1655 10.0046 13.0821 10.0137 13H14.4863C14.4954 13.0821 14.5 13.1655 14.5 13.25C14.5 14.4945 13.4945 15.5 12.25 15.5C11.0055 15.49 10 14.4845 10 13.25Z" fill="#402A32" />
|
||||
<path d="M21 13.25C21 13.1655 21.0046 13.0821 21.0137 13H25.4863C25.4954 13.0821 25.5 13.1655 25.5 13.25C25.5 14.4945 24.4945 15.5 23.25 15.5C22.0055 15.49 21 14.4845 21 13.25Z" fill="#402A32" />
|
||||
<path d="M8.06915 7.98761C7.47625 8.55049 7.11769 9.22774 6.97423 9.65811C6.88691 9.92009 6.60375 10.0617 6.34178 9.97434C6.07981 9.88702 5.93823 9.60386 6.02555 9.34189C6.21542 8.77226 6.65687 7.94951 7.38064 7.26239C8.1129 6.5672 9.1478 6 10.4999 6C10.776 6 10.9999 6.22386 10.9999 6.5C10.9999 6.77614 10.776 7 10.4999 7C9.45198 7 8.65355 7.4328 8.06915 7.98761Z" fill="#402A32" />
|
||||
<path d="M23.9309 7.98761C24.5238 8.55049 24.8823 9.22774 25.0258 9.65811C25.1131 9.92009 25.3963 10.0617 25.6582 9.97434C25.9202 9.88702 26.0618 9.60386 25.9745 9.34189C25.7846 8.77226 25.3431 7.94951 24.6194 7.26239C23.8871 6.5672 22.8522 6 21.5001 6C21.224 6 21.0001 6.22386 21.0001 6.5C21.0001 6.77614 21.224 7 21.5001 7C22.548 7 23.3465 7.4328 23.9309 7.98761Z" fill="#402A32" />
|
||||
<path d="M23.9466 21.2622C24.1246 20.7393 23.845 20.1713 23.3222 19.9933C22.7993 19.8153 22.2313 20.0949 22.0533 20.6178C21.1017 23.4135 18.0618 24.9046 15.2647 23.9442C14.7424 23.7648 14.1735 24.0429 13.9942 24.5652C13.8148 25.0876 14.0929 25.6564 14.6152 25.8358C18.4581 27.1553 22.6382 25.1065 23.9466 21.2622Z" fill="#402A32" />
|
||||
</svg>
|
||||
)
|
||||
}, {
|
||||
rating: 2,
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M15.9989 29.9978C25.3333 29.9978 29.9978 23.7303 29.9978 15.9989C29.9978 8.26751 25.3333 2 15.9989 2C6.66443 2 2 8.26751 2 15.9989C2 23.7303 6.66443 29.9978 15.9989 29.9978Z" fill="#FFB02E" />
|
||||
<path d="M10.4191 16.2244C12.742 16.2244 14.6251 14.3414 14.6251 12.0185C14.6251 9.69557 12.742 7.8125 10.4191 7.8125C8.09621 7.8125 6.21313 9.69557 6.21313 12.0185C6.21313 14.3414 8.09621 16.2244 10.4191 16.2244Z" fill="white" />
|
||||
<path d="M21.5683 16.3011C23.9123 16.3011 25.8126 14.4009 25.8126 12.0568C25.8126 9.71274 23.9123 7.8125 21.5683 7.8125C19.2242 7.8125 17.324 9.71274 17.324 12.0568C17.324 14.4009 19.2242 16.3011 21.5683 16.3011Z" fill="white" />
|
||||
<path d="M11 15C12.6569 15 14 13.6569 14 12C14 10.3431 12.6569 9 11 9C9.34315 9 8 10.3431 8 12C8 13.6569 9.34315 15 11 15Z" fill="#402A32" />
|
||||
<path d="M21 15C22.6569 15 24 13.6569 24 12C24 10.3431 22.6569 9 21 9C19.3431 9 18 10.3431 18 12C18 13.6569 19.3431 15 21 15Z" fill="#402A32" />
|
||||
<path d="M10 20C10 19.4477 10.4477 19 11 19H21C21.5523 19 22 19.4477 22 20C22 20.5523 21.5523 21 21 21H11C10.4477 21 10 20.5523 10 20Z" fill="#402A32" />
|
||||
</svg>
|
||||
)
|
||||
}, {
|
||||
rating: 1,
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M15.9989 29.9978C25.3333 29.9978 29.9978 23.7303 29.9978 15.9989C29.9978 8.26751 25.3333 2 15.9989 2C6.66443 2 2 8.26751 2 15.9989C2 23.7303 6.66443 29.9978 15.9989 29.9978Z" fill="#F8312F" />
|
||||
<path d="M10.5 19C12.9853 19 15 16.9853 15 14.5C15 12.0147 12.9853 10 10.5 10C8.01472 10 6 12.0147 6 14.5C6 16.9853 8.01472 19 10.5 19Z" fill="white" />
|
||||
<path d="M21.5 19C23.9853 19 26 16.9853 26 14.5C26 12.0147 23.9853 10 21.5 10C19.0147 10 17 12.0147 17 14.5C17 16.9853 19.0147 19 21.5 19Z" fill="white" />
|
||||
<path d="M14.9989 11.2899C15.0209 10.8763 14.7035 10.5231 14.2899 10.5011C13.4607 10.4569 12.7846 10.2597 12.2504 9.88776C11.7235 9.52078 11.2715 8.93987 10.9612 8.01214C10.8299 7.61931 10.4049 7.40737 10.0121 7.53875C9.61925 7.67013 9.40731 8.09508 9.53869 8.48791C9.93308 9.66714 10.558 10.537 11.3932 11.1187C12.2213 11.6954 13.1929 11.9447 14.21 11.999C14.6237 12.021 14.9768 11.7036 14.9989 11.2899Z" fill="#402A32" />
|
||||
<path d="M17.001 11.2899C16.979 10.8763 17.2964 10.5231 17.71 10.5011C18.5392 10.4569 19.2153 10.2597 19.7495 9.88776C20.2764 9.52078 20.7284 8.93987 21.0387 8.01214C21.1701 7.61931 21.595 7.40737 21.9878 7.53875C22.3807 7.67013 22.5926 8.09508 22.4612 8.48791C22.0668 9.66714 21.442 10.537 20.6067 11.1187C19.7786 11.6954 18.807 11.9447 17.7899 11.999C17.3763 12.021 17.0231 11.7036 17.001 11.2899Z" fill="#402A32" />
|
||||
<path d="M14 15C14 16.1046 13.1046 17 12 17C10.8954 17 10 16.1046 10 15C10 13.8954 10.8954 13 12 13C13.1046 13 14 13.8954 14 15Z" fill="#402A32" />
|
||||
<path d="M22 15C22 16.1046 21.1046 17 20 17C18.8954 17 18 16.1046 18 15C18 13.8954 18.8954 13 20 13C21.1046 13 22 13.8954 22 15Z" fill="#402A32" />
|
||||
<path d="M7 21.5C7 20.6716 7.67157 20 8.5 20H23.5C24.3284 20 25 20.6716 25 21.5V27.5C25 28.3284 24.3284 29 23.5 29H8.5C7.67157 29 7 28.3284 7 27.5V21.5Z" fill="#533566" />
|
||||
<path d="M12.1756 22.0318C12.4341 22.1288 12.5651 22.417 12.4682 22.6756L12.3465 23H13.7785L14.0318 22.3245C14.1288 22.0659 14.417 21.9349 14.6756 22.0318C14.9341 22.1288 15.0651 22.417 14.9682 22.6756L14.8465 23H15C15.2761 23 15.5 23.2239 15.5 23.5C15.5 23.7762 15.2761 24 15 24H14.4715L14.0965 25H14.5C14.7761 25 15 25.2239 15 25.5C15 25.7762 14.7761 26 14.5 26H13.7215L13.4682 26.6756C13.3712 26.9341 13.083 27.0651 12.8244 26.9682C12.5659 26.8712 12.4349 26.583 12.5318 26.3245L12.6535 26H11.2215L10.9682 26.6756C10.8712 26.9341 10.583 27.0651 10.3244 26.9682C10.0659 26.8712 9.93488 26.583 10.0318 26.3245L10.1535 26H10C9.72386 26 9.5 25.7762 9.5 25.5C9.5 25.2239 9.72386 25 10 25H10.5285L10.9035 24H10.5C10.2239 24 10 23.7762 10 23.5C10 23.2239 10.2239 23 10.5 23H11.2785L11.5318 22.3245C11.6288 22.0659 11.917 21.9349 12.1756 22.0318ZM13.4035 24H11.9715L11.5965 25H13.0285L13.4035 24Z" fill="white" />
|
||||
<path d="M18.9682 22.6756C19.0651 22.417 18.9341 22.1288 18.6756 22.0318C18.417 21.9349 18.1288 22.0659 18.0318 22.3245L16.5318 26.3245C16.4349 26.583 16.5659 26.8712 16.8244 26.9682C17.083 27.0651 17.3712 26.9341 17.4682 26.6756L18.9682 22.6756Z" fill="white" />
|
||||
<path d="M19 27C19.2761 27 19.5 26.7762 19.5 26.5C19.5 26.2239 19.2761 26 19 26C18.7239 26 18.5 26.2239 18.5 26.5C18.5 26.7762 18.7239 27 19 27Z" fill="white" />
|
||||
<path d="M22 22.5C22 22.2239 21.7761 22 21.5 22C21.2239 22 21 22.2239 21 22.5V24.5C21 24.7762 21.2239 25 21.5 25C21.7761 25 22 24.7762 22 24.5V22.5Z" fill="white" />
|
||||
<path d="M16.5 23C16.7761 23 17 22.7762 17 22.5C17 22.2239 16.7761 22 16.5 22C16.2239 22 16 22.2239 16 22.5C16 22.7762 16.2239 23 16.5 23Z" fill="white" />
|
||||
<path d="M22 26.5C22 26.7762 21.7761 27 21.5 27C21.2239 27 21 26.7762 21 26.5C21 26.2239 21.2239 26 21.5 26C21.7761 26 22 26.2239 22 26.5Z" fill="white" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
export default component$(() => {
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavBar />
|
||||
<TitleSection client:load title="Contact" description="Need help? Found a bug? Have a suggestion? Let us know!" />
|
||||
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={transition}
|
||||
client:load
|
||||
class="flex items-center justify-center w-full"
|
||||
as="div"
|
||||
>
|
||||
<section class="w-full flex justify-center items-center">
|
||||
<div class="w-full max-w-xl space-y-6 px-3">
|
||||
<div class="flex gap-3 w-full justify-around" >
|
||||
{feedback.map((item, index) => (
|
||||
<div key={`emoji-${index}`} class="flex relative">
|
||||
<input type="radio" class="hidden peer" name="feedback-emoji" value={item.rating} id={`emoji-${index + 1}`} />
|
||||
<label for={`emoji-${index + 1}`} class="peer-checked:bg-gray-300/70 dark:peer-checked:bg-gray-700/70 border-gray-300 dark:border-gray-700 peer-checked:ring-gray-400 dark:peer-checked:ring-gray-600 peer-checked:ring-offset-gray-50 dark:peer-checked:ring-offset-gray-950 peer-checked:ring-2 peer-checked:ring-offset-2 border cursor-pointer bg-gray-200/70 dark:bg-gray-800/70 rounded-full p-3">
|
||||
<item.icon />
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="bg-gray-200/70 dark:bg-gray-800/70 flex rounded-lg w-full relative h-10 flex-none border focus-within:bg-gray-300/50 dark:focus-within:bg-gray-700/50 border-gray-300 dark:border-gray-700 ">
|
||||
<input type="text" class="w-full h-full bg-transparent rounded-lg p-3 focus-within:outline-none focus-within:ring-2 focus-within:ring-gray-400 dark:focus-within:ring-gray-600 focus-within:ring-offset-2 focus-visible:outline-none focus-within:ring-offset-gray-50 dark:focus-within:ring-offset-gray-950 placeholder:text-gray-500/70" placeholder="Full Name" />
|
||||
</div>
|
||||
<div class="bg-gray-200/70 dark:bg-gray-800/70 flex rounded-lg w-full relative h-10 flex-none border focus-within:bg-gray-300/50 dark:focus-within:bg-gray-700/50 border-gray-300 dark:border-gray-700 ">
|
||||
<input type="email" class="w-full h-full bg-transparent rounded-lg p-3 focus-within:outline-none focus-within:ring-2 focus-within:ring-gray-400 dark:focus-within:ring-gray-600 focus-within:ring-offset-2 focus-visible:outline-none focus-within:ring-offset-gray-50 dark:focus-within:ring-offset-gray-950 placeholder:text-gray-500/70" placeholder="Email Address" />
|
||||
</div>
|
||||
<div class="bg-gray-200/70 dark:bg-gray-800/70 flex rounded-lg w-full relative h-max flex-none border focus-within:bg-gray-300/50 dark:focus-within:bg-gray-700/50 border-gray-300 dark:border-gray-700 ">
|
||||
<textarea class="resize-y overflow-y-auto whitespace-break-spaces [form-sizing:content] min-h-[193px] w-full h-full bg-transparent rounded-lg p-3 focus-within:outline-none focus-within:ring-2 focus-within:ring-gray-400 dark:focus-within:ring-gray-600 focus-within:ring-offset-2 focus-visible:outline-none focus-within:ring-offset-gray-50 dark:focus-within:ring-offset-gray-950 placeholder:text-gray-500/70" placeholder="Your message" />
|
||||
</div>
|
||||
<button class={cn(buttonVariants.solid({ size: "md", intent: "neutral" }), "w-full")} type="submit" >
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</MotionComponent>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
})
|
||||
10
apps/www/src/routes/home/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { HomeNavBar } from "@nestri/ui";
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
<>
|
||||
<HomeNavBar />
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -1,9 +1,683 @@
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { Link, type DocumentHead } from "@builder.io/qwik-city";
|
||||
import { HeroSection, Cursor, MotionComponent, transition } from "@nestri/ui/react"
|
||||
import { NavBar, Footer } from "@nestri/ui"
|
||||
import { BasicImageLoader } from "@nestri/ui/image";
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: "Play games from shared Steam libraries",
|
||||
description: "Grant access to your Steam library, allowing everyone on your team to enjoy a wide range of games without additional purchases.",
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none"><circle cx="10" cy="6" r="4" stroke="currentColor" stroke-width="1.5" /><path stroke="currentColor" stroke-width="1.5" d="M18 17.5c0 2.485 0 4.5-8 4.5s-8-2.015-8-4.5S5.582 13 10 13s8 2.015 8 4.5Z" opacity=".5" /><path fill="currentColor" d="m18.089 12.539l.455-.597zM19 8.644l-.532.528a.75.75 0 0 0 1.064 0zm.912 3.895l-.456-.597zm-1.368-.597c-.487-.371-.925-.668-1.278-1.053c-.327-.357-.516-.725-.516-1.19h-1.5c0 .95.414 1.663.91 2.204c.471.513 1.077.93 1.474 1.232zM16.75 9.7c0-.412.24-.745.547-.881c.267-.118.69-.13 1.171.353l1.064-1.057c-.87-.875-1.945-1.065-2.842-.668A2.46 2.46 0 0 0 15.25 9.7zm.884 3.435c.148.113.342.26.545.376s.487.239.821.239v-1.5c.034 0 .017.011-.082-.044c-.1-.056-.212-.14-.374-.264zm2.732 0c.397-.303 1.003-.719 1.473-1.232c.497-.541.911-1.255.911-2.203h-1.5c0 .464-.189.832-.516 1.19c-.353.384-.791.681-1.278 1.052zM22.75 9.7c0-1-.585-1.875-1.44-2.253c-.896-.397-1.973-.207-2.842.668l1.064 1.057c.48-.483.904-.471 1.17-.353a.96.96 0 0 1 .548.88zm-3.294 2.242a4 4 0 0 1-.374.264c-.099.056-.116.044-.082.044v1.5c.334 0 .617-.123.82-.239c.204-.115.398-.263.546-.376z" /></g></svg>
|
||||
)
|
||||
}, {
|
||||
title: "Create a safe gaming environment for all ages",
|
||||
description: "Keep gaming safe and enjoyable for everyone. Set playtime limits and content restrictions to maintain a family-friendly environment, giving you peace of mind.",
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 10.417c0-3.198 0-4.797.378-5.335c.377-.537 1.88-1.052 4.887-2.081l.573-.196C10.405 2.268 11.188 2 12 2c.811 0 1.595.268 3.162.805l.573.196c3.007 1.029 4.51 1.544 4.887 2.081C21 5.62 21 7.22 21 10.417v1.574c0 5.638-4.239 8.375-6.899 9.536C13.38 21.842 13.02 22 12 22s-1.38-.158-2.101-.473C7.239 20.365 3 17.63 3 11.991z" opacity=".5" /><path stroke-linecap="round" d="M12 13.5v3m1.5-3.402a3 3 0 1 1-3-5.195a3 3 0 0 1 3 5.195Z" /></g></svg>
|
||||
)
|
||||
}, {
|
||||
title: "Experience stunning HD gameplay with zero lag",
|
||||
description: "Experience games in high definition with Real-Time Ray Tracing, while our QUIC-enhanced infrastructure ensures smooth, responsive sessions with minimal latency.",
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24"><mask id="lineMdSpeedLoop0"><path fill="none" stroke="#fff" stroke-dasharray="56" stroke-dashoffset="56" stroke-linecap="round" stroke-width="2" d="M5 19V19C4.69726 19 4.41165 18.8506 4.25702 18.5904C3.45852 17.2464 3 15.6767 3 14C3 9.02944 7.02944 5 12 5C16.9706 5 21 9.02944 21 14C21 15.6767 20.5415 17.2464 19.743 18.5904C19.5884 18.8506 19.3027 19 19 19z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="56;0" /></path><g fill-opacity="0" transform="rotate(-100 12 14)"><path d="M12 14C12 14 12 14 12 14C12 14 12 14 12 14C12 14 12 14 12 14C12 14 12 14 12 14Z"><animate fill="freeze" attributeName="d" begin="0.4s" dur="0.2s" values="M12 14C12 14 12 14 12 14C12 14 12 14 12 14C12 14 12 14 12 14C12 14 12 14 12 14Z;M16 14C16 16.21 14.21 18 12 18C9.79 18 8 16.21 8 14C8 11.79 12 0 12 0C12 0 16 11.79 16 14Z" /></path><path fill="#fff" d="M12 14C12 14 12 14 12 14C12 14 12 14 12 14C12 14 12 14 12 14C12 14 12 14 12 14Z"><animate fill="freeze" attributeName="d" begin="0.4s" dur="0.2s" values="M12 14C12 14 12 14 12 14C12 14 12 14 12 14C12 14 12 14 12 14C12 14 12 14 12 14Z;M14 14C14 15.1 13.1 16 12 16C10.9 16 10 15.1 10 14C10 12.9 12 4 12 4C12 4 14 12.9 14 14Z" /></path><set attributeName="fill-opacity" begin="0.4s" to="1" /><animateTransform attributeName="transform" begin="0.6s" dur="6s" repeatCount="indefinite" type="rotate" values="-100 12 14;45 12 14;45 12 14;45 12 14;20 12 14;10 12 14;0 12 14;35 12 14;45 12 14;55 12 14;50 12 14;15 12 14;-20 12 14;-100 12 14" /></g></mask><rect width="24" height="24" fill="currentColor" mask="url(#lineMdSpeedLoop0)" /></svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Share and connect with gamers worldwide",
|
||||
description: "Post clips, screenshots, and live streams directly from your game, and join Nestri Parties to team up with friends or challenge players globally in multiplayer battles and co-op adventures.",
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.5"><path d="M10.861 3.363C11.368 2.454 11.621 2 12 2s.632.454 1.139 1.363l.13.235c.145.259.217.388.329.473c.112.085.252.117.532.18l.254.058c.984.222 1.476.334 1.593.71c.117.376-.218.769-.889 1.553l-.174.203c-.19.223-.285.334-.328.472c-.043.138-.029.287 0 .584l.026.27c.102 1.047.152 1.57-.154 1.803c-.306.233-.767.02-1.688-.404l-.239-.11c-.261-.12-.392-.18-.531-.18s-.27.06-.531.18l-.239.11c-.92.425-1.382.637-1.688.404c-.306-.233-.256-.756-.154-1.802l.026-.271c.029-.297.043-.446 0-.584c-.043-.138-.138-.25-.328-.472l-.174-.203c-.67-.784-1.006-1.177-.889-1.553c.117-.376.609-.488 1.593-.71l.254-.058c.28-.063.42-.095.532-.18c.112-.085.184-.214.328-.473zm8.569 4.319c.254-.455.38-.682.57-.682c.19 0 .316.227.57.682l.065.117c.072.13.108.194.164.237c.056.042.126.058.266.09l.127.028c.492.112.738.167.796.356c.059.188-.109.384-.444.776l-.087.101c-.095.112-.143.168-.164.237c-.022.068-.014.143 0 .292l.013.135c.05.523.076.785-.077.901c-.153.116-.383.01-.844-.202l-.12-.055c-.13-.06-.196-.09-.265-.09c-.07 0-.135.03-.266.09l-.119.055c-.46.212-.69.318-.844.202c-.153-.116-.128-.378-.077-.901l.013-.135c.014-.15.022-.224 0-.292c-.021-.07-.069-.125-.164-.237l-.087-.101c-.335-.392-.503-.588-.444-.776c.058-.189.304-.244.796-.356l.127-.028c.14-.032.21-.048.266-.09c.056-.043.092-.108.164-.237zm-16 0C3.685 7.227 3.81 7 4 7c.19 0 .316.227.57.682l.065.117c.072.13.108.194.164.237c.056.042.126.058.266.09l.127.028c.492.112.738.167.797.356c.058.188-.11.384-.445.776l-.087.101c-.095.112-.143.168-.164.237c-.022.068-.014.143 0 .292l.013.135c.05.523.076.785-.077.901c-.153.116-.384.01-.844-.202l-.12-.055c-.13-.06-.196-.09-.265-.09c-.07 0-.135.03-.266.09l-.119.055c-.46.212-.69.318-.844.202c-.153-.116-.128-.378-.077-.901l.013-.135c.014-.15.022-.224 0-.292c-.021-.07-.069-.125-.164-.237l-.087-.101c-.335-.392-.503-.588-.445-.776c.059-.189.305-.244.797-.356l.127-.028c.14-.032.21-.048.266-.09c.056-.043.092-.108.164-.237z" /><path stroke-linecap="round" d="M4 21.388h2.26c1.01 0 2.033.106 3.016.308a14.85 14.85 0 0 0 5.33.118c.868-.14 1.72-.355 2.492-.727c.696-.337 1.549-.81 2.122-1.341c.572-.53 1.168-1.397 1.59-2.075c.364-.582.188-1.295-.386-1.728a1.887 1.887 0 0 0-2.22 0l-1.807 1.365c-.7.53-1.465 1.017-2.376 1.162c-.11.017-.225.033-.345.047m0 0a8.176 8.176 0 0 1-.11.012m.11-.012a.998.998 0 0 0 .427-.24a1.492 1.492 0 0 0 .126-2.134a1.9 1.9 0 0 0-.45-.367c-2.797-1.669-7.15-.398-9.779 1.467m9.676 1.274a.524.524 0 0 1-.11.012m0 0a9.274 9.274 0 0 1-1.814.004" opacity=".5" /></g></svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Customize your entire gaming experience",
|
||||
description: "Personalize controls for your preferred device, enhance your experience with mods, and adapt game settings to match your unique style or add exciting new challenges.",
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path d="m12.636 15.262l-1.203 1.202c-1.23 1.232-1.846 1.847-2.508 1.702c-.662-.146-.963-.963-1.565-2.596l-2.007-5.45C4.152 6.861 3.55 5.232 4.39 4.392c.84-.84 2.47-.24 5.73.962l5.45 2.006c1.633.602 2.45.903 2.596 1.565c.145.662-.47 1.277-1.702 2.508l-1.202 1.203" /><path d="m12.636 15.262l3.938 3.938c.408.408.612.612.84.706c.303.126.643.126.947 0c.227-.094.431-.298.839-.706s.611-.612.706-.84a1.238 1.238 0 0 0 0-.946c-.095-.228-.298-.432-.706-.84l-3.938-3.938" opacity=".5" /></g></svg>
|
||||
)
|
||||
}, {
|
||||
title: "Access and share your progress from anywhere",
|
||||
description: "With Nestri's Cloud-based saving, you can access and share your progress with just a link, keeping you connected to your games and friends instantly wherever you are.",
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" d="M16.5 7.5h-3" opacity=".5" /><path d="M5 5.217c0-.573 0-.86.049-1.099c.213-1.052 1.1-1.874 2.232-2.073C7.538 2 7.847 2 8.465 2c.27 0 .406 0 .536.011c.56.049 1.093.254 1.526.587c.1.078.196.167.388.344l.385.358c.571.53.857.795 1.198.972c.188.097.388.174.594.228c.377.1.78.1 1.588.1h.261c1.843 0 2.765 0 3.363.5c.055.046.108.095.157.146C19 5.802 19 6.658 19 8.369V9.8c0 2.451 0 3.677-.82 4.438c-.82.762-2.14.762-4.78.762h-2.8c-2.64 0-3.96 0-4.78-.761C5 13.477 5 12.25 5 9.8z" /><path stroke-linecap="round" d="M22 20h-8M2 20h8m2-2v-3" opacity=".5" /><circle cx="12" cy="20" r="2" /></g></svg>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
const games = [
|
||||
{
|
||||
title: "Apex Legends",
|
||||
rotate: -5,
|
||||
image: "https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/1172470/library_600x900_2x.jpg",
|
||||
link: "/games/apex-legends"
|
||||
},
|
||||
{
|
||||
title: "Control Ultimate Edition",
|
||||
rotate: 3,
|
||||
image: "https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/870780/library_600x900_2x.jpg",
|
||||
link: "/games/control-ultimate-edition"
|
||||
},
|
||||
{
|
||||
title: "Black Myth: Wukong",
|
||||
rotate: -3,
|
||||
image: "https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/2358720/library_600x900_2x.jpg",
|
||||
link: "/games/black-myth-wukong"
|
||||
},
|
||||
{
|
||||
title: "Shell Runner - Prelude",
|
||||
rotate: 2,
|
||||
image: "https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/2581970/library_600x900_2x.jpg",
|
||||
link: "/games/shell-runner-prelude"
|
||||
},
|
||||
{
|
||||
title: "Counter-Strike 2",
|
||||
rotate: -5,
|
||||
image: "https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/730/library_600x900_2x.jpg",
|
||||
link: "/games/counter-strike-2"
|
||||
},
|
||||
{
|
||||
title: "Add from Steam",
|
||||
rotate: 7,
|
||||
image: undefined,
|
||||
link: "/games/add-from-steam"
|
||||
}
|
||||
]
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
<div class="bg-primary-500 h-screen w-screen">
|
||||
<>
|
||||
<NavBar />
|
||||
<HeroSection client:load>
|
||||
<button class="w-full max-w-xl rounded-xl flex items-center justify-between hover:bg-gray-300/70 dark:hover:bg-gray-700/70 transition-colors gap-2 px-4 py-3 h-[45px] border border-gray-300 dark:border-gray-700 mx-autotext-gray-500/70 bg-gray-200/70 dark:bg-gray-800/70">
|
||||
<span class="flex items-center gap-3 h-max justify-center overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-[18] flex-shrink-0" height="18" width="18" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2"><circle cx="11.5" cy="11.5" r="9.5" /><path stroke-linecap="round" d="M18.5 18.5L22 22" /></g></svg>
|
||||
Search for a game to play...
|
||||
</span>
|
||||
<span class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2 text-base font-title font-bold">
|
||||
<kbd class="border-neutral-300 dark:border-neutral-700 border px-2 py-1 rounded-md">
|
||||
{/* ⌘ */}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-5 flex-shrink-0" width="20" height="20" viewBox="0 0 256 256"><path fill="currentColor" d="M180 144h-20v-32h20a36 36 0 1 0-36-36v20h-32V76a36 36 0 1 0-36 36h20v32H76a36 36 0 1 0 36 36v-20h32v20a36 36 0 1 0 36-36m-20-68a20 20 0 1 1 20 20h-20ZM56 76a20 20 0 0 1 40 0v20H76a20 20 0 0 1-20-20m40 104a20 20 0 1 1-20-20h20Zm16-68h32v32h-32Zm68 88a20 20 0 0 1-20-20v-20h20a20 20 0 0 1 0 40" /></svg>
|
||||
{/* <svg xmlns="http://www.w3.org/2000/svg" class="size-5 flex-shrink-0" width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M6.5 21q-1.45 0-2.475-1.025T3 17.5t1.025-2.475T6.5 14H8v-4H6.5q-1.45 0-2.475-1.025T3 6.5t1.025-2.475T6.5 3t2.475 1.025T10 6.5V8h4V6.5q0-1.45 1.025-2.475T17.5 3t2.475 1.025T21 6.5t-1.025 2.475T17.5 10H16v4h1.5q1.45 0 2.475 1.025T21 17.5t-1.025 2.475T17.5 21t-2.475-1.025T14 17.5V16h-4v1.5q0 1.45-1.025 2.475T6.5 21m0-2q.625 0 1.063-.437T8 17.5V16H6.5q-.625 0-1.062.438T5 17.5t.438 1.063T6.5 19m11 0q.625 0 1.063-.437T19 17.5t-.437-1.062T17.5 16H16v1.5q0 .625.438 1.063T17.5 19M10 14h4v-4h-4zM6.5 8H8V6.5q0-.625-.437-1.062T6.5 5t-1.062.438T5 6.5t.438 1.063T6.5 8M16 8h1.5q.625 0 1.063-.437T19 6.5t-.437-1.062T17.5 5t-1.062.438T16 6.5z"/></svg> */}
|
||||
</kbd>
|
||||
<span class="px-2 py-0.5 rounded-md border border-neutral-300 dark:border-neutral-700">
|
||||
K
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
</HeroSection>
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={transition}
|
||||
client:load
|
||||
class="items-center justify-center w-full flex"
|
||||
as="div"
|
||||
>
|
||||
<section class="relative py-10 flex-col w-full overflow-hidden">
|
||||
<div class="grid grid-cols-3 -mx-5 max-w-7xl md:grid-cols-6 lg:mx-auto">
|
||||
{games.map((game, index) => (
|
||||
<Link
|
||||
key={game.title}
|
||||
href={game.link}
|
||||
style={{
|
||||
zIndex: 1 + index,
|
||||
transform: game.rotate ? `rotate(${game.rotate}deg)` : undefined,
|
||||
}}
|
||||
class={"aspect-[2/3] bg-gray-100 dark:bg-gray-900 rounded-md overflow-hidden block hover:!rotate-0 hover:scale-[1.17] hover:!z-10 ring-1 shadow-lg shadow-gray-300 dark:shadow-gray-700 ring-gray-300 dark:ring-gray-700 transition-all duration-200"}>
|
||||
{game.image ? <BasicImageLoader width={600} height={900} src={game.image} alt={game.title} /> :
|
||||
<div class="w-full text-gray-900 dark:text-gray-100 h-full flex flex-col px-3 text-center gap-3 items-center justify-center">
|
||||
<p>Can't find your game here?</p>
|
||||
<span class="text-gray-800 dark:text-gray-200 underline text-sm">
|
||||
Import from Steam
|
||||
</span>
|
||||
</div>}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</MotionComponent>
|
||||
<section class="relative py-10 flex-col w-full justify-center items-center">
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ ...transition, delay: 0.2 }}
|
||||
client:load
|
||||
class="items-center justify-center w-full flex"
|
||||
as="div"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center text-left px-4 w-full mx-auto gap-4 sm:max-w-[560px] py-8">
|
||||
<h2 class="text-5xl font-bold font-title w-full">Why Us?</h2>
|
||||
<p class="text-gray-500 text-2xl">From streaming quality to social integration, we nail the details.</p>
|
||||
</div>
|
||||
</MotionComponent>
|
||||
<div class="flex items-center flex-col px-5 gap-5 justify-between w-full mx-auto max-w-xl">
|
||||
{
|
||||
features.map((feature, index) => (
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ ...transition, delay: 0.2 * index }}
|
||||
client:load
|
||||
key={feature.title}
|
||||
class="w-full"
|
||||
as="div"
|
||||
>
|
||||
<div class="w-full flex gap-4 group">
|
||||
<div class="size-9 [&>svg]:size-9 group-hover:scale-110 transition-all duration-200">
|
||||
<feature.icon />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold font-title">
|
||||
{feature.title}
|
||||
</h2>
|
||||
<p class="text-gray-500">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</MotionComponent>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
<section class="relative py-10 flex-col w-full space-y-8">
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={transition}
|
||||
client:load
|
||||
class="items-center justify-center w-full flex"
|
||||
as="div"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center text-left w-full mx-auto px-4 gap-4 sm:max-w-[560px] py-8">
|
||||
<h2 class="text-5xl font-bold font-title w-full">How it works</h2>
|
||||
<p class="text-gray-500 text-2xl w-full">From click → play in under three minutes</p>
|
||||
</div>
|
||||
</MotionComponent>
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={transition}
|
||||
client:load
|
||||
class="items-center justify-center w-full flex"
|
||||
as="div"
|
||||
>
|
||||
<div class="w-full mx-auto max-w-xl flex items-center flex-col lg:flex-row gap-6 justify-center">
|
||||
<div class="flex cursor-default items-end group">
|
||||
<div class="flex [transform:perspective(700px)] w-[61px] [transform-style:preserve-3d] relative">
|
||||
<p class="font-bold text-[200px] text-gray-50 dark:text-gray-950 group-hover:text-primary-200 dark:group-hover:text-primary-800 group-hover:-translate-x-2 transition-all duration-200 [-webkit-text-stroke-color:theme(colors.primary.500)] [-webkit-text-stroke-width:2px] leading-[1em]">
|
||||
1
|
||||
</p>
|
||||
</div>
|
||||
<div class="z-[1] group-hover:ring-primary-500 gap-4 flex items-center justify-center flex-col transition-all ring-[3px] ring-gray-300 dark:ring-gray-700 duration-200 h-[260px] aspect-square bg-gray-100 dark:bg-gray-900 rounded-2xl overflow-hidden">
|
||||
<div class="flex items-center justify-center" >
|
||||
<div class="z-[4] flex relative items-center justify-center size-[66px] transition-all duration-200 rounded-full bg-gray-200 dark:bg-gray-800 text-gray-500 dark:group-hover:bg-primary-800 group-hover:bg-primary-200 shadow-lg shadow-gray-300 dark:shadow-gray-700" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" class="size-10 flex-shrink-0 group-hover:hidden" height="32" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"><path d="M6.286 19C3.919 19 2 17.104 2 14.765s1.919-4.236 4.286-4.236q.427.001.83.08m7.265-2.582a5.8 5.8 0 0 1 1.905-.321c.654 0 1.283.109 1.87.309m-11.04 2.594a5.6 5.6 0 0 1-.354-1.962C6.762 5.528 9.32 3 12.476 3c2.94 0 5.361 2.194 5.68 5.015m-11.04 2.594a4.3 4.3 0 0 1 1.55.634m9.49-3.228C20.392 8.78 22 10.881 22 13.353c0 2.707-1.927 4.97-4.5 5.52" opacity=".5" /><path stroke-linejoin="round" d="M12 22v-6m0 6l2-2m-2 2l-2-2" /></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" class="size-10 flex-shrink-0 transition-all duration-200 group-hover:block hidden text-primary-500" height="32" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"><path d="M6.286 19C3.919 19 2 17.104 2 14.765s1.919-4.236 4.286-4.236q.427.001.83.08m7.265-2.582a5.8 5.8 0 0 1 1.905-.321c.654 0 1.283.109 1.87.309m-11.04 2.594a5.6 5.6 0 0 1-.354-1.962C6.762 5.528 9.32 3 12.476 3c2.94 0 5.361 2.194 5.68 5.015m-11.04 2.594a4.3 4.3 0 0 1 1.55.634m9.49-3.228C20.392 8.78 22 10.881 22 13.353c0 2.707-1.927 4.97-4.5 5.52" opacity=".5" /><path stroke-linejoin="round" d="m10 19.8l1.143 1.2L14 18" /></g></svg>
|
||||
</div>
|
||||
<div class="-mx-3 group-hover:-mx-2 transition-all duration-200 size-[90px] rounded-full bg-gray-100 dark:bg-gray-900 shadow-lg shadow-gray-300 dark:shadow-gray-700 z-10 relative flex items-center justify-center">
|
||||
<svg
|
||||
width="48.672001"
|
||||
height="36.804001"
|
||||
class="size-[50px] flex-shrink-0"
|
||||
viewBox="0 0 12.8778 9.7377253"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="layer1">
|
||||
<path
|
||||
d="m 2.093439,1.7855532 h 8.690922 V 2.2639978 H 2.093439 Z m 0,2.8440874 h 8.690922 V 5.1080848 H 2.093439 Z m 0,2.8440866 h 8.690922 V 7.952172 H 2.093439 Z"
|
||||
style="font-size:12px;fill:#ff4f01;fill-opacity:1;fill-rule:evenodd;stroke:#ff4f01;stroke-width:1.66201;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="z-[4] flex relative items-center justify-center size-[66px] rounded-full bg-[#273b4b] text-gray-50 shadow-lg shadow-gray-300 dark:shadow-gray-700" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-10 flex-shrink-0" width="28" height="32" viewBox="0 0 448 512"><path fill="currentColor" d="M395.5 177.5c0 33.8-27.5 61-61 61c-33.8 0-61-27.3-61-61s27.3-61 61-61c33.5 0 61 27.2 61 61m52.5.2c0 63-51 113.8-113.7 113.8L225 371.3c-4 43-40.5 76.8-84.5 76.8c-40.5 0-74.7-28.8-83-67L0 358V250.7L97.2 290c15.1-9.2 32.2-13.3 52-11.5l71-101.7c.5-62.3 51.5-112.8 114-112.8C397 64 448 115 448 177.7M203 363c0-34.7-27.8-62.5-62.5-62.5q-6.75 0-13.5 1.5l26 10.5c25.5 10.2 38 39 27.7 64.5c-10.2 25.5-39.2 38-64.7 27.5c-10.2-4-20.5-8.3-30.7-12.2c10.5 19.7 31.2 33.2 55.2 33.2c34.7 0 62.5-27.8 62.5-62.5m207.5-185.3c0-42-34.3-76.2-76.2-76.2c-42.3 0-76.5 34.2-76.5 76.2c0 42.2 34.3 76.2 76.5 76.2c41.9.1 76.2-33.9 76.2-76.2" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 w-full items-center justify-center">
|
||||
<p class="text-gray-500 max-w-[80%] text-center mx-auto text-2xl font-title">
|
||||
<strong>Add</strong> your game from Steam
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex cursor-default group items-end">
|
||||
<div class="flex [transform:perspective(700px)] w-[80px] [transform-style:preserve-3d] relative">
|
||||
<p class="font-bold text-[200px] dark:text-gray-950 group-hover:text-primary-200 dark:group-hover:text-primary-800 leading-[1em] group-hover:-translate-x-2 transition-all duration-200 relative text-gray-50 [-webkit-text-stroke-color:theme(colors.primary.500)] [-webkit-text-stroke-width:2px]">
|
||||
2
|
||||
</p>
|
||||
</div>
|
||||
<div class="z-[1] group-hover:ring-primary-500 gap-4 flex items-center justify-center flex-col transition-all ring-[3px] ring-gray-300 dark:ring-gray-700 duration-200 h-[260px] aspect-square bg-gray-100 dark:bg-gray-900 rounded-2xl overflow-hidden">
|
||||
<div class="flex flex-col gap-2 w-full items-center justify-center">
|
||||
<p class="text-gray-500 max-w-[80%] text-center mx-auto text-2xl font-title">
|
||||
<strong>Create</strong> or join a Nestri Party 🎈
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-full [mask-image:linear-gradient(0deg,transparent,#000_30px)] justify-center items-center p-0.5 py-1 pb-0 flex flex-col-reverse">
|
||||
<div class="rounded-2xl rounded-b-none pt-2 px-2 pb-6 bg-white dark:bg-black relative z-[4] flex flex-col gap-2 ring-2 ring-gray-300 dark:ring-gray-700 -mb-4 w-[calc(100%-10px)]">
|
||||
<div class="flex absolute py-2 px-1 gap-0.5" >
|
||||
<span class="size-2.5 rounded-full bg-red-500" />
|
||||
<span class="size-2.5 rounded-full bg-blue-500" />
|
||||
<span class="size-2.5 rounded-full bg-green-500" />
|
||||
</div>
|
||||
<div class="mx-auto w-full max-w-max rounded-lg bg-gray-200 dark:bg-gray-800 justify-center items-center px-2 py-1 flex gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 flex-shrink-0" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M2 16c0-2.828 0-4.243.879-5.121C3.757 10 5.172 10 8 10h8c2.828 0 4.243 0 5.121.879C22 11.757 22 13.172 22 16s0 4.243-.879 5.121C20.243 22 18.828 22 16 22H8c-2.828 0-4.243 0-5.121-.879C2 20.243 2 18.828 2 16" opacity=".5" /><path fill="currentColor" d="M6.75 8a5.25 5.25 0 0 1 10.5 0v2.004c.567.005 1.064.018 1.5.05V8a6.75 6.75 0 0 0-13.5 0v2.055a24 24 0 0 1 1.5-.051z" /></svg>
|
||||
<span class="text-gray-500 text-sm">
|
||||
/play/Lqj8a0
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-2xl rounded-b-none pt-1.5 px-2 pb-1 transition-all duration-200 group-hover:ring-primary-500 group-hover:-translate-y-4 bg-white dark:bg-black relative z-[3] flex flex-col gap-2 ring-2 ring-gray-300 dark:ring-gray-700 -mb-4 w-[calc(100%-25px)]">
|
||||
<div class="flex absolute py-2 px-1 gap-0.5" >
|
||||
<span class="size-2.5 rounded-full bg-gray-500 group-hover:bg-primary-300 dark:group-hover:bg-primary-700 transition-all duration-200" />
|
||||
<span class="size-2.5 rounded-full bg-gray-500 group-hover:bg-primary-300 dark:group-hover:bg-primary-700 transition-all duration-200" />
|
||||
<span class="size-2.5 rounded-full bg-gray-500 group-hover:bg-primary-300 dark:group-hover:bg-primary-700 transition-all duration-200" />
|
||||
</div>
|
||||
<div class="mx-auto w-full max-w-max rounded-lg h-max transition-all duration-200 group-hover:text-primary-500 group-hover:bg-primary-200 dark:group-hover:bg-primary-800 bg-gray-200 dark:bg-gray-800 justify-center items-center px-2 py-1 pb-0.5 flex gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 flex-shrink-0 h-full" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M2 16c0-2.828 0-4.243.879-5.121C3.757 10 5.172 10 8 10h8c2.828 0 4.243 0 5.121.879C22 11.757 22 13.172 22 16s0 4.243-.879 5.121C20.243 22 18.828 22 16 22H8c-2.828 0-4.243 0-5.121-.879C2 20.243 2 18.828 2 16" opacity=".5" /><path fill="currentColor" d="M6.75 8a5.25 5.25 0 0 1 10.5 0v2.004c.567.005 1.064.018 1.5.05V8a6.75 6.75 0 0 0-13.5 0v2.055a24 24 0 0 1 1.5-.051z" /></svg>
|
||||
<span class=" text-gray-500 text-sm transition-all duration-200 group-hover:text-primary-500">
|
||||
/play/vgCaA2
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-[18px] rounded-b-none pt-1.5 px-2 pb-1 bg-white dark:bg-black relative z-[2] flex flex-col gap-2 ring-2 ring-gray-300 dark:ring-gray-700 -mb-4 w-[calc(100%-40px)]">
|
||||
<div class="flex absolute py-2 px-1 gap-0.5" >
|
||||
<span class="size-2.5 rounded-full bg-gray-500" />
|
||||
<span class="size-2.5 rounded-full bg-gray-500" />
|
||||
<span class="size-2.5 rounded-full bg-gray-500" />
|
||||
</div>
|
||||
<div class="mx-auto w-full max-w-max rounded-lg flex justify-center items-center bg-gray-200 dark:bg-gray-800 px-2 py-1 gap-1 pb-0.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-3.5 flex-shrink-0" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M2 16c0-2.828 0-4.243.879-5.121C3.757 10 5.172 10 8 10h8c2.828 0 4.243 0 5.121.879C22 11.757 22 13.172 22 16s0 4.243-.879 5.121C20.243 22 18.828 22 16 22H8c-2.828 0-4.243 0-5.121-.879C2 20.243 2 18.828 2 16" opacity=".5" /><path fill="currentColor" d="M6.75 8a5.25 5.25 0 0 1 10.5 0v2.004c.567.005 1.064.018 1.5.05V8a6.75 6.75 0 0 0-13.5 0v2.055a24 24 0 0 1 1.5-.051z" /></svg>
|
||||
<span class="text-gray-500 text-sm max-w-[75%] text-ellipsis whitespace-pre-wrap">
|
||||
/play/I5kzHj
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex cursor-none group items-end">
|
||||
<div class="flex [transform:perspective(700px)] w-[80px] [transform-style:preserve-3d] relative">
|
||||
<p class="relative font-bold text-[200px] dark:text-gray-950 group-hover:text-primary-200 dark:group-hover:text-primary-800 leading-[1em] group-hover:-translate-x-2 transition-all duration-200 text-gray-50 [-webkit-text-stroke-color:theme(colors.primary.500)] [-webkit-text-stroke-width:2px]">
|
||||
3
|
||||
</p>
|
||||
</div>
|
||||
<div class="z-[1] relative group-hover:ring-primary-500 gap-4 flex items-center justify-center flex-col transition-all ring-[3px] ring-gray-300 dark:ring-gray-700 duration-200 h-[260px] aspect-square bg-gray-100 dark:bg-gray-900 rounded-2xl overflow-hidden">
|
||||
<div class="absolute top-0 left-0 bottom-0 right-0 w-full h-full z-[3]">
|
||||
<Cursor client:load class="absolute left-4 top-4" text="Wanjohi" />
|
||||
<Cursor client:load color="#3a9a00" flip class="absolute right-2 top-8" text="Jd" />
|
||||
<Cursor client:load color="#0096c7" class="absolute top-14 right-1/3" text="DatHorse" />
|
||||
<Cursor client:load color="#FF4F01" flip class="hidden transition-all duration-200 absolute top-20 right-6 group-hover:flex" text="You" />
|
||||
</div>
|
||||
<div class="flex z-[2] flex-col gap-2 w-full items-center justify-center">
|
||||
<p class="text-gray-500 max-w-[80%] text-center mx-auto text-2xl font-title">
|
||||
<strong>Play</strong> your game with friends
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-full overflow-hidden flex items-center absolute bottom-1/2 translate-y-2/3 group-hover:translate-y-[62%] transition-all duration-200 justify-center text-gray-500 group-hover:text-primary-500">
|
||||
<svg
|
||||
width="700"
|
||||
height="465"
|
||||
viewBox="0 0 185.20833 123.03125"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1"><linearGradient
|
||||
id="paint0_linear_693_16793"
|
||||
x1="640"
|
||||
y1="0"
|
||||
x2="640"
|
||||
y2="960"
|
||||
gradientUnits="userSpaceOnUse"><stop
|
||||
stop-color="white"
|
||||
stop-opacity="0"
|
||||
id="stop40" /><stop
|
||||
offset="0.177083"
|
||||
stop-color="white"
|
||||
id="stop41" /><stop
|
||||
offset="0.739583"
|
||||
stop-color="white"
|
||||
id="stop42" /><stop
|
||||
offset="1"
|
||||
stop-color="white"
|
||||
stop-opacity="0"
|
||||
id="stop43" /></linearGradient><clipPath
|
||||
id="clip0_693_16793"><rect
|
||||
width="1280"
|
||||
height="960"
|
||||
fill="#ffffff"
|
||||
id="rect43"
|
||||
x="0"
|
||||
y="0" /></clipPath><filter
|
||||
id="filter0_d_693_16793-0"
|
||||
x="374"
|
||||
y="528"
|
||||
width="229"
|
||||
height="230"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="s-rGB"><feFlood
|
||||
flood-opacity="0"
|
||||
result="BackgroundImageFix"
|
||||
id="feFlood34-6" /><feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
id="feColorMatrix34-3" /><feOffset
|
||||
id="feOffset34-2" /><feGaussianBlur
|
||||
stdDeviation="30"
|
||||
id="feGaussianBlur34-3" /><feComposite
|
||||
in2="hardAlpha"
|
||||
operator="out"
|
||||
id="feComposite34-4" /><feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.498039 0 0 0 0 0.811765 0 0 0 0 1 0 0 0 0.5 0"
|
||||
id="feColorMatrix35-7" /><feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_693_16793"
|
||||
id="feBlend35-2" /><feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_693_16793"
|
||||
result="shape"
|
||||
id="feBlend36-5" /></filter><filter
|
||||
id="filter1_d_693_16793-1"
|
||||
x="534.93402"
|
||||
y="-271.39801"
|
||||
width="209.134"
|
||||
height="654.79999"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="s-rGB"><feFlood
|
||||
flood-opacity="0"
|
||||
result="BackgroundImageFix"
|
||||
id="feFlood36-1" /><feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
id="feColorMatrix36-3" /><feOffset
|
||||
id="feOffset36-8" /><feGaussianBlur
|
||||
stdDeviation="30"
|
||||
id="feGaussianBlur36-6" /><feComposite
|
||||
in2="hardAlpha"
|
||||
operator="out"
|
||||
id="feComposite36-7" /><feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.3 0"
|
||||
id="feColorMatrix37-8" /><feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_693_16793"
|
||||
id="feBlend37-1" /><feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_693_16793"
|
||||
result="shape"
|
||||
id="feBlend38-3" /></filter><filter
|
||||
id="filter2_d_693_16793-1"
|
||||
x="535.31598"
|
||||
y="304.94"
|
||||
width="208.367"
|
||||
height="227.076"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="s-rGB"><feFlood
|
||||
flood-opacity="0"
|
||||
result="BackgroundImageFix"
|
||||
id="feFlood38-0" /><feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
id="feColorMatrix38-4" /><feOffset
|
||||
id="feOffset38-7" /><feGaussianBlur
|
||||
stdDeviation="30"
|
||||
id="feGaussianBlur38-6" /><feComposite
|
||||
in2="hardAlpha"
|
||||
operator="out"
|
||||
id="feComposite38-0" /><feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.498039 0 0 0 0 0.811765 0 0 0 0 1 0 0 0 0.5 0"
|
||||
id="feColorMatrix39-5" /><feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_693_16793"
|
||||
id="feBlend39-5" /><feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_693_16793"
|
||||
result="shape"
|
||||
id="feBlend40-9" /></filter><clipPath
|
||||
id="clip0_693_16793-6"><rect
|
||||
width="1280"
|
||||
height="960"
|
||||
fill="#ffffff"
|
||||
id="rect43-9"
|
||||
x="0"
|
||||
y="0" /></clipPath></defs><g
|
||||
id="layer1"><g
|
||||
style="fill:none"
|
||||
id="g5"
|
||||
transform="matrix(0.22977648,0,0,0.22977648,-63.558251,-97.516373)"><g
|
||||
clip-path="url(#clip0_693_16793)"
|
||||
id="g34"
|
||||
transform="translate(0,1.1269769)"><mask
|
||||
id="mask0_693_16793"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="1280"
|
||||
height="960"><rect
|
||||
width="1280"
|
||||
height="960"
|
||||
fill="url(#paint0_linear_693_16793)"
|
||||
id="rect1-5"
|
||||
x="0"
|
||||
y="0" /></mask><g
|
||||
clip-path="url(#clip0_693_16793-6)"
|
||||
id="g34-2"
|
||||
transform="matrix(0.62946008,0,0,0.62946008,276.77306,424.23217)"
|
||||
style="fill:none"><g
|
||||
mask="url(#mask0_693_16793-9)"
|
||||
id="g33"><path
|
||||
d="m 374.298,326.6944 v -16.698 c 0,-4.161 -3.12,-7.602 -7.276,-7.792 -27.473,-1.256 -126.447,-2.398 -187.77,41.383 -2.039,1.457 -3.202,3.827 -3.202,6.333 v 29.704"
|
||||
stroke="currentColor"
|
||||
stroke-width="8"
|
||||
stroke-miterlimit="10"
|
||||
id="path1-0"
|
||||
/><path
|
||||
d="m 905.526,326.6944 v -16.698 c 0,-4.161 3.12,-7.602 7.276,-7.792 27.474,-1.256 126.448,-2.398 187.768,41.383 2.04,1.457 3.2,3.827 3.2,6.333 v 29.704"
|
||||
stroke="currentColor"
|
||||
stroke-width="8"
|
||||
stroke-miterlimit="10"
|
||||
id="path2"
|
||||
/><path
|
||||
d="m 1306.08,1004.1594 c -25.21,-191.48 -78.54,-399.327 -126.04,-523.456 -46.54,-125.091 -169.68,-150.109 -285.052,-150.109 H 384.034 c -115.377,0 -238.51,25.018 -285.048,150.109 -46.5385,125.091 -99.86388082,331.976 -126.0418,523.456 -14.5433,95.26 55.2642,153 117.3156,159.73 62.0512,6.74 136.7072,-16.35 173.5502,-110.65 36.843,-93.34 52.356,-173.21 129.92,-173.21 65.929,0 424.663,0 490.593,0 77.564,0 93.077,79.87 129.917,173.21 36.85,93.33 111.5,117.39 173.55,110.65 62.05,-6.73 132.83,-64.47 118.29,-159.73 z"
|
||||
fill="#ffffff"
|
||||
fill-opacity="0.05"
|
||||
stroke="currentColor"
|
||||
stroke-width="10"
|
||||
stroke-miterlimit="10"
|
||||
id="path3" /><path
|
||||
d="m 349.335,517.7594 h -39.599 c -2.209,0 -4,-1.791 -4,-4 v -39.598 c 0,-18.408 -15.501,-33.909 -33.91,-33.909 -18.408,0 -33.909,15.501 -33.909,33.909 v 39.598 c 0,2.209 -1.791,4 -4,4 h -39.599 c -18.408,0 -33.91,15.501 -33.91,33.909 0,18.408 15.502,33.91 33.91,33.91 h 39.599 c 2.209,0 4,1.791 4,4 v 39.598 c 0,18.408 15.501,33.909 33.909,33.909 18.409,0 33.91,-15.501 33.91,-33.909 v -39.598 c 0,-2.209 1.791,-4 4,-4 h 39.599 c 18.408,0 33.91,-15.502 33.91,-33.91 0,-18.408 -14.533,-33.909 -33.91,-33.909 z"
|
||||
stroke="currentColor"
|
||||
stroke-width="6"
|
||||
stroke-miterlimit="10"
|
||||
id="path4" /><path
|
||||
d="m 441.98,822.9794 c 43.758,0 79.231,-35.476 79.231,-79.233 0,-43.758 -35.473,-79.23 -79.231,-79.23 -43.757,0 -79.23,35.472 -79.23,79.23 0,43.757 35.473,79.233 79.23,79.233 z"
|
||||
stroke="currentColor"
|
||||
opacity="0.3"
|
||||
stroke-width="2"
|
||||
stroke-miterlimit="10"
|
||||
id="path5" /><path
|
||||
d="m 441.42,803.1684 c 32.818,0 59.423,-26.604 59.423,-59.422 0,-32.818 -26.605,-59.423 -59.423,-59.423 -32.819,0 -59.423,26.605 -59.423,59.423 0,32.818 26.604,59.422 59.423,59.422 z"
|
||||
stroke="currentColor"
|
||||
stroke-width="6"
|
||||
stroke-miterlimit="10"
|
||||
id="path6" /><path
|
||||
d="m 639.5,788.3124 c 24.614,0 44.567,-19.953 44.567,-44.566 0,-24.614 -19.953,-44.567 -44.567,-44.567 -24.613,0 -44.566,19.953 -44.566,44.567 0,24.613 19.953,44.566 44.566,44.566 z"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
stroke-miterlimit="10"
|
||||
id="path7" /><path
|
||||
d="m 628.11,739.7604 c 13.935,-6.284 26.114,-2.496 32.619,0.679 0.61,0.297 1.341,-0.01 1.556,-0.653 l 1.902,-5.709 c 0.223,-0.667 -0.084,-1.395 -0.717,-1.704 -8.029,-3.922 -27.092,-10.177 -48.139,4.634 -0.545,0.385 -0.739,1.103 -0.468,1.712 l 4.442,9.998 c 0.299,0.674 1.069,1.001 1.762,0.747 5.084,-1.863 12.772,-3.816 20.742,-2.666 -5.394,0.913 -9.728,2.816 -13.056,4.859 -0.595,0.364 -0.83,1.11 -0.553,1.749 0,0 1.766,4.043 2.731,6.255 0.24,0.552 0.966,0.68 1.379,0.245 1.023,-1.081 2.156,-1.867 3.075,-2.401 4.305,-2.499 10.256,-4.35 18.302,-3.925 0.628,0.033 1.203,-0.358 1.401,-0.955 l 2.033,-6.1 c 0.204,-0.61 -0.032,-1.283 -0.575,-1.626 -5.967,-3.771 -15.156,-6.913 -28.472,-5.124 z"
|
||||
fill="currentColor"
|
||||
opacity="0.5"
|
||||
id="path8"
|
||||
/><path
|
||||
d="m 837.574,822.9694 c 43.758,0 79.23,-35.468 79.23,-79.224 0,-43.757 -35.472,-79.229 -79.23,-79.229 -43.757,0 -79.229,35.472 -79.229,79.229 0,43.756 35.472,79.224 79.229,79.224 z"
|
||||
stroke="currentColor"
|
||||
opacity="0.3"
|
||||
stroke-width="2"
|
||||
stroke-miterlimit="10"
|
||||
id="path9" /><path
|
||||
d="m 838.156,803.7784 c 32.818,0 59.422,-26.604 59.422,-59.422 0,-32.817 -26.604,-59.421 -59.422,-59.421 -32.818,0 -59.423,26.604 -59.423,59.421 0,32.818 26.605,59.422 59.423,59.422 z"
|
||||
stroke="currentColor"
|
||||
stroke-width="6"
|
||||
stroke-miterlimit="10"
|
||||
id="path10" /><path
|
||||
d="m 506.295,479.8024 c 13.031,0 23.788,-11.067 23.788,-24.284 0,-13.216 -10.757,-24.283 -23.788,-24.283 h -35.654 c -13.031,0 -23.787,11.067 -23.787,24.283 0,13.217 10.756,24.284 23.787,24.284 z"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
stroke-miterlimit="10"
|
||||
id="path11"
|
||||
style="fill:none;stroke:currentColor;stroke-opacity:1" /><path
|
||||
d="m 478.565,455.3004 c 0,2.735 -2.217,4.952 -4.952,4.952 -2.735,0 -4.952,-2.217 -4.952,-4.952 0,-2.735 2.217,-4.952 4.952,-4.952 2.735,0 4.952,2.217 4.952,4.952 z"
|
||||
fill="currentColor"
|
||||
opacity="0.5"
|
||||
id="path12"
|
||||
/><path
|
||||
d="m 493.42,455.3004 c 0,2.735 -2.217,4.952 -4.951,4.952 -2.735,0 -4.952,-2.217 -4.952,-4.952 0,-2.735 2.217,-4.952 4.952,-4.952 2.734,0 4.951,2.217 4.951,4.952 z"
|
||||
fill="currentColor"
|
||||
opacity="0.5"
|
||||
id="path13"
|
||||
/><path
|
||||
d="m 508.276,455.3004 c 0,2.735 -2.217,4.952 -4.952,4.952 -2.735,0 -4.952,-2.217 -4.952,-4.952 0,-2.735 2.217,-4.952 4.952,-4.952 2.735,0 4.952,2.217 4.952,4.952 z"
|
||||
fill="currentColor"
|
||||
opacity="0.5"
|
||||
id="path14"
|
||||
/><path
|
||||
d="m 545.415,582.3724 c 16.146,0 29.235,-13.089 29.235,-29.235 0,-16.147 -13.089,-29.236 -29.235,-29.236 -16.146,0 -29.235,13.089 -29.235,29.236 0,16.146 13.089,29.235 29.235,29.235 z"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
stroke-miterlimit="10"
|
||||
id="path15" /><path
|
||||
d="m 559.135,548.7664 c -0.602,0 -1.119,-0.204 -1.552,-0.613 -0.41,-0.434 -0.614,-0.951 -0.614,-1.553 0,-0.602 0.204,-1.107 0.614,-1.516 0.433,-0.434 0.95,-0.65 1.552,-0.65 0.602,0 1.108,0.216 1.517,0.65 0.433,0.409 0.65,0.914 0.65,1.516 0,0.602 -0.217,1.119 -0.65,1.553 -0.409,0.409 -0.915,0.613 -1.517,0.613 z m -6.499,7.222 c -1.204,0 -2.227,-0.421 -3.069,-1.264 -0.843,-0.842 -1.264,-1.865 -1.264,-3.069 0,-1.204 0.421,-2.227 1.264,-3.069 0.842,-0.843 1.865,-1.264 3.069,-1.264 1.204,0 2.227,0.421 3.069,1.264 0.843,0.842 1.264,1.865 1.264,3.069 0,1.204 -0.421,2.227 -1.264,3.069 -0.842,0.843 -1.865,1.264 -3.069,1.264 z m 0,11.554 c -1.396,0 -2.588,-0.493 -3.575,-1.48 -0.987,-0.987 -1.48,-2.179 -1.48,-3.575 0,-1.396 0.493,-2.587 1.48,-3.574 0.987,-0.987 2.179,-1.481 3.575,-1.481 1.396,0 2.588,0.494 3.575,1.481 0.987,0.987 1.48,2.178 1.48,3.574 0,1.396 -0.493,2.588 -1.48,3.575 -0.987,0.987 -2.179,1.48 -3.575,1.48 z m -14.443,-11.554 c -2.407,0 -4.453,-0.843 -6.138,-2.528 -1.685,-1.685 -2.528,-3.731 -2.528,-6.138 0,-2.407 0.843,-4.453 2.528,-6.138 1.685,-1.685 3.731,-2.528 6.138,-2.528 2.407,0 4.453,0.843 6.138,2.528 1.685,1.685 2.528,3.731 2.528,6.138 0,2.407 -0.843,4.453 -2.528,6.138 -1.685,1.685 -3.731,2.528 -6.138,2.528 z"
|
||||
fill="currentColor"
|
||||
opacity="0.5"
|
||||
id="path16" /><path
|
||||
d="m 732.802,582.3714 c 16.146,0 29.235,-13.089 29.235,-29.235 0,-16.146 -13.089,-29.235 -29.235,-29.235 -16.146,0 -29.236,13.089 -29.236,29.235 0,16.146 13.09,29.235 29.236,29.235 z"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
stroke-miterlimit="10"
|
||||
id="path17" /><path
|
||||
d="m 719.349,566.0984 v -9.145 h 3.249 v 5.895 h 5.896 v 3.25 z m 0,-16.898 v -9.099 h 9.145 v 3.249 h -5.896 v 5.85 z m 16.898,16.898 v -3.25 h 5.849 v -5.895 h 3.25 v 9.145 z m 5.849,-16.898 v -5.85 h -5.849 v -3.249 h 9.099 v 9.099 z"
|
||||
fill="currentColor"
|
||||
opacity="0.5"
|
||||
id="path18" /><path
|
||||
d="m 808.203,479.8024 c 13.031,0 23.788,-11.067 23.788,-24.284 0,-13.216 -10.757,-24.283 -23.788,-24.283 H 772.55 c -13.031,0 -23.788,11.067 -23.788,24.283 0,13.217 10.757,24.284 23.788,24.284 z"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
stroke-miterlimit="10"
|
||||
id="path19" /><path
|
||||
d="m 774.747,465.8234 v -3.157 h 31.568 v 3.157 z m 0,-8.944 v -3.157 h 31.568 v 3.157 z m 0,-8.945 v -3.156 h 31.568 v 3.156 z"
|
||||
fill="currentColor"
|
||||
opacity="0.5"
|
||||
id="path20" /><path
|
||||
d="m 1019.24,676.4204 c 22.99,0 41.62,-18.632 41.62,-41.615 0,-22.983 -18.63,-41.615 -41.62,-41.615 -22.97898,0 -41.611,18.632 -41.611,41.615 0,22.983 18.63202,41.615 41.611,41.615 z"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
stroke-miterlimit="10"
|
||||
id="path21" /><path
|
||||
d="m 1006.98,648.4234 10.69,-28.365 h 4.95 l 10.74,28.365 h -4.79 l -2.62,-7.29 h -11.57 l -2.61,7.29 z m 17.51,-11.33 -3.13,-8.676 -1.07,-3.248 h -0.24 l -1.07,3.248 -3.13,8.676 z"
|
||||
fill="currentColor"
|
||||
opacity="0.5"
|
||||
id="path22" /><path
|
||||
d="m 935.374,592.1454 c 22.983,0 41.614,-18.632 41.614,-41.615 0,-22.983 -18.631,-41.615 -41.614,-41.615 -22.984,0 -41.615,18.632 -41.615,41.615 0,22.983 18.631,41.615 41.615,41.615 z"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
stroke-miterlimit="10"
|
||||
id="path23" /><path
|
||||
d="m 923.628,535.8774 h 5.348 l 6.299,10.181 h 0.237 l 6.339,-10.181 h 5.308 l -8.834,13.627 9.468,14.737 h -5.308 l -6.973,-11.052 h -0.237 l -6.972,11.052 h -5.309 l 9.468,-14.737 z"
|
||||
fill="currentColor"
|
||||
opacity="0.5"
|
||||
id="path24" /><path
|
||||
d="m 1019.24,508.0574 c 22.99,0 41.62,-18.632 41.62,-41.615 0,-22.983 -18.63,-41.615 -41.62,-41.615 -22.97898,0 -41.611,18.632 -41.611,41.615 0,22.983 18.63202,41.615 41.611,41.615 z"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
stroke-miterlimit="10"
|
||||
id="path25" /><path
|
||||
d="m 1017.49,479.9514 v -13.152 l -9.59,-15.212 h 5.15 l 6.46,10.696 h 0.24 l 6.3,-10.696 h 5.19 l -9.43,15.212 v 13.152 z"
|
||||
fill="currentColor"
|
||||
opacity="0.5"
|
||||
id="path26" /><path
|
||||
d="m 1103.74,592.1454 c 22.98,0 41.61,-18.632 41.61,-41.615 0,-22.983 -18.63,-41.615 -41.61,-41.615 -22.99,0 -41.62,18.632 -41.62,41.615 0,22.983 18.63,41.615 41.62,41.615 z"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
stroke-miterlimit="10"
|
||||
id="path27" /><path
|
||||
d="m 1095.17,564.2414 v -28.364 h 10.61 c 1.56,0 2.97,0.33 4.24,0.99 1.27,0.661 2.27,1.558 3.01,2.694 0.77,1.136 1.15,2.417 1.15,3.843 0,1.452 -0.36,2.694 -1.07,3.724 -0.71,1.03 -1.64,1.809 -2.77,2.337 v 0.198 c 1.42,0.475 2.59,1.294 3.48,2.456 0.9,1.162 1.35,2.549 1.35,4.16 0,1.584 -0.41,2.971 -1.23,4.159 -0.79,1.189 -1.87,2.126 -3.24,2.813 -1.35,0.66 -2.83,0.99 -4.44,0.99 z m 4.35,-12.558 v 8.517 h 6.74 c 0.95,0 1.77,-0.198 2.45,-0.594 0.69,-0.396 1.21,-0.924 1.55,-1.584 0.37,-0.661 0.55,-1.347 0.55,-2.06 0,-0.766 -0.18,-1.466 -0.55,-2.1 -0.37,-0.66 -0.91,-1.188 -1.62,-1.585 -0.69,-0.396 -1.54,-0.594 -2.54,-0.594 z m 0,-3.882 h 6.06 c 0.93,0 1.71,-0.185 2.34,-0.555 0.66,-0.396 1.16,-0.898 1.51,-1.505 0.34,-0.634 0.51,-1.281 0.51,-1.941 0,-0.66 -0.17,-1.281 -0.51,-1.862 -0.32,-0.607 -0.79,-1.096 -1.43,-1.466 -0.63,-0.396 -1.39,-0.594 -2.26,-0.594 h -6.22 z"
|
||||
fill="currentColor"
|
||||
opacity="0.5"
|
||||
id="path28" /><g
|
||||
filter="url(#filter1_d_693_16793)"
|
||||
id="g31"
|
||||
style="filter:url(#filter1_d_693_16793-1)"
|
||||
transform="translate(1.8206821e-7,-187.9906)" /><g
|
||||
filter="url(#filter2_d_693_16793)"
|
||||
id="g32"
|
||||
style="filter:url(#filter2_d_693_16793-1)"
|
||||
transform="translate(1.8206821e-7,-187.9906)" /></g><g
|
||||
filter="url(#filter0_d_693_16793)"
|
||||
id="g1"
|
||||
style="filter:url(#filter0_d_693_16793-0)" /><mask
|
||||
id="mask0_693_16793-9"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="1280"
|
||||
height="960"><rect
|
||||
width="1280"
|
||||
height="960"
|
||||
fill="url(#paint0_linear_693_16793)"
|
||||
id="rect1-6"
|
||||
x="0"
|
||||
y="0" />
|
||||
</mask>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MotionComponent>
|
||||
</section>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
export const head: DocumentHead = {
|
||||
title: "Welcome to Qwik",
|
||||
meta: [
|
||||
{
|
||||
name: "description",
|
||||
content: "Qwik site description",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
335
apps/www/src/routes/pricing/index.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { Link } from "@builder.io/qwik-city";
|
||||
import { TitleSection, MotionComponent, transition } from "@nestri/ui/react";
|
||||
import { TeamCounter, NavBar, Footer } from "@nestri/ui"
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
<>
|
||||
<NavBar />
|
||||
<TitleSection client:load title="Pricing" description={["We're growing at the speed of trust.", "Start free, then choose a price that feels right for you."]} />
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={transition}
|
||||
client:load
|
||||
class="flex items-center justify-center w-full"
|
||||
as="div"
|
||||
>
|
||||
<div class="px-2" >
|
||||
<section class="flex flex-col gap-4 justify-center items-center mx-auto w-full text-left max-w-xl pt-20 pb-4">
|
||||
<div class="flex flex-col gap-4 justify-center items-center">
|
||||
<div class="flex sm:flex-row flex-col w-[90%] sm:w-full h-min p-1.5 overflow-hidden bg-gray-200/70 ring-2 ring-gray-300 dark:ring-gray-700 dark:bg-gray-800/70 rounded-xl gap-4">
|
||||
<div class="gap-3 w-full p-6 flex flex-col rounded-lg bg-white dark:bg-black">
|
||||
<div class="flex items-center font-title h-min w-full justify-between">
|
||||
<div class="flex items-center justify-center gap-2 ">
|
||||
<div class="bg-gradient-to-t from-[#d596ff] to-[rgb(145,147,255)] rounded-full h-4 w-4" />
|
||||
<p class="text-base font-semibold">Basic</p>
|
||||
</div>
|
||||
<p class="text-base font-medium">Free</p>
|
||||
</div>
|
||||
<div class="break-words [word-break:break-word] [text-wrap:balance] [word-wrap:break-word] w-full relative whitespace-pre-wrap">
|
||||
<p class="text-base text-primary-950/70 dark:text-primary-50/70">
|
||||
Perfect for casual gamers and those new to Nestri. Dive into cloud gaming without spending a dime.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-1.5">
|
||||
<p class="text-base font-medium font-title"> Your Team </p>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="bg-primary-200/70 dark:bg-primary-800/70 flex rounded-full py-[3px] pr-4 pl-[3px] justify-center items-center gap-2">
|
||||
<div class="bg-gradient-to-t from-primary-400 to-primary-600 rounded-full aspect-square relative overflow-hidden flex justify-center items-center" >
|
||||
<p class="text-sm font-medium text-primary-50 text-center p-2">
|
||||
Y
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-sm font-medium text-primary-500">
|
||||
You
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-gray-200/70 dark:bg-gray-800 flex rounded-full relative size-[32px] overflow-hidden items-center justify-center">
|
||||
<p class="text-lg font-normal font-title">+1</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="h-[2px] bg-gray-200 dark:bg-gray-800" />
|
||||
<div class="w-full relative sm:text-sm text-base gap-3 flex flex-col">
|
||||
<div class="flex item-center flex-col gap-2 w-full">
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-full h-full" viewBox="0 0 24 24"><path fill="currentColor" d="M8.21 17.32L7 16.8a2.13 2.13 0 1 0 1.17-2.93l1.28.53a1.58 1.58 0 0 1-1.22 2.92z" /><path fill="currentColor" d="M12 2a10 10 0 0 0-10 9.34l5.38 2.21a2.31 2.31 0 0 1 .47-.24A2.62 2.62 0 0 1 9 13.1l2.44-3.56a3.8 3.8 0 1 1 3.8 3.8h-.08l-3.51 2.5a2.77 2.77 0 0 1-5.47.68l-3.77-1.6A10 10 0 1 0 12 2" /><path fill="currentColor" d="M17.79 9.5a2.53 2.53 0 1 0-2.53 2.5a2.54 2.54 0 0 0 2.53-2.5m-4.42 0a1.9 1.9 0 1 1 1.9 1.91a1.9 1.9 0 0 1-1.9-1.92z" /></svg>
|
||||
</div>
|
||||
<p class="group relative">Add upto <span class="">3 games</span> at a time</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-full h-full" viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="currentColor" stroke-width="1.5"><path d="M7 10c0-1.414 0-2.121.44-2.56C7.878 7 8.585 7 10 7h4c1.414 0 2.121 0 2.56.44c.44.439.44 1.146.44 2.56v4c0 1.414 0 2.121-.44 2.56c-.439.44-1.146.44-2.56.44h-4c-1.414 0-2.121 0-2.56-.44C7 16.122 7 15.415 7 14z" opacity=".5" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12.429 10L11 12h2l-1.429 2" />
|
||||
<path d="M4 12c0-3.771 0-5.657 1.172-6.828C6.343 4 8.229 4 12 4c3.771 0 5.657 0 6.828 1.172C20 6.343 20 8.229 20 12c0 3.771 0 5.657-1.172 6.828C17.657 20 15.771 20 12 20c-3.771 0-5.657 0-6.828-1.172C4 17.657 4 15.771 4 12Z" />
|
||||
<path stroke-linecap="round" d="M4 12H2m20 0h-2M4 9H2m20 0h-2M4 15H2m20 0h-2m-8 5v2m0-20v2M9 20v2M9 2v2m6 16v2m0-20v2" opacity=".5" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<p>Basic <span class="">datacenter</span> GPU</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-full h-full" viewBox="0 0 24 24"><path fill="currentColor" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10" opacity=".5" /><path fill="currentColor" fill-rule="evenodd" d="M12 7.25a.75.75 0 0 1 .75.75v3.69l2.28 2.28a.75.75 0 1 1-1.06 1.06l-2.5-2.5a.75.75 0 0 1-.22-.53V8a.75.75 0 0 1 .75-.75" clip-rule="evenodd" /></svg>
|
||||
</div>
|
||||
<p><span class="">3 hours</span> of daily playtime</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-full h-full" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="6" r="4" /><path stroke-linecap="round" d="M18 9c1.657 0 3-1.12 3-2.5S19.657 4 18 4M6 9C4.343 9 3 7.88 3 6.5S4.343 4 6 4" opacity=".5" /><ellipse cx="12" cy="17" rx="6" ry="4" /><path stroke-linecap="round" d="M20 19c1.754-.385 3-1.359 3-2.5s-1.246-2.115-3-2.5M4 19c-1.754-.385-3-1.359-3-2.5s1.246-2.115 3-2.5" opacity=".5" /></g></svg>
|
||||
</div>
|
||||
<p>Invite upto <span class="">3 play buddies</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
|
||||
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p><span class="">Cross-play</span> with home server</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
|
||||
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p>Frame Rates upto <span class="">120 fps</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
|
||||
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p>Video quality upto <span class="">4k UHD</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
|
||||
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p><span class="">Unlimited</span> cloud saves</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
|
||||
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p>Unlimited <span class="">State Shares</span> </p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
|
||||
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p><span class="">Game mod</span> support</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
|
||||
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p><span class="">Stream</span> to Youtube/Twitch</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-3 w-full p-6 flex flex-col rounded-lg">
|
||||
<div class="flex items-center font-title h-min w-full justify-between">
|
||||
<div class="flex items-center justify-center gap-2 ">
|
||||
<div class="bg-gradient-to-t from-[#685fea] to-[rgb(153,148,224)] rounded-full h-4 w-4" />
|
||||
<h1 class="text-base font-semibold">Pro</h1>
|
||||
</div>
|
||||
{/**FIXME: Add a ticker for pricing, when we figure it out */}
|
||||
<h2 class="text-base font-medium">TBD</h2>
|
||||
</div>
|
||||
<div class="break-words [word-break:break-word] [text-wrap:balance] [word-wrap:break-word] w-full relative whitespace-pre-wrap">
|
||||
<p class="text-base text-primary-950/70 dark:text-primary-50/70">
|
||||
Ideal for dedicated gamers who crave more power, flexibility, and social gaming experiences.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-1.5">
|
||||
<p class="text-base font-medium font-title">Your Team </p>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="bg-primary-200/70 dark:bg-primary-800/70 flex rounded-full py-[3px] pr-4 pl-[3px] justify-center items-center gap-2">
|
||||
{/** Avatar Placeholder*/}
|
||||
<div class="bg-gradient-to-t from-primary-400 to-primary-600 rounded-full aspect-square relative overflow-hidden flex justify-center items-center" >
|
||||
<p class="text-sm font-medium text-primary-50 text-center p-2">
|
||||
Y
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-sm font-medium text-primary-500">
|
||||
You
|
||||
</p>
|
||||
</div>
|
||||
<TeamCounter class="h-[30px]" />
|
||||
</div>
|
||||
</div>
|
||||
<hr class="h-[2px] bg-gray-300 dark:bg-gray-700" />
|
||||
<div class="w-full sm:text-sm text-base relative gap-3 flex flex-col">
|
||||
<div class="flex item-center flex-col gap-2 w-full">
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-full h-full" viewBox="0 0 24 24"><path fill="currentColor" d="M8.21 17.32L7 16.8a2.13 2.13 0 1 0 1.17-2.93l1.28.53a1.58 1.58 0 0 1-1.22 2.92z" /><path fill="currentColor" d="M12 2a10 10 0 0 0-10 9.34l5.38 2.21a2.31 2.31 0 0 1 .47-.24A2.62 2.62 0 0 1 9 13.1l2.44-3.56a3.8 3.8 0 1 1 3.8 3.8h-.08l-3.51 2.5a2.77 2.77 0 0 1-5.47.68l-3.77-1.6A10 10 0 1 0 12 2" /><path fill="currentColor" d="M17.79 9.5a2.53 2.53 0 1 0-2.53 2.5a2.54 2.54 0 0 0 2.53-2.5m-4.42 0a1.9 1.9 0 1 1 1.9 1.91a1.9 1.9 0 0 1-1.9-1.92z" /></svg>
|
||||
</div>
|
||||
<p>Add upto <span class="">7 games</span></p>
|
||||
<div class="py-0.5 text-xs font-title rounded-full bg-gray-300 dark:bg-gray-700 px-1.5" >
|
||||
<p>+$3/game</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-full h-full" viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="currentColor" stroke-width="1.5"><path d="M7 10c0-1.414 0-2.121.44-2.56C7.878 7 8.585 7 10 7h4c1.414 0 2.121 0 2.56.44c.44.439.44 1.146.44 2.56v4c0 1.414 0 2.121-.44 2.56c-.439.44-1.146.44-2.56.44h-4c-1.414 0-2.121 0-2.56-.44C7 16.122 7 15.415 7 14z" opacity=".5" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12.429 10L11 12h2l-1.429 2" />
|
||||
<path d="M4 12c0-3.771 0-5.657 1.172-6.828C6.343 4 8.229 4 12 4c3.771 0 5.657 0 6.828 1.172C20 6.343 20 8.229 20 12c0 3.771 0 5.657-1.172 6.828C17.657 20 15.771 20 12 20c-3.771 0-5.657 0-6.828-1.172C4 17.657 4 15.771 4 12Z" />
|
||||
<path stroke-linecap="round" d="M4 12H2m20 0h-2M4 9H2m20 0h-2M4 15H2m20 0h-2m-8 5v2m0-20v2M9 20v2M9 2v2m6 16v2m0-20v2" opacity=".5" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<p>Premium <span class="">consumer</span> GPU</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-full h-full" viewBox="0 0 24 24"><path fill="currentColor" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10" opacity=".5" /><path fill="currentColor" fill-rule="evenodd" d="M12 7.25a.75.75 0 0 1 .75.75v3.69l2.28 2.28a.75.75 0 1 1-1.06 1.06l-2.5-2.5a.75.75 0 0 1-.22-.53V8a.75.75 0 0 1 .75-.75" clip-rule="evenodd" /></svg>
|
||||
</div>
|
||||
<p><span class="">Unlimited</span> daily playtime</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-full h-full" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="6" r="4" /><path stroke-linecap="round" d="M18 9c1.657 0 3-1.12 3-2.5S19.657 4 18 4M6 9C4.343 9 3 7.88 3 6.5S4.343 4 6 4" opacity=".5" /><ellipse cx="12" cy="17" rx="6" ry="4" /><path stroke-linecap="round" d="M20 19c1.754-.385 3-1.359 3-2.5s-1.246-2.115-3-2.5M4 19c-1.754-.385-3-1.359-3-2.5s1.246-2.115 3-2.5" opacity=".5" /></g></svg>
|
||||
</div>
|
||||
<p>Invite upto <span class="">9 play buddies</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
|
||||
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p><span class="">Cross-play</span> with home server</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
|
||||
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p>Frame Rates upto <span class="">120 fps</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
|
||||
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p>Video quality upto <span class="">4k UHD</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
|
||||
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p><span class="">Unlimited</span> cloud saves</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
|
||||
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p>Unlimited <span class="">State Shares</span> </p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
|
||||
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p><span class="">Game mod</span> support</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2.5 flex relative items-center w-full" >
|
||||
<div class="gap-1.5 flex w-full items-center text-neutral-800/70 dark:text-neutral-200/70" >
|
||||
<div class="size-5 relative">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-full h-full">
|
||||
<path fill-rule="evenodd" d="M2 10a.75.75 0 01.75-.75h12.59l-2.1-1.95a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.1-1.95H2.75A.75.75 0 012 10z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p><span class="">Stream</span> to Youtube/Twitch</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-200/70 dark:bg-gray-800/70 ring-2 ring-gray-300 dark:ring-gray-700 rounded-xl p-6 w-[90%] sm:w-full" >
|
||||
<div class="flex gap-3 relative w-full flex-col" >
|
||||
<div class="w-full flex items-center gap-2" >
|
||||
<div class="rounded-full size-4 overflow-hidden bg-gradient-to-tr from-[#a0f906] to-[#e60d0d]" />
|
||||
<p class="text-base font-medium">Enterprise</p>
|
||||
</div>
|
||||
<p class="text-neutral-800/70 dark:text-neutral-200/70 text-base" >
|
||||
Looking for a custom cloud gaming platform? Use Nestri as your own on our servers or yours. Flexible licensing and white-glove onboarding included.
|
||||
</p>
|
||||
<Link class="underline underline-offset-1 font-medium font-title hover:opacity-70" href="mailto:enterprise@nestri.io">
|
||||
Let's Chat
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</MotionComponent>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -11,8 +11,51 @@ import { setupServiceWorker } from "@builder.io/qwik-city/service-worker";
|
||||
|
||||
setupServiceWorker();
|
||||
|
||||
addEventListener("install", () => self.skipWaiting());
|
||||
const IMAGE_CACHE_NAME = 'image-cache-v1';
|
||||
|
||||
// Install event: Open a cache
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(IMAGE_CACHE_NAME)
|
||||
);
|
||||
});
|
||||
|
||||
addEventListener("activate", () => self.clients.claim());
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
if (event.request.destination === 'image') {
|
||||
event.respondWith(
|
||||
caches.open(IMAGE_CACHE_NAME).then((cache) => {
|
||||
console.log('cache', cache);
|
||||
return cache.match(event.request).then((response) => {
|
||||
console.log('response', response);
|
||||
if (response) {
|
||||
// If image is in cache, return it
|
||||
return response;
|
||||
}
|
||||
|
||||
// If not in cache, fetch from network, cache it, then return
|
||||
return fetch(event.request).then((networkResponse) => {
|
||||
cache.put(event.request, networkResponse.clone());
|
||||
return networkResponse;
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Add a message event listener
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'CACHE_IMAGES') {
|
||||
event.waitUntil(
|
||||
caches.open(IMAGE_CACHE_NAME).then((cache) => {
|
||||
return Promise.all(event.data.urls.map((url: RequestInfo | URL) =>
|
||||
cache.add(url).catch(error => console.error('Failed to cache:', url, error))
|
||||
));
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
declare const self: ServiceWorkerGlobalScope;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import baseConfig from "@nestri/ui/tailwind.config";
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
|
||||
content: [
|
||||
"./{src,components,app}/**/*.{ts,tsx,html}",
|
||||
"../../packages/ui/src/**/*.{ts,tsx}",
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
{
|
||||
"extends": "@nestri/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"target": "ES2017",
|
||||
"module": "ES2022",
|
||||
"lib": ["es2022", "DOM", "WebWorker", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@builder.io/qwik",
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"incremental": true,
|
||||
"isolatedModules": true,
|
||||
"outDir": "tmp",
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": ["./.eslintrc.cjs"],
|
||||
"include": ["src", "./*.d.ts", "./*.config.ts", "./*.config.js", "postcss.config.cjs"]
|
||||
}
|
||||
"files": [
|
||||
".eslintrc.js"
|
||||
],
|
||||
"include": [
|
||||
"src",
|
||||
"./*.d.ts",
|
||||
"./*.config.ts",
|
||||
"./*.config.js"
|
||||
]
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { qwikVite } from "@builder.io/qwik/optimizer";
|
||||
import { qwikCity } from "@builder.io/qwik-city/vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import pkg from "./package.json";
|
||||
|
||||
import { qwikReact } from "@builder.io/qwik-react/vite";
|
||||
type PkgDep = Record<string, string>;
|
||||
const { dependencies = {}, devDependencies = {} } = pkg as any as {
|
||||
dependencies: PkgDep;
|
||||
@@ -15,20 +15,19 @@ const { dependencies = {}, devDependencies = {} } = pkg as any as {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
errorOnDuplicatesPkgDeps(devDependencies, dependencies);
|
||||
|
||||
/**
|
||||
* Note that Vite normally starts from `index.html` but the qwikCity plugin makes start at `src/entry.ssr.tsx` instead.
|
||||
*/
|
||||
|
||||
export default defineConfig(({ command, mode }): UserConfig => {
|
||||
return {
|
||||
plugins: [qwikCity(), qwikVite(), tsconfigPaths()],
|
||||
plugins: [qwikCity({ trailingSlash: false }), qwikVite(), tsconfigPaths(), qwikReact()],
|
||||
// This tells Vite which dependencies to pre-build in dev mode.
|
||||
optimizeDeps: {
|
||||
// Put problematic deps that break bundling here, mostly those with binaries.
|
||||
// For example ['better-sqlite3'] if you use that in server functions.
|
||||
exclude: [],
|
||||
},
|
||||
|
||||
/**
|
||||
* This is an advanced setting. It improves the bundling of your server code. To use it, make sure you understand when your consumed packages are dependencies or dev dependencies. (otherwise things will break in production)
|
||||
*/
|
||||
@@ -45,7 +44,6 @@ export default defineConfig(({ command, mode }): UserConfig => {
|
||||
// external: Object.keys(dependencies),
|
||||
// }
|
||||
// : undefined,
|
||||
|
||||
server: {
|
||||
headers: {
|
||||
// Don't cache the server response in dev mode
|
||||
@@ -60,9 +58,7 @@ export default defineConfig(({ command, mode }): UserConfig => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// *** utils ***
|
||||
|
||||
/**
|
||||
* Function to identify duplicate dependencies and throw an error
|
||||
* @param {Object} devDependencies - List of development dependencies
|
||||
@@ -78,27 +74,22 @@ function errorOnDuplicatesPkgDeps(
|
||||
const duplicateDeps = Object.keys(devDependencies).filter(
|
||||
(dep) => dependencies[dep],
|
||||
);
|
||||
|
||||
// include any known qwik packages
|
||||
const qwikPkg = Object.keys(dependencies).filter((value) =>
|
||||
/qwik/i.test(value),
|
||||
);
|
||||
|
||||
// any errors for missing "qwik-city-plan"
|
||||
// [PLUGIN_ERROR]: Invalid module "@qwik-city-plan" is not a valid package
|
||||
msg = `Move qwik packages ${qwikPkg.join(", ")} to devDependencies`;
|
||||
|
||||
if (qwikPkg.length > 0) {
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
// Format the error message with the duplicates list.
|
||||
// The `join` function is used to represent the elements of the 'duplicateDeps' array as a comma-separated string.
|
||||
msg = `
|
||||
Warning: The dependency "${duplicateDeps.join(", ")}" is listed in both "devDependencies" and "dependencies".
|
||||
Please move the duplicated dependencies to "devDependencies" only and remove it from "dependencies"
|
||||
`;
|
||||
|
||||
// Throw an error with the constructed message.
|
||||
if (duplicateDeps.length > 0) {
|
||||
throw new Error(msg);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.2.5",
|
||||
"turbo": "^2.0.14",
|
||||
"turbo": "^2.0.12",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"engines": {
|
||||
@@ -19,5 +19,10 @@
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"trustedDependencies": [
|
||||
"core-js-pure",
|
||||
"esbuild",
|
||||
"workerd"
|
||||
]
|
||||
}
|
||||
|
||||
47
packages/cache/caches.ts
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
const buildCacheKey = (namespace: string) => (key: string) => {
|
||||
return `${namespace}:${key}`;
|
||||
};
|
||||
|
||||
export interface KVResponseCache {
|
||||
match(key: string): Promise<Response | null>;
|
||||
put(key: string, res: Response, options?: KVNamespacePutOptions): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
export const kvResponseCache =
|
||||
(kv: KVNamespace) =>
|
||||
(cacheName: string): KVResponseCache => {
|
||||
const cacheKey = buildCacheKey(cacheName);
|
||||
|
||||
return {
|
||||
async match(key: string) {
|
||||
const [headers, status, body] = await Promise.all([
|
||||
kv.get(cacheKey(`${key}:headers`)),
|
||||
kv.get(cacheKey(`${key}:status`)),
|
||||
kv.get(cacheKey(`${key}:body`), "stream"),
|
||||
]);
|
||||
|
||||
if (headers === null || body === null || status === null) return null;
|
||||
|
||||
return new Response(body, { headers: JSON.parse(headers), status: parseInt(status, 10) });
|
||||
},
|
||||
async put(key: string, res: Response, options?: KVNamespacePutOptions) {
|
||||
const headers = Array.from(res.headers.entries()).reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
|
||||
const body = res.body;
|
||||
if (body === null) return;
|
||||
|
||||
await Promise.all([
|
||||
kv.put(cacheKey(`${key}:headers`), JSON.stringify(headers), options),
|
||||
kv.put(cacheKey(`${key}:status`), `${res.status}`, options),
|
||||
kv.put(cacheKey(`${key}:body`), body, options),
|
||||
]);
|
||||
},
|
||||
async delete(key: string) {
|
||||
await Promise.all([
|
||||
kv.delete(cacheKey(`${key}:headers`)),
|
||||
kv.delete(cacheKey(`${key}:status`)),
|
||||
kv.delete(cacheKey(`${key}:body`)),
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
||||
8
packages/cache/index.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
//copied from https://github.com/napolab/kv-response-cache with some minor changes
|
||||
import { kvResponseCache } from "./caches";
|
||||
import { kvCaches, defaultGetCacheKey } from "./middleware";
|
||||
|
||||
import type { KVResponseCache } from "./caches";
|
||||
|
||||
export type { KVResponseCache };
|
||||
export { kvResponseCache, kvCaches, defaultGetCacheKey as getCacheKey };
|
||||
47
packages/cache/middleware.ts
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
import { kvResponseCache } from "./caches";
|
||||
|
||||
import type { Filter } from "./types";
|
||||
import type { Context, Env, MiddlewareHandler } from "hono";
|
||||
|
||||
type Namespace<E extends Env> = string | ((c: Context<E>) => string);
|
||||
interface GetCacheKey<E extends Env> {
|
||||
(c: Context<E>): string;
|
||||
}
|
||||
|
||||
type KVCacheOption<E extends Env & { Bindings: Record<string, unknown> }> = {
|
||||
key: keyof E["Bindings"];
|
||||
namespace: Namespace<E>;
|
||||
getCacheKey?: GetCacheKey<E>;
|
||||
options?: KVNamespacePutOptions;
|
||||
};
|
||||
|
||||
export const defaultGetCacheKey = <E extends Env>(c: Context<E>) => c.req.url;
|
||||
|
||||
export const kvCaches =
|
||||
<E extends Env & { Bindings: Record<string, unknown> }>({
|
||||
key: bindingKey,
|
||||
namespace,
|
||||
options,
|
||||
getCacheKey = defaultGetCacheKey,
|
||||
}: KVCacheOption<E>): MiddlewareHandler<E> =>
|
||||
async (c, next) => {
|
||||
const kv: KVNamespace = c.env?.[bindingKey] as KVNamespace;
|
||||
const kvNamespace = typeof namespace === "function" ? namespace(c) : namespace;
|
||||
|
||||
const kvCaches = kvResponseCache(kv);
|
||||
const cache = kvCaches(kvNamespace);
|
||||
|
||||
const key = getCacheKey(c);
|
||||
const response = await cache.match(key);
|
||||
if (response) {
|
||||
response.headers.set("X-KV-CACHE", "hit");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
await next();
|
||||
|
||||
if (c.res.status >= 200 && c.res.status < 300) {
|
||||
c.executionCtx.waitUntil(cache.put(key, c.res.clone(), options));
|
||||
}
|
||||
};
|
||||
13
packages/cache/package.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@nestri/cache",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"exports":{
|
||||
".":"./index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240529.0",
|
||||
"wrangler": "^3.57.2"
|
||||
}
|
||||
}
|
||||
15
packages/cache/tsconfig.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"ESNext"
|
||||
],
|
||||
"types": [
|
||||
"@cloudflare/workers-types/2023-07-01"
|
||||
]
|
||||
},
|
||||
}
|
||||
3
packages/cache/types.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export type Filter<T extends Record<string, unknown>, V> = {
|
||||
[K in keyof T as T[K] extends V ? K : never]: T[K];
|
||||
};
|
||||
73
packages/core/image-brightness-analyzer.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// image-brightness-analyzer.js
|
||||
|
||||
export class ImageBrightnessAnalyzer {
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D ;
|
||||
|
||||
constructor() {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.ctx = this.canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
analyze(imgElement: HTMLImageElement) {
|
||||
if (!(imgElement instanceof HTMLImageElement)) {
|
||||
throw new Error('Input must be an HTMLImageElement');
|
||||
}
|
||||
|
||||
this.canvas.width = imgElement.width;
|
||||
this.canvas.height = imgElement.height;
|
||||
this.ctx.drawImage(imgElement, 0, 0);
|
||||
|
||||
const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
let brightestPixel = { value: 0, x: 0, y: 0 };
|
||||
let dullestPixel = { value: 765, x: 0, y: 0 }; // 765 is the max value (255 * 3)
|
||||
|
||||
for (let y = 0; y < this.canvas.height; y++) {
|
||||
for (let x = 0; x < this.canvas.width; x++) {
|
||||
const index = (y * this.canvas.width + x) * 4;
|
||||
const brightness = data[index] + data[index + 1] + data[index + 2];
|
||||
|
||||
if (brightness > brightestPixel.value) {
|
||||
brightestPixel = { value: brightness, x, y };
|
||||
}
|
||||
if (brightness < dullestPixel.value) {
|
||||
dullestPixel = { value: brightness, x, y };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
brightest: {
|
||||
x: brightestPixel.x,
|
||||
y: brightestPixel.y,
|
||||
color: this.getPixelColor(data, brightestPixel.x, brightestPixel.y)
|
||||
},
|
||||
dullest: {
|
||||
x: dullestPixel.x,
|
||||
y: dullestPixel.y,
|
||||
color: this.getPixelColor(data, dullestPixel.x, dullestPixel.y)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getPixelColor(data: any[] | Uint8ClampedArray, x: number, y: number) {
|
||||
const index = (y * this.canvas.width + x) * 4;
|
||||
return {
|
||||
r: data[index],
|
||||
g: data[index + 1],
|
||||
b: data[index + 2]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// // Export the class for use in browser environments
|
||||
// if (typeof window !== 'undefined') {
|
||||
// window.ImageBrightnessAnalyzer = ImageBrightnessAnalyzer;
|
||||
// }
|
||||
|
||||
// // Export for module environments (if using a bundler)
|
||||
// if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
// module.exports = ImageBrightnessAnalyzer;
|
||||
// }
|
||||
1
packages/core/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./image-brightness-analyzer.ts"
|
||||
13
packages/core/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@nestri/core",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"exports":{
|
||||
".":"./index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240529.0",
|
||||
"wrangler": "^3.57.2"
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
"files": [
|
||||
"library.js",
|
||||
"next.js",
|
||||
"react-internal.js",
|
||||
"qwik.js"
|
||||
"qwik.js",
|
||||
"react-internal.js"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@vercel/style-guide": "^5.2.0",
|
||||
@@ -22,4 +22,4 @@
|
||||
"prettier": "3.3.3",
|
||||
"typescript": "5.4.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
packages/typescript-config/react-library.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"display": "React Library",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@nestri/eslint-config/qwik.js"],
|
||||
extends: [
|
||||
"@nestri/eslint-config/qwik.js",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "./tsconfig.lint.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
project: ["./tsconfig.json"],
|
||||
ecmaVersion: 2021,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,46 +2,51 @@
|
||||
"name": "@nestri/ui",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"tailwind.config.ts",
|
||||
"postcss.config.js",
|
||||
"globals.css"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./react": "./src/react/index.ts",
|
||||
"./tailwind.config": "./tailwind.config.js",
|
||||
"./globals.css": "./globals.css",
|
||||
"./postcss": "./post.config.js"
|
||||
"./postcss.config": "./postcss.config.js",
|
||||
"./tailwind.config": "./tailwind.config.js",
|
||||
"./image": "./src/image/index.ts",
|
||||
"./design": "./src/design/index.ts"
|
||||
},
|
||||
"files": [
|
||||
"tailwind.config.js",
|
||||
"post.config.js",
|
||||
"globals.css",
|
||||
"postcss.config.js"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint . --max-warnings 0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@builder.io/qwik": "^1.8.0",
|
||||
"@builder.io/qwik-city": "^1.8.0",
|
||||
"@builder.io/qwik-react": "^0.5.5",
|
||||
"@builder.io/qwik-react": "0.5.0",
|
||||
"@fontsource/bricolage-grotesque": "^5.0.7",
|
||||
"@fontsource/geist-sans": "^5.0.3",
|
||||
"@nestri/eslint-config": "*",
|
||||
"@nestri/typescript-config": "*",
|
||||
"@nestri/eslint-config": "workspace:*",
|
||||
"@nestri/typescript-config": "workspace:*",
|
||||
"@nestri/core": "workspace:*",
|
||||
"@turbo/gen": "^1.12.4",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/eslint": "^8.56.5",
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react": "^18.2.28",
|
||||
"@types/react-dom": "^18.2.13",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.9.1",
|
||||
"framer-motion": "^11.3.31",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^8.57.0",
|
||||
"framer-motion": "^11.3.24",
|
||||
"nprogress": "^0.2.0",
|
||||
"postcss": "^8.4.41",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-wrap-balancer": "^1.1.1",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwind-variants": "^0.2.1",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
6
packages/ui/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
178
packages/ui/src/design/button-variants.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
// The solid and outlined variants are based on CSS Pro 3D Buttons code (https://csspro.com/css-3d-buttons/)
|
||||
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
export type ButtonVariantProps = {
|
||||
variant?: keyof typeof buttonVariants | undefined;
|
||||
intent?: keyof typeof solid.variants.intent;
|
||||
size?: keyof typeof baseButton.variants.size;
|
||||
};
|
||||
export type ButtonVariantIconProps = {
|
||||
type?: keyof typeof buttonIcon.variants.type;
|
||||
size?: keyof typeof buttonIcon.variants.size;
|
||||
};
|
||||
|
||||
const baseButton = tv({
|
||||
base: "group font-title flex justify-center select-none gap-1.5 items-center rounded-lg outline-2 outline-offset-2 focus-visible:outline outline-primary-6000 disabled:text-gray-400 disabled:border disabled:border-gray-300 dark:disabled:bg-gray-500/10 disabled:bg-gray-200 disabled:shadow-none disabled:hover:brightness-100 dark:disabled:text-gray-700 dark:disabled:shadow-none disabled:cursor-not-allowed dark:disabled:border dark:disabled:border-gray-700", // dark:disabled:[background-image:none]
|
||||
variants: {
|
||||
size: {
|
||||
xs: "text-sm h-7 px-3",
|
||||
sm: "text-sm h-8 px-3.5",
|
||||
md: "text-base h-9 px-4",
|
||||
lg: "text-base h-10 px-5",
|
||||
xl: "text-lg h-12 px-6",
|
||||
},
|
||||
iconOnlyButtonSize: {
|
||||
xs: "size-7",
|
||||
sm: "size-8",
|
||||
md: "size-9",
|
||||
lg: "size-10",
|
||||
xl: "size-12",
|
||||
},
|
||||
defaultVariants: {
|
||||
intent: "primary",
|
||||
size: "md",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const link = tv({
|
||||
// extend: baseButton,
|
||||
// base: "group transition-shadows relative ml-1.5 font-medium shadow-[inset_0_-0.2em_0_0_theme(colors.primary.200)] dark:shadow-[inset_0_-0.2em_0_0_theme(colors.primary.600)] duration-300 ease-out hover:shadow-[inset_0_-1em_0_0_theme(colors.primary.200)] dark:hover:shadow-[inset_0_-1em_0_0_theme(colors.primary.600)] text-primary-950 font-medium dark:text-primary-50", //"relative text-primary-950 font-medium dark:text-primary-50 hover:after:w-[calc(100%+2px)] focus:after:w-[calc(100%+2px)] px-1 after:-bottom-1 transition-all duration-[.2s] ease-[cubic-bezier(.165,.84,.44,1)] after:w-0 after:h-0.5 after:bg-primary-950 dark:after:bg-primary-50 after:absolute after:left-0 after:transition-[width] after:duration-[.2s] focus:shadow-none outline-none"
|
||||
base: "group relative ml-1.5 font-medium border-b-2 border-primary-950 dark:border-primary-50 hover:border-primary-950/60 dark:hover:border-primary-50/60 hover:text-primary-950/70 dark:hover:text-primary-50/70 text-primary-950 font-medium dark:text-primary-50", //"relative text-primary-950 font-medium dark:text-primary-50 hover:after:w-[calc(100%+2px)] focus:after:w-[calc(100%+2px)] px-1 after:-bottom-1 transition-all duration-[.2s] ease-[cubic-bezier(.165,.84,.44,1)] after:w-0 after:h-0.5 after:bg-primary-950 dark:after:bg-primary-50 after:absolute after:left-0 after:transition-[width] after:duration-[.2s] focus:shadow-none outline-none"
|
||||
variants: {
|
||||
size: {
|
||||
xs: "text-sm h-7 px-3",
|
||||
sm: "text-sm h-8 px-3.5",
|
||||
md: "text-base h-9 px-4",
|
||||
lg: "text-base h-10 px-5",
|
||||
xl: "text-lg h-12 px-6",
|
||||
},
|
||||
iconOnlyButtonSize: {
|
||||
xs: "size-7",
|
||||
sm: "size-8",
|
||||
md: "size-9",
|
||||
lg: "size-10",
|
||||
xl: "size-12",
|
||||
},
|
||||
defaultVariants: {
|
||||
intent: "primary",
|
||||
size: "md",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const solid = tv({
|
||||
extend: baseButton,
|
||||
base: "bg-gradient-to-b [box-shadow:rgba(255,255,255,0.25)_0px_1px_0px_0px_inset,var(--btn-border-color)_0px_0px_0px_1px] text-white hover:brightness-[1.1] transition-[filter] duration-150 ease-in-out active:brightness-95 dark:border-t dark:shadow-white/10 disabled:from-gray-200 disabled:to-gray-200 dark:disabled:text-gray-400 dark:disabled:from-gray-800 dark:disabled:to-gray-800",
|
||||
variants: {
|
||||
intent: {
|
||||
primary:
|
||||
"from-primary-500 to-primary-600 [--btn-border-color:theme(colors.primary.600)] dark:border-primary-500/75",
|
||||
secondary:
|
||||
"from-secondary-500 to-secondary-600 [--btn-border-color:theme(colors.secondary.700)] dark:border-secondary-400/75",
|
||||
accent: "from-accent-500 to-accent-600 [--btn-border-color:theme(colors.accent.700)] dark:border-accent-400/75",
|
||||
danger: "from-danger-500 to-danger-600 [--btn-border-color:theme(colors.danger.700)] dark:border-danger-400/75",
|
||||
info: "from-info-500 to-info-600 [--btn-border-color:theme(colors.info.700)] dark:border-info-400/75",
|
||||
success:
|
||||
"from-success-500 to-success-600 [--btn-border-color:theme(colors.success.700)] dark:border-success-400/75",
|
||||
warning:
|
||||
"from-warning-400 to-warning-500 text-warning-950 [--btn-border-color:theme(colors.warning.600)] dark:border-warning-300",
|
||||
gray: "from-gray-500 to-gray-600 [--btn-border-color:theme(colors.gray.700)] dark:border-gray-500",
|
||||
neutral:
|
||||
"bg-gray-900 [background-image:radial-gradient(76%_151%_at_52%_-52%,rgba(255,255,255,0.5)_0%,transparent_100%)] [box-shadow:rgba(255,255,255,0.3)_0px_1px_0px_0px_inset,theme(colors.gray.950)_0px_0px_0px_1px] hover:brightness-125 dark:bg-white dark:text-gray-950 dark:border-gray-300",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const outlined = tv({
|
||||
extend: baseButton,
|
||||
base: "[--outline-radial-opacity:0.6] dark:[background-image:none] [--inner-border-color:1] dark:[--inner-border-color:0] dark:[--outline-radial-opacity:0.2] [background-image:radial-gradient(76%_151%_at_52%_-52%,rgba(255,255,255,var(--outline-radial-opacity))_0%,transparent_100%)] [box-shadow:rgba(255,255,255,var(--inner-border-color))_0px_1px_0px_0px_inset,var(--btn-border-color)_0px_0px_0px_1px,0px_1px_2px_rgba(0,0,0,0.1)] hover:brightness-[0.98] active:brightness-100 transtion-[filter] ease-in-out duration-150",
|
||||
variants: {
|
||||
intent: {
|
||||
primary:
|
||||
"[--btn-border-color:theme(colors.primary.200)] dark:[--btn-border-color:theme(colors.primary.500/0.3)] text-primary-800 bg-primary-50 dark:text-primary-300 dark:bg-primary-500/5 dark:hover:bg-primary-500/10 dark:active:bg-primary-500/5",
|
||||
secondary:
|
||||
"[--btn-border-color:theme(colors.secondary.200)] dark:[--btn-border-color:theme(colors.secondary.500/0.3)] text-secondary-800 bg-secondary-50 dark:text-secondary-300 dark:bg-secondary-500/5 dark:hover:bg-secondary-500/10 dark:active:bg-secondary-500/5",
|
||||
accent: "[--btn-border-color:theme(colors.accent.200)] dark:[--btn-border-color:theme(colors.accent.500/0.3)] text-accent-800 bg-accent-50 dark:text-accent-300 dark:bg-accent-500/5 dark:hover:bg-accent-500/10 dark:active:bg-accent-500/5",
|
||||
danger: "[--btn-border-color:theme(colors.danger.200)] dark:[--btn-border-color:theme(colors.danger.500/0.3)] text-danger-800 bg-danger-50 dark:text-danger-300 dark:bg-danger-500/5 dark:hover:bg-danger-500/10 dark:active:bg-danger-500/5",
|
||||
info: "[--btn-border-color:theme(colors.info.200)] dark:[--btn-border-color:theme(colors.info.500/0.3)] text-info-800 bg-info-50 dark:text-info-300 dark:bg-info-500/5 dark:hover:bg-info-500/10 dark:active:bg-info-500/5",
|
||||
success:
|
||||
"[--btn-border-color:theme(colors.success.200)] dark:[--btn-border-color:theme(colors.success.500/0.3)] text-success-800 bg-success-100 dark:text-success-300 dark:bg-success-500/5 dark:hover:bg-success-500/10 dark:active:bg-success-500/5",
|
||||
warning:
|
||||
"[--btn-border-color:theme(colors.warning.200)] dark:[--btn-border-color:theme(colors.warning.500/0.3)] text-warning-800 bg-warning-50 dark:text-warning-300 dark:bg-warning-500/5 dark:hover:bg-warning-500/10 dark:active:bg-warning-500/5",
|
||||
gray: "[--btn-border-color:theme(colors.gray.200)] dark:[--btn-border-color:theme(colors.gray.500/0.3)] text-gray-800 bg-gray-50 dark:text-gray-300 dark:bg-gray-500/5 dark:hover:bg-gray-500/10 dark:active:bg-gray-500/5",
|
||||
neutral:
|
||||
"[--btn-border-color:theme(colors.gray.300)] dark:[--btn-border-color:theme(colors.gray.700)] text-gray-800 bg-gray-100 dark:text-white dark:bg-gray-500/5 dark:hover:bg-gray-500/10 dark:active:bg-gray-500/5",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const soft = tv({
|
||||
extend: baseButton,
|
||||
variants: {
|
||||
intent: {
|
||||
primary:
|
||||
"text-primary-700 bg-primary-100 hover:bg-primary-200/75 active:bg-primary-100 dark:text-primary-300 dark:bg-primary-500/10 dark:hover:bg-primary-500/15 dark:active:bg-primary-500/10",
|
||||
secondary:
|
||||
"text-secondary-700 bg-secondary-100 hover:bg-secondary-200/75 active:bg-secondary-100 dark:text-secondary-300 dark:bg-secondary-500/10 dark:hover:bg-secondary-500/15 dark:active:bg-secondary-500/10",
|
||||
accent: "text-accent-700 bg-accent-100 hover:bg-accent-200/75 active:bg-accent-100 dark:text-accent-300 dark:bg-accent-500/10 dark:hover:bg-accent-500/15 dark:active:bg-accent-500/10",
|
||||
danger: "text-danger-700 bg-danger-100 hover:bg-danger-200/75 active:bg-danger-100 dark:text-danger-300 dark:bg-danger-500/10 dark:hover:bg-danger-500/15 dark:active:bg-danger-500/10",
|
||||
info: "text-info-700 bg-info-100 hover:bg-info-200/75 active:bg-info-100 dark:text-info-300 dark:bg-info-500/10 dark:hover:bg-info-500/15 dark:active:bg-info-500/10",
|
||||
success:
|
||||
"text-success-700 bg-success-100 hover:bg-success-200/75 active:bg-success-100 dark:text-success-300 dark:bg-success-500/10 dark:hover:bg-success-500/15 dark:active:bg-success-500/10",
|
||||
warning:
|
||||
"text-warning-700 bg-warning-100 hover:bg-warning-200/75 active:bg-warning-100 dark:text-warning-300 dark:bg-warning-500/10 dark:hover:bg-warning-500/15 dark:active:bg-warning-500/10",
|
||||
gray: "text-gray-800 bg-gray-100 hover:bg-gray-200/75 active:bg-gray-100 dark:text-gray-300 dark:bg-gray-500/10 dark:hover:bg-gray-500/15 dark:active:bg-gray-500/10",
|
||||
neutral:
|
||||
"text-gray-950 bg-gray-100 hover:bg-gray-950 hover:text-white active:text-white active:bg-gray-900 dark:text-gray-300 dark:bg-gray-500/10 dark:hover:bg-white dark:hover:text-gray-950 dark:active:bg-gray-200 dark:active:text-gray-950",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ghost = tv({
|
||||
extend: baseButton,
|
||||
variants: {
|
||||
intent: {
|
||||
primary:
|
||||
"hover:bg-gray-100 active:bg-primary-200/75 dark:hover:bg-gray-800 dark:active:bg-primary-500/15",
|
||||
secondary:
|
||||
"text-secondary-600 hover:bg-secondary-100 active:bg-secondary-200/75 dark:text-secondary-400 dark:hover:bg-secondary-500/10 dark:active:bg-secondary-500/15",
|
||||
accent: "text-accent-600 hover:bg-accent-100 active:bg-accent-200/75 dark:text-accent-400 dark:hover:bg-accent-500/10 dark:active:bg-accent-500/15",
|
||||
danger: "text-danger-600 hover:bg-danger-100 active:bg-danger-200/75 dark:text-danger-400 dark:hover:bg-danger-500/10 dark:active:bg-danger-500/15",
|
||||
info: "text-info-600 hover:bg-info-100 active:bg-info-200/75 dark:text-info-400 dark:hover:bg-info-500/10 dark:active:bg-info-500/15",
|
||||
success:
|
||||
"text-success-600 hover:bg-success-100 active:bg-success-200/75 dark:text-success-400 dark:hover:bg-success-500/10 dark:active:bg-success-500/15",
|
||||
warning:
|
||||
"text-warning-600 hover:bg-warning-100 active:bg-warning-200/75 dark:text-warning-400 dark:hover:bg-warning-500/10 dark:active:bg-warning-500/15",
|
||||
gray: "text-gray-800 hover:bg-gray-100 active:bg-gray-200/75 dark:text-gray-300 dark:hover:bg-gray-500/10 dark:active:bg-gray-500/15",
|
||||
neutral:
|
||||
"text-gray-950 hover:bg-gray-950 hover:text-white active:text-white active:bg-gray-900 dark:text-white dark:hover:bg-white dark:hover:text-gray-950 dark:active:bg-gray-200 dark:active:text-gray-950",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const buttonIcon = tv({
|
||||
variants: {
|
||||
type: {
|
||||
leading: "-ml-1",
|
||||
trailing: "-mr-1",
|
||||
only: "m-auto",
|
||||
},
|
||||
size: {
|
||||
xs: "size-3.5",
|
||||
sm: "size-4",
|
||||
md: "size-[1.125rem]",
|
||||
lg: "size-5",
|
||||
xl: "size-6",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const buttonVariants = {
|
||||
solid,
|
||||
outlined,
|
||||
soft,
|
||||
ghost,
|
||||
link
|
||||
};
|
||||
215
packages/ui/src/design/form.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
export const form = tv({
|
||||
slots: {
|
||||
label: "text-nowrap text-[--title-text-color]",
|
||||
input: "[--btn-radius:lg] w-full px-[--input-px] bg-transparent peer transition-[outline] placeholder-[--placeholder-text-color] text-[--title-text-color] rounded-[--btn-radius] disabled:opacity-50",
|
||||
message: "mt-2 text-[--caption-text-color]",
|
||||
icon: "absolute inset-y-0 my-auto text-[--placeholder-text-color] pointer-events-none",
|
||||
field: "relative group *:has-[:disabled]:opacity-50 *:has-[:disabled]:pointer-events-none data-[invalid]:[--caption-text-color:theme(colors.danger.600)] dark:data-[invalid]:[--caption-text-color:theme(colors.danger.400)] data-[valid]:[--caption-text-color:theme(colors.success.600)] dark:data-[valid]:[--caption-text-color:theme(colors.success.400)]",
|
||||
textarea: "py-[calc(var(--input-px)/1.5)] h-auto",
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
outlined: {
|
||||
input: "outline-2 focus:outline-primary-600 -outline-offset-1 focus:outline border data-[invalid]:border-danger-600 focus:data-[invalid]:outline-danger-600 dark:data-[invalid]:border-danger-500 dark:focus:data-[invalid]:outline-danger-500 data-[valid]:border-success-600 focus:data-[valid]:outline-success-600 dark:data-[valid]:border-success-500 dark:focus:data-[valid]:outline-success-500",
|
||||
},
|
||||
soft: {
|
||||
input: "outline-none bg-[--ui-soft-bg] focus:brightness-95 dark:focus:brightness-105 data-[invalid]:[--ui-soft-bg:theme(colors.danger.100)] dark:data-[invalid]:[--ui-soft-bg:theme(colors.danger.800/0.25)] data-[valid]:[--ui-soft-bg:theme(colors.success.100)] dark:data-[valid]:[--ui-soft-bg:theme(colors.success.800/0.25)]",
|
||||
},
|
||||
mixed: {
|
||||
input: "placeholder-gray-950/50 dark:placeholder-gray-50/50 shadow-sm hover:shadow-lg dark:hover:shadow-sm shadow-gray-950/50 dark:shadow-gray-50 outline-2 focus:outline-primary-600 focus:outline -outline-offset-1 border border-primary-950 dark:border-primary-50 bg-gray-100 dark:bg-gray-800 data-[invalid]:border-2 data-[invalid]:border-danger-600 focus:data-[invalid]:outline-danger-600 dark:data-[invalid]:border-danger-500 dark:focus:data-[invalid]:outline-danger-500 data-[valid]:border-success-600 focus:data-[valid]:outline-success-600 dark:data-[valid]:border-success-500 dark:focus:data-[valid]:outline-success-500",
|
||||
},
|
||||
plain: {
|
||||
input: "rounded-none px-0 outline-none bg-transparent invalid:text-danger-600 dark:invalid:text-danger-400",
|
||||
},
|
||||
bottomOutlined: {
|
||||
input: "rounded-none transition-[border] px-0 focus:outline-none border-b focus:border-b-2 focus:border-primary-600 data-[invalid]:border-danger-400 dark:data-[invalid]:border-danger-600 data-[valid]:border-success-400 dark:data-[valid]:border-success-600",
|
||||
},
|
||||
},
|
||||
size: {
|
||||
xs: {
|
||||
message: "text-xs",
|
||||
},
|
||||
sm: {
|
||||
label: "text-sm",
|
||||
message: "text-sm",
|
||||
input: "text-sm h-8 [--input-px:theme(spacing[2.5])]",
|
||||
field: "[--input-px:theme(spacing[2.5])]",
|
||||
},
|
||||
md: {
|
||||
label: "text-base",
|
||||
message: "text-base",
|
||||
input: "text-sm h-9 [--input-px:theme(spacing[3])]",
|
||||
field: "[--input-px:theme(spacing[3])]",
|
||||
},
|
||||
lg: {
|
||||
label: "text-lg",
|
||||
input: "text-base h-10 [--input-px:theme(spacing[4])]",
|
||||
field: "[--input-px:theme(spacing[4])]",
|
||||
},
|
||||
xl: {
|
||||
label: "text-xl",
|
||||
input: "text-base h-12 [--input-px:theme(spacing[5])]",
|
||||
field: "[--input-px:theme(spacing[5])]",
|
||||
},
|
||||
},
|
||||
fancy: {
|
||||
true: {
|
||||
input: "shadow-inner shadow-gray-950/5 dark:shadow-gray-950/35",
|
||||
}
|
||||
},
|
||||
floating: {
|
||||
true: {
|
||||
label: "absolute block inset-y-0 text-base left-[--input-px] h-fit text-nowrap my-auto text-[--caption-text-color] pointer-events-none transition duration-150 scale-[.8] origin-top-left peer-placeholder-shown:scale-100 peer-focus:scale-[.8] peer-placeholder-shown:translate-y-0",
|
||||
},
|
||||
},
|
||||
asTextarea: {
|
||||
true: {
|
||||
label: "top-[calc(var(--input-px)/1.5)] mt-0",
|
||||
},
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
floating: true,
|
||||
variant: ["bottomOutlined", "plain"],
|
||||
class: {
|
||||
input: "px-0",
|
||||
label: "left-0",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
variant: ["outlined", "soft", "bottomOutlined", "mixed"],
|
||||
size: "xl",
|
||||
class: {
|
||||
field: "[--input-px:theme(spacing[2.5])]",
|
||||
input: "pt-3.5 h-14",
|
||||
label: "-translate-y-2.5 peer-focus:-translate-y-2.5",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
variant: ["soft", "bottomOutlined"],
|
||||
size: "lg",
|
||||
class: {
|
||||
input: "pt-4 h-12",
|
||||
label: "-translate-y-2 peer-focus:-translate-y-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
variant: ["outlined", "mixed"],
|
||||
size: "lg",
|
||||
class: {
|
||||
input: "h-12",
|
||||
label: "-translate-y-[21.5px] peer-focus:-translate-y-[21.5px]",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
variant: ["outlined", "bottomOutlined", "mixed"],
|
||||
size: "md",
|
||||
class: {
|
||||
input: "h-9",
|
||||
label: "-translate-y-[15.5px] peer-focus:-translate-y-[15.5px]",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
variant: ["outlined", "bottomOutlined", "mixed"],
|
||||
size: "sm",
|
||||
class: {
|
||||
input: "h-8",
|
||||
label: "text-base -translate-y-[13.5px] peer-focus:-translate-y-[13.5px] peer-placeholder-shown:text-sm peer-focus:text-base",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
size: ["sm", "md", "lg"],
|
||||
variant: ["outlined", "mixed"],
|
||||
class: {
|
||||
label: "peer-placeholder-shown:before:scale-x-0 peer-focus:before:scale-x-100 before:scale-x-100 before:absolute peer-placeholder-shown:before:transition-none before:transition-none peer-focus:before:transition peer-focus:before:delay-[.01s] before:duration-500 before:-z-[1] peer-placeholder-shown:before:-top-[9px] before:top-[48%] peer-focus:before:top-[44%] before:-inset-x-1 before:h-0.5 before:my-auto group-has-[:focus]:before:h-[3px] before:bg-white dark:before:bg-[--ui-bg]",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
variant: ["outlined", "mixed"],
|
||||
class: {
|
||||
label: "translate-x-px",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
asTextarea: true,
|
||||
size: ["lg", "xl"],
|
||||
variant: "bottomOutlined",
|
||||
class: {
|
||||
input: "pt-0",
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: "plain",
|
||||
class: {
|
||||
input: "py-0",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
asTextarea: true,
|
||||
size: ["xl"],
|
||||
class: {
|
||||
input: "pt-7",
|
||||
label: "-translate-y-1 peer-focus:-translate-y-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
asTextarea: true,
|
||||
size: ["lg"],
|
||||
class: {
|
||||
input: "pt-6",
|
||||
label: "-translate-y-1 peer-focus:-translate-y-1 before:hidden",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
asTextarea: true,
|
||||
size: ["md"],
|
||||
variant: ["outlined", "bottomOutlined", "mixed"],
|
||||
class: {
|
||||
label: "-translate-y-[17.5px] peer-focus:-translate-y-[17.5px]",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
asTextarea: true,
|
||||
size: ["sm"],
|
||||
variant: ["outlined", "bottomOutlined", "mixed"],
|
||||
class: {
|
||||
label: "-translate-y-4 peer-focus:-translate-y-4",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export type FormProps = {
|
||||
variant?: keyof typeof form.variants.variant;
|
||||
size?: keyof typeof form.variants.size;
|
||||
floating?: boolean;
|
||||
asTextarea?: boolean;
|
||||
};
|
||||
|
||||
export type InputProps = Omit<FormProps, "asTextarea"> & {
|
||||
size?: Exclude<FormProps["size"], "xs">;
|
||||
fancy?: boolean;
|
||||
};
|
||||
|
||||
export type LabelProps = FormProps & {
|
||||
size?: Exclude<FormProps["size"], "xs">;
|
||||
};
|
||||
|
||||
export type MessageProps = {
|
||||
size?: Exclude<FormProps["size"], "lg" | "xl">;
|
||||
};
|
||||
4
packages/ui/src/design/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./button-variants"
|
||||
export * from "./typography"
|
||||
export * from "./utils"
|
||||
export * from "./form"
|
||||
401
packages/ui/src/design/typography.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
export const base = tv({
|
||||
variants: {
|
||||
weight: {
|
||||
black: "font-black",
|
||||
bold: "font-bold",
|
||||
semibold: "font-semibold",
|
||||
medium: "font-medium",
|
||||
normal: "font-normal",
|
||||
},
|
||||
align: {
|
||||
left: "text-left",
|
||||
center: "text-center",
|
||||
right: "text-right",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "xl",
|
||||
weight: "normal",
|
||||
},
|
||||
});
|
||||
|
||||
export const caption = tv(
|
||||
{
|
||||
extend: base,
|
||||
base: "text-gray-500",
|
||||
variants: {
|
||||
size: {
|
||||
xs: "text-xs",
|
||||
sm: "text-sm",
|
||||
base: "text-base",
|
||||
},
|
||||
neutral: {
|
||||
true: "text-gray-950 dark:text-white",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "sm",
|
||||
weight: "normal",
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: ["sm", "md", "lg", "xl", "2xl"],
|
||||
},
|
||||
);
|
||||
|
||||
export const text = tv(
|
||||
{
|
||||
extend: base,
|
||||
base: "text-gray-700",
|
||||
variants: {
|
||||
size: {
|
||||
sm: "text-sm",
|
||||
base: "text-base",
|
||||
lg: "text-lg",
|
||||
xl: "text-xl",
|
||||
},
|
||||
neutral: {
|
||||
true: "text-gray-950 dark:text-white",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "base",
|
||||
weight: "normal",
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: ["sm", "md", "lg", "xl", "2xl"],
|
||||
},
|
||||
);
|
||||
|
||||
export const list = tv(
|
||||
{
|
||||
extend: text,
|
||||
base: "list-outside pl-4",
|
||||
variants: {
|
||||
type: {
|
||||
disc: "list-disc",
|
||||
decimal: "list-decimal",
|
||||
none: "list-none",
|
||||
},
|
||||
inside: {
|
||||
true: "list-outside pl-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "base",
|
||||
type: "disc",
|
||||
weight: "normal",
|
||||
inside: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: ["sm", "md", "lg", "xl", "2xl"],
|
||||
},
|
||||
);
|
||||
|
||||
export const link = tv(
|
||||
{
|
||||
extend: base,
|
||||
base: "transition",
|
||||
variants: {
|
||||
size: {
|
||||
xs: "text-xs",
|
||||
sm: "text-sm",
|
||||
base: "text-base",
|
||||
lg: "text-lg",
|
||||
xl: "text-xl",
|
||||
},
|
||||
intent: {
|
||||
primary:
|
||||
"text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-500",
|
||||
secondary:
|
||||
"text-secondary-600 hover:text-secondary-700 dark:text-secondary-400 dark:hover:text-secondary-500",
|
||||
accent: "text-accent-600 hover:text-accent-700 dark:text-accent-400 dark:hover:text-accent-500",
|
||||
info: "text-info-600 hover:text-info-700 dark:text-info-400 dark:hover:text-info-500",
|
||||
danger: "text-danger-600 hover:text-danger-700 dark:text-danger-400 dark:hover:text-danger-500",
|
||||
success:
|
||||
"text-success-600 hover:text-success-700 dark:text-success-400 dark:hover:text-success-500",
|
||||
warning:
|
||||
"text-warning-700 hover:text-warning-600 dark:text-warning-400 dark:hover:text-warning-500",
|
||||
gray: "text-gray-700",
|
||||
neutral:
|
||||
"text-gray-950 hover:text-gray-800 dark:text-white dark:hover:text-gray-200",
|
||||
},
|
||||
variant: {
|
||||
plain: "",
|
||||
underlined: "underline",
|
||||
ghost: "hover:underline",
|
||||
animated:
|
||||
"relative before:absolute before:inset-x-0 before:bottom-0 before:h-px before:scale-x-0 before:origin-right hover:before:origin-left hover:before:scale-x-100 before:transition before:duration-200",
|
||||
},
|
||||
visited: {
|
||||
true: "visited:text-accent-600 dark:visited:text-accent-400",
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
variant: ["plain", "ghost", "underlined"],
|
||||
intent: "gray",
|
||||
class: "hover:text-gray-950 dark:hover:text-white",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "primary",
|
||||
class: "before:bg-primary-600/50 dark:before:bg-primary-400/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "info",
|
||||
class: "before:bg-info-600/50 dark:before:bg-info-400/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "neutral",
|
||||
class: "before:bg-gray-950/50 dark:before:bg-white/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "gray",
|
||||
class: "before:bg-gray-600/50 dark:before:bg-gray-400/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "secondary",
|
||||
class: "before:bg-secondary-600/50 dark:before:bg-secondary-400/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "accent",
|
||||
class: "before:bg-accent-600/50 dark:before:bg-accent-400/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "danger",
|
||||
class: "before:bg-danger-600/50 dark:before:bg-danger-400/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "success",
|
||||
class: "before:bg-success-600/50 dark:before:bg-success-400/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "warning",
|
||||
class: "before:bg-warning-600/50 dark:before:bg-warning-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "primary",
|
||||
class: "decoration-primary-600/50 dark:decoration-primary-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "info",
|
||||
class: "decoration-info-600/50 dark:decoration-info-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "gray",
|
||||
class: "decoration-gray-600/50 dark:decoration-gray-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "neutral",
|
||||
class: "decoration-gray-950/25 dark:decoration-white/25",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "secondary",
|
||||
class: "decoration-secondary-600/50 dark:decoration-secondary-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "accent",
|
||||
class: "decoration-accent-600/50 dark:decoration-accent-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "danger",
|
||||
class: "decoration-danger-600/50 dark:decoration-danger-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "success",
|
||||
class: "decoration-success-600/50 dark:decoration-success-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "warning",
|
||||
class: "decoration-warning-600/50 dark:decoration-warning-400/50",
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
intent: "primary",
|
||||
variant: "ghost",
|
||||
size: "base",
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: ["sm", "md", "lg", "xl", "2xl"],
|
||||
},
|
||||
);
|
||||
|
||||
export const display = tv(
|
||||
{
|
||||
extend: base,
|
||||
base: "block text-gray-950 dark:text-gray-50",
|
||||
variants: {
|
||||
size: {
|
||||
"4xl": "text-4xl",
|
||||
"5xl": "text-5xl",
|
||||
"6xl": "text-6xl",
|
||||
"7xl": "text-7xl",
|
||||
"8xl": "text-8xl",
|
||||
"9xl": "text-9xl",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "6xl",
|
||||
weight: "bold",
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: ["sm", "md", "lg", "xl", "2xl"],
|
||||
},
|
||||
);
|
||||
|
||||
export const title = tv(
|
||||
{
|
||||
extend: base,
|
||||
base: "block text-gray-950",
|
||||
variants: {
|
||||
size: {
|
||||
base: "text-base",
|
||||
lg: "text-lg",
|
||||
xl: "text-xl",
|
||||
"2xl": "text-2xl",
|
||||
"3xl": "text-3xl",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "xl",
|
||||
weight: "semibold",
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: ["sm", "md", "lg", "xl", "2xl"],
|
||||
},
|
||||
);
|
||||
|
||||
export const codeTheme = tv({
|
||||
base: "text-sm inline-block border rounded-md py-px px-1",
|
||||
variants: {
|
||||
intent: {
|
||||
primary:
|
||||
"bg-primary-50 text-primary-600 dark:text-primary-300 border-primary-200 dark:border-primary-500/20 dark:bg-primary-500/5",
|
||||
secondary:
|
||||
"bg-secondary-50 text-secondary-600 dark:text-secondary-300 border-secondary-200 dark:border-secondary-500/20 dark:bg-secondary-500/5",
|
||||
accent: "bg-accent-50 text-accent-600 dark:text-accent-300 border-accent-200 dark:border-accent-500/20 dark:bg-accent-500/5",
|
||||
gray: "bg-gray-50 text-gray-700 dark:border-gray-500/20 dark:bg-gray-500/5 dark:border-gray-500/20",
|
||||
neutral: "bg-gray-50 text-gray-950 dark:text-white dark:bg-gray-500/5 dark:border-gray-500/20",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
intent: "gray",
|
||||
},
|
||||
});
|
||||
|
||||
export const kbdTheme = tv({
|
||||
base: "inline-flex items-center justify-center text-gray-800 dark:text-white h-5 text-[11px] min-w-5 px-1.5 rounded font-sans bg-gray-100 dark:bg-white/10 ring-1 border-b border-t border-t-white border-b-gray-200 dark:border-t-transparent dark:border-b-gray-950 ring-gray-300 dark:ring-white/15",
|
||||
});
|
||||
|
||||
export type CodeThemeProps = {
|
||||
intent?: keyof typeof codeTheme.variants.intent;
|
||||
};
|
||||
|
||||
export type Weight = keyof typeof base.variants.weight;
|
||||
export type Align = keyof typeof base.variants.align;
|
||||
|
||||
type BaseTextProps = {
|
||||
weight?: Weight;
|
||||
align?: Align;
|
||||
};
|
||||
|
||||
export type CaptionProps = BaseTextProps & {
|
||||
size?: keyof typeof caption.variants.size;
|
||||
neutral?: boolean;
|
||||
};
|
||||
|
||||
export type TextProps = BaseTextProps & {
|
||||
size?: keyof typeof text.variants.size;
|
||||
neutral?: boolean;
|
||||
};
|
||||
|
||||
export type ListProps = BaseTextProps & {
|
||||
size?: keyof typeof text.variants.size;
|
||||
type?: keyof typeof list.variants.type;
|
||||
inside?: boolean;
|
||||
neutral?: boolean;
|
||||
};
|
||||
|
||||
export type LinkProps = BaseTextProps & {
|
||||
size?: keyof typeof text.variants.size | keyof typeof link.variants.size;
|
||||
variant?: keyof typeof link.variants.variant;
|
||||
intent?: keyof typeof link.variants.intent;
|
||||
visited?: boolean;
|
||||
};
|
||||
|
||||
export type TitleProps = BaseTextProps & {
|
||||
size?: keyof typeof title.variants.size;
|
||||
};
|
||||
|
||||
export type TitleSizeProp =
|
||||
| TitleProps["size"]
|
||||
| {
|
||||
initial?: TitleProps["size"];
|
||||
sm?: TitleProps["size"];
|
||||
md?: TitleProps["size"];
|
||||
lg?: TitleProps["size"];
|
||||
xl?: TitleProps["size"];
|
||||
xxl?: TitleProps["size"];
|
||||
};
|
||||
|
||||
export type TextSizeProp =
|
||||
| TextProps["size"]
|
||||
| {
|
||||
initial?: TextProps["size"];
|
||||
sm?: TextProps["size"];
|
||||
md?: TextProps["size"];
|
||||
lg?: TextProps["size"];
|
||||
xl?: TextProps["size"];
|
||||
xxl?: TextProps["size"];
|
||||
};
|
||||
|
||||
export type DisplayProps = BaseTextProps & {
|
||||
size?: keyof typeof display.variants.size;
|
||||
};
|
||||
|
||||
export type TextWeightProp =
|
||||
| Weight
|
||||
| {
|
||||
initial?: Weight;
|
||||
sm?: Weight;
|
||||
md?: Weight;
|
||||
lg?: Weight;
|
||||
xl?: Weight;
|
||||
xxl?: Weight;
|
||||
};
|
||||
|
||||
export type TextAlignProp =
|
||||
| Align
|
||||
| {
|
||||
initial?: Align;
|
||||
sm?: Align;
|
||||
md?: Align;
|
||||
lg?: Align;
|
||||
xl?: Align;
|
||||
xxl?: Align;
|
||||
};
|
||||
6
packages/ui/src/design/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
17
packages/ui/src/fonts.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { component$, Slot } from "@builder.io/qwik";
|
||||
|
||||
// import SansNormal from "@fontsource/geist-sans/400.css?inline"
|
||||
import "@fontsource/geist-sans/400.css"
|
||||
import "@fontsource/geist-sans/500.css"
|
||||
import "@fontsource/geist-sans/600.css"
|
||||
import "@fontsource/geist-sans/700.css"
|
||||
import "@fontsource/bricolage-grotesque/500.css"
|
||||
import "@fontsource/bricolage-grotesque/700.css"
|
||||
import "@fontsource/bricolage-grotesque/800.css"
|
||||
|
||||
export const Fonts = component$(() => {
|
||||
|
||||
// useStyles$(SansNormal);
|
||||
|
||||
return <Slot />;
|
||||
});
|
||||
132
packages/ui/src/footer.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
/* eslint-disable qwik/jsx-img */
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { Link } from "@builder.io/qwik-city";
|
||||
import { MotionComponent, transition } from "@nestri/ui/react"
|
||||
import { GithubBanner } from "./github-banner";
|
||||
|
||||
{/*
|
||||
|
||||
|
||||
*/}
|
||||
const socialMedia = [
|
||||
{
|
||||
link: "https://github.com/nestriness/nestri",
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 .999c-6.074 0-11 5.05-11 11.278c0 4.983 3.152 9.21 7.523 10.702c.55.104.727-.246.727-.543v-2.1c-3.06.683-3.697-1.33-3.697-1.33c-.5-1.304-1.222-1.65-1.222-1.65c-.998-.7.076-.686.076-.686c1.105.08 1.686 1.163 1.686 1.163c.98 1.724 2.573 1.226 3.201.937c.098-.728.383-1.226.698-1.508c-2.442-.286-5.01-1.253-5.01-5.574c0-1.232.429-2.237 1.132-3.027c-.114-.285-.49-1.432.107-2.985c0 0 .924-.303 3.026 1.156c.877-.25 1.818-.375 2.753-.38c.935.005 1.876.13 2.755.38c2.1-1.459 3.023-1.156 3.023-1.156c.598 1.554.222 2.701.108 2.985c.706.79 1.132 1.796 1.132 3.027c0 4.332-2.573 5.286-5.022 5.565c.394.35.754 1.036.754 2.088v3.095c0 .3.176.652.734.542C19.852 21.484 23 17.258 23 12.277C23 6.048 18.075.999 12 .999" /></svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
link: "https://discord.com/invite/Y6etn3qKZ3",
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M20.317 4.492c-1.53-.69-3.17-1.2-4.885-1.49a.075.075 0 0 0-.079.036c-.21.369-.444.85-.608 1.23a18.6 18.6 0 0 0-5.487 0a12 12 0 0 0-.617-1.23A.08.08 0 0 0 8.562 3c-1.714.29-3.354.8-4.885 1.491a.1.1 0 0 0-.032.027C.533 9.093-.32 13.555.099 17.961a.08.08 0 0 0 .031.055a20 20 0 0 0 5.993 2.98a.08.08 0 0 0 .084-.026a14 14 0 0 0 1.226-1.963a.074.074 0 0 0-.041-.104a13 13 0 0 1-1.872-.878a.075.075 0 0 1-.008-.125q.19-.14.372-.287a.08.08 0 0 1 .078-.01c3.927 1.764 8.18 1.764 12.061 0a.08.08 0 0 1 .079.009q.18.148.372.288a.075.075 0 0 1-.006.125q-.895.515-1.873.877a.075.075 0 0 0-.041.105c.36.687.772 1.341 1.225 1.962a.08.08 0 0 0 .084.028a20 20 0 0 0 6.002-2.981a.08.08 0 0 0 .032-.054c.5-5.094-.838-9.52-3.549-13.442a.06.06 0 0 0-.031-.028M8.02 15.278c-1.182 0-2.157-1.069-2.157-2.38c0-1.312.956-2.38 2.157-2.38c1.21 0 2.176 1.077 2.157 2.38c0 1.312-.956 2.38-2.157 2.38m7.975 0c-1.183 0-2.157-1.069-2.157-2.38c0-1.312.955-2.38 2.157-2.38c1.21 0 2.176 1.077 2.157 2.38c0 1.312-.946 2.38-2.157 2.38" /></svg>)
|
||||
},
|
||||
{
|
||||
link: "https://www.reddit.com/r/nestri",
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286A.72.72 0 0 0 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0m4.388 3.199a1.999 1.999 0 1 1-1.947 2.46v.002a2.37 2.37 0 0 0-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363a2.802 2.802 0 1 1 2.908 4.753c-.088 3.256-3.637 5.876-7.997 5.876c-4.361 0-7.905-2.617-7.998-5.87a2.8 2.8 0 0 1 1.189-5.34c.645 0 1.239.218 1.712.585c1.275-.79 2.881-1.291 4.64-1.365v-.01a3.23 3.23 0 0 1 2.88-3.207a2 2 0 0 1 1.959-1.595m-8.085 8.376c-.784 0-1.459.78-1.506 1.797s.64 1.429 1.426 1.429s1.371-.369 1.418-1.385s-.553-1.841-1.338-1.841m7.406 0c-.786 0-1.385.824-1.338 1.841s.634 1.385 1.418 1.385c.785 0 1.473-.413 1.426-1.429c-.046-1.017-.721-1.797-1.506-1.797m-3.703 4.013c-.974 0-1.907.048-2.77.135a.222.222 0 0 0-.183.305a3.2 3.2 0 0 0 2.953 1.964a3.2 3.2 0 0 0 2.953-1.964a.222.222 0 0 0-.184-.305a28 28 0 0 0-2.769-.135" /></svg>)
|
||||
},
|
||||
{
|
||||
link: "https://x.com/nestriness",
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M8 2H1l8.26 11.015L1.45 22H4.1l6.388-7.349L16 22h7l-8.608-11.478L21.8 2h-2.65l-5.986 6.886zm9 18L5 4h2l12 16z" /></svg>
|
||||
)
|
||||
},
|
||||
]
|
||||
|
||||
export const Footer = component$(() => {
|
||||
return (
|
||||
<>
|
||||
<GithubBanner />
|
||||
<footer class="flex justify-center flex-col items-center w-full pt-8 sm:pb-0 pb-8 [&>*]:w-full px-3">
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={transition}
|
||||
client:load
|
||||
as="div"
|
||||
class="w-full max-w-xl mx-auto z-[5] flex flex-col border-t-2 dark:border-gray-50/50 border-gray-950/50">
|
||||
<section class="flex justify-between items-center py-6 border-b-2 dark:border-gray-50/50 border-gray-950/50" >
|
||||
<div class="flex flex-row gap-1 h-max items-center justify-center">
|
||||
<svg
|
||||
width={40} height={40}
|
||||
viewBox="0 0 12.8778 9.7377253"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="layer1">
|
||||
<path
|
||||
d="m 2.093439,1.7855532 h 8.690922 V 2.2639978 H 2.093439 Z m 0,2.8440874 h 8.690922 V 5.1080848 H 2.093439 Z m 0,2.8440866 h 8.690922 V 7.952172 H 2.093439 Z"
|
||||
style="font-size:12px;fill:#ff4f01;fill-opacity:1;fill-rule:evenodd;stroke:#ff4f01;stroke-width:1.66201;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
<h3 class="text-lg font-extrabold font-title">Nestri</h3>
|
||||
</div>
|
||||
<p class="text-gray-950 dark:text-gray-50">Your games. Your rules.</p>
|
||||
</section>
|
||||
<section class="gap-4 grid grid-cols-2 sm:grid-cols-3 justify-around py-6 items-start" >
|
||||
<div class="flex flex-col gap-2">
|
||||
<h2 class="font-title text-sm font-bold" >Product</h2>
|
||||
<div class="text-gray-950/70 dark:text-gray-50/70 flex flex-col gap-2" >
|
||||
<Link href="/pricing" class="text-base hover:text-primary-500" >Pricing</Link>
|
||||
<Link href="/changelog" class="text-base hover:text-primary-500" >Changelog</Link>
|
||||
<p class="text-base opacity-50 cursor-not-allowed" >Docs</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<h2 class="font-title text-sm font-bold" >Company</h2>
|
||||
<div class="text-gray-950/70 dark:text-gray-50/70 flex flex-col gap-2" >
|
||||
<Link href="/contact" class="text-base hover:text-primary-500" >Contact Us</Link>
|
||||
<p class="text-base opacity-50 cursor-not-allowed" >Open Nestri</p>
|
||||
<p class="text-base opacity-50 cursor-not-allowed" >Blog</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<h2 class="font-title text-sm font-bold" >Relations</h2>
|
||||
<div class="text-gray-950/70 dark:text-gray-50/70 flex flex-col gap-2" >
|
||||
<Link href="/terms" class="text-base hover:text-primary-500" >Terms of Service</Link>
|
||||
<Link href="/privacy" class="text-base hover:text-primary-500" >Privacy Policy</Link>
|
||||
{/**Social Media Icons with Links */}
|
||||
<div class="flex flex-row gap-3">
|
||||
{socialMedia.map((item) => (
|
||||
<Link key={item.link} href={item.link} class="hover:text-primary-500" target="_blank" rel="noopener noreferrer">
|
||||
{item.icon()}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</MotionComponent>
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
transition={{
|
||||
...transition,
|
||||
duration: 0.8,
|
||||
delay: 0.7
|
||||
}}
|
||||
client:load
|
||||
as="div" class="w-full sm:flex z-[1] hidden pointer-events-none overflow-hidden -mt-[100px] justify-center items-center flex-col" >
|
||||
<section class='my-0 bottom-0 text-[100%] max-w-[1440px] pointer-events-none w-full flex items-center translate-y-[45%] justify-center relative overflow-hidden px-2 z-10 [&_svg]:w-full [&_svg]:max-w-[1440px] [&_svg]:h-full [&_svg]:opacity-70' >
|
||||
<svg viewBox="0 0 498.05 70.508" xmlns="http://www.w3.org/2000/svg" height={157} width={695} class="" >
|
||||
<g stroke-linecap="round" fill-rule="evenodd" font-size="9pt" stroke="currentColor" stroke-width="0.25mm" fill="currentColor" style="stroke:currentColor;stroke-width:0.25mm;fill:currentColor">
|
||||
<path
|
||||
fill="url(#paint1)"
|
||||
pathLength="1"
|
||||
stroke="url(#paint1)"
|
||||
d="M 261.23 41.65 L 212.402 41.65 Q 195.313 41.65 195.313 27.002 L 195.313 14.795 A 17.814 17.814 0 0 1 196.311 8.57 Q 199.443 0.146 212.402 0.146 L 283.203 0.146 L 283.203 14.844 L 217.236 14.844 Q 215.337 14.844 214.945 16.383 A 3.67 3.67 0 0 0 214.844 17.285 L 214.844 24.561 Q 214.844 27.002 217.236 27.002 L 266.113 27.002 Q 283.203 27.002 283.203 41.65 L 283.203 53.857 A 17.814 17.814 0 0 1 282.205 60.083 Q 279.073 68.506 266.113 68.506 L 195.313 68.506 L 195.313 53.809 L 261.23 53.809 A 3.515 3.515 0 0 0 262.197 53.688 Q 263.672 53.265 263.672 51.367 L 263.672 44.092 A 3.515 3.515 0 0 0 263.551 43.126 Q 263.128 41.65 261.23 41.65 Z M 185.547 53.906 L 185.547 68.506 L 114.746 68.506 Q 97.656 68.506 97.656 53.857 L 97.656 14.795 A 17.814 17.814 0 0 1 98.655 8.57 Q 101.787 0.146 114.746 0.146 L 168.457 0.146 Q 185.547 0.146 185.547 14.795 L 185.547 31.885 A 17.827 17.827 0 0 1 184.544 38.124 Q 181.621 45.972 170.174 46.538 A 36.906 36.906 0 0 1 168.457 46.582 L 117.188 46.582 L 117.236 51.465 Q 117.236 53.906 119.629 53.955 L 185.547 53.906 Z M 19.531 14.795 L 19.531 68.506 L 0 68.506 L 0 0.146 L 70.801 0.146 Q 87.891 0.146 87.891 14.795 L 87.891 68.506 L 68.359 68.506 L 68.359 17.236 Q 68.359 14.795 65.967 14.795 L 19.531 14.795 Z M 449.219 68.506 L 430.176 46.533 L 400.391 46.533 L 400.391 68.506 L 380.859 68.506 L 380.859 0.146 L 451.66 0.146 A 24.602 24.602 0 0 1 458.423 0.994 Q 466.007 3.166 468.021 10.907 A 25.178 25.178 0 0 1 468.75 17.236 L 468.75 31.885 A 18.217 18.217 0 0 1 467.887 37.73 Q 465.954 43.444 459.698 45.455 A 23.245 23.245 0 0 1 454.492 46.436 L 473.633 68.506 L 449.219 68.506 Z M 292.969 0 L 371.094 0.098 L 371.094 14.795 L 341.846 14.795 L 341.846 68.506 L 322.266 68.506 L 322.217 14.795 L 292.969 14.844 L 292.969 0 Z M 478.516 0.146 L 498.047 0.146 L 498.047 68.506 L 478.516 68.506 L 478.516 0.146 Z M 400.391 14.844 L 400.391 31.885 L 446.826 31.885 Q 448.726 31.885 449.117 30.345 A 3.67 3.67 0 0 0 449.219 29.443 L 449.219 17.285 Q 449.219 14.844 446.826 14.844 L 400.391 14.844 Z M 117.188 31.836 L 163.574 31.934 Q 165.528 31.895 165.918 30.355 A 3.514 3.514 0 0 0 166.016 29.492 L 166.016 17.236 Q 166.016 15.337 164.476 14.945 A 3.67 3.67 0 0 0 163.574 14.844 L 119.629 14.795 Q 117.188 14.795 117.188 17.188 L 117.188 31.836 Z" />
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient gradientUnits="userSpaceOnUse" id="paint1" x1="317.5" x2="314.007" y1="-51.5" y2="126">
|
||||
<stop stop-color="white"></stop>
|
||||
<stop offset="1" stop-opacity="0"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</section>
|
||||
</MotionComponent>
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
});
|
||||
64
packages/ui/src/game-card.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { component$ } from "@builder.io/qwik"
|
||||
import { cn } from "@nestri/ui/design"
|
||||
import { ImageLoader } from "@nestri/ui/image"
|
||||
|
||||
type Game = {
|
||||
appid: string
|
||||
name: string
|
||||
release_date: number
|
||||
compatibility: string
|
||||
teams: number
|
||||
}
|
||||
|
||||
type Props = {
|
||||
game: Game;
|
||||
class?: string;
|
||||
}
|
||||
export const GameCard = component$(({ game, class: className }: Props) => {
|
||||
return (
|
||||
<div key={`game-${game.appid}`} class={cn("bg-gray-200/70 min-w-[250px] backdrop-blur-sm ring-gray-300 select-none max-w-[270px] group dark:ring-gray-700 ring dark:bg-gray-800/70 group rounded-3xl dark:text-primary-50/70 text-primary-950/70 duration-300 transition-colors flex flex-col", className)}>
|
||||
<header class="flex gap-4 justify-between p-4">
|
||||
<div class="flex relative pr-[22px] overflow-hidden overflow-ellipsis whitespace-nowrap" >
|
||||
<h3 class="overflow-hidden overflow-ellipsis whitespace-nowrap">{game.name}</h3>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" class={cn("absolute right-0 bottom-0 top-0", game.compatibility == "perfect" ? "text-green-600 dark:text-green-400" : "text-amber-600 dark:text-amber-300")} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M9.592 3.2a5.727 5.727 0 0 1-.495.399c-.298.2-.633.338-.985.408c-.153.03-.313.043-.632.068c-.801.064-1.202.096-1.536.214a2.713 2.713 0 0 0-1.655 1.655c-.118.334-.15.735-.214 1.536a5.707 5.707 0 0 1-.068.632c-.07.352-.208.687-.408.985c-.087.13-.191.252-.399.495c-.521.612-.782.918-.935 1.238c-.353.74-.353 1.6 0 2.34c.153.32.414.626.935 1.238c.208.243.312.365.399.495c.2.298.338.633.408.985c.03.153.043.313.068.632c.064.801.096 1.202.214 1.536a2.713 2.713 0 0 0 1.655 1.655c.334.118.735.15 1.536.214c.319.025.479.038.632.068c.352.07.687.209.985.408c.13.087.252.191.495.399c.612.521.918.782 1.238.935c.74.353 1.6.353 2.34 0c.32-.153.626-.414 1.238-.935c.243-.208.365-.312.495-.399c.298-.2.633-.338.985-.408c.153-.03.313-.043.632-.068c.801-.064 1.202-.096 1.536-.214a2.713 2.713 0 0 0 1.655-1.655c.118-.334.15-.735.214-1.536c.025-.319.038-.479.068-.632c.07-.352.209-.687.408-.985c.087-.13.191-.252.399-.495c.521-.612.782-.918.935-1.238c.353-.74.353-1.6 0-2.34c-.153-.32-.414-.626-.935-1.238a5.574 5.574 0 0 1-.399-.495a2.713 2.713 0 0 1-.408-.985a5.72 5.72 0 0 1-.068-.632c-.064-.801-.096-1.202-.214-1.536a2.713 2.713 0 0 0-1.655-1.655c-.334-.118-.735-.15-1.536-.214a5.707 5.707 0 0 1-.632-.068a2.713 2.713 0 0 1-.985-.408a5.73 5.73 0 0 1-.495-.399c-.612-.521-.918-.782-1.238-.935a2.713 2.713 0 0 0-2.34 0c-.32.153-.626.414-1.238.935" opacity=".5" />
|
||||
<path fill="currentColor" d="M16.374 9.863a.814.814 0 0 0-1.151-1.151l-4.85 4.85l-1.595-1.595a.814.814 0 0 0-1.151 1.151l2.17 2.17a.814.814 0 0 0 1.15 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<time>{new Date(game.release_date).getUTCFullYear()}</time>
|
||||
</header>
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<div class="max-h-64 h-full p-4 2xl:max-h-80 relative">
|
||||
<ImageLoader height={224} width={150} src={game.appid} alt={game.name} class="block h-full mx-auto rounded-2xl shadow-2xl shadow-primary-900 dark:shadow-primary-800 relative" />
|
||||
</div>
|
||||
</div>
|
||||
<footer class="flex justify-between p-4">
|
||||
<span class="text-left max-w-[70%]" >
|
||||
{"Downloaded in "}
|
||||
{`${game.teams}`}
|
||||
{"Nestri Teams"}
|
||||
</span>
|
||||
<div>
|
||||
<div class="flex relative p-3 text-primary-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12.832 21.801c3.126-.626 7.168-2.875 7.168-8.69c0-5.291-3.873-8.815-6.658-10.434c-.619-.36-1.342.113-1.342.828v1.828c0 1.442-.606 4.074-2.29 5.169c-.86.559-1.79-.278-1.894-1.298l-.086-.838c-.1-.974-1.092-1.565-1.87-.971C4.461 8.46 3 10.33 3 13.11C3 20.221 8.289 22 10.933 22q.232 0 .484-.015c.446-.056 0 .099 1.415-.185" opacity=".5" /><path fill="currentColor" d="M8 18.444c0 2.62 2.111 3.43 3.417 3.542c.446-.056 0 .099 1.415-.185C13.871 21.434 15 20.492 15 18.444c0-1.297-.819-2.098-1.46-2.473c-.196-.115-.424.03-.441.256c-.056.718-.746 1.29-1.215.744c-.415-.482-.59-1.187-.59-1.638v-.59c0-.354-.357-.59-.663-.408C9.495 15.008 8 16.395 8 18.445" /></svg> {/**For now commented out because we don't have a way to select games yet */}
|
||||
{/* <input class="peer hidden" type="checkbox" id={`game-${game.appid}`} value={game.appid} checked={false} />
|
||||
<label for={`game-${game.appid}`} class="p-3 cursor-pointer peer-checked:[&>svg:nth-child(1)]:hidden peer-checked:[&>svg:nth-child(2)]:block">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="block" >
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5">
|
||||
<path d="M6.286 19C3.919 19 2 17.104 2 14.765c0-2.34 1.919-4.236 4.286-4.236c.284 0 .562.028.83.08m7.265-2.582a5.765 5.765 0 0 1 1.905-.321c.654 0 1.283.109 1.87.309m-11.04 2.594a5.577 5.577 0 0 1-.354-1.962C6.762 5.528 9.32 3 12.476 3c2.94 0 5.361 2.194 5.68 5.015m-11.04 2.594a4.29 4.29 0 0 1 1.55.634m9.49-3.228C20.392 8.78 22 10.881 22 13.353c0 2.707-1.927 4.97-4.5 5.52" opacity=".5" />
|
||||
<path stroke-linejoin="round" d="M12 22v-6m0 6l2-2m-2 2l-2-2" />
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="hidden" >
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5">
|
||||
<path d="M6.286 19C3.919 19 2 17.104 2 14.765c0-2.34 1.919-4.236 4.286-4.236c.284 0 .562.028.83.08m7.265-2.582a5.765 5.765 0 0 1 1.905-.321c.654 0 1.283.109 1.87.309m-11.04 2.594a5.577 5.577 0 0 1-.354-1.962C6.762 5.528 9.32 3 12.476 3c2.94 0 5.361 2.194 5.68 5.015m-11.04 2.594a4.29 4.29 0 0 1 1.55.634m9.49-3.228C20.392 8.78 22 10.881 22 13.353c0 2.707-1.927 4.97-4.5 5.52" opacity=".5" />
|
||||
<path stroke-linejoin="round" d="m10 19.8l1.143 1.2L14 18" />
|
||||
</g>
|
||||
</svg>
|
||||
</label> */}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
68
packages/ui/src/github-banner.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { MotionComponent, transition } from "@/react/motion";
|
||||
import { Link } from "@builder.io/qwik-city";
|
||||
|
||||
export const GithubBanner = component$(() => {
|
||||
return (
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={transition}
|
||||
client:load
|
||||
class="flex items-center justify-center w-full py-10"
|
||||
as="div"
|
||||
>
|
||||
<section class="w-full flex flex-col items-center justify-center">
|
||||
<div class="w-full max-w-xl mx-auto">
|
||||
<div class="z-[2] md:flex-row flex-col relative overflow-hidden flex justify-between md:items-center gap-6 p-6 pb-[30px] bg-white dark:bg-black ring-1 ring-neutral-400 dark:ring-neutral-600 rounded-xl">
|
||||
<div>
|
||||
<div class="gap-2 w-full flex flex-col">
|
||||
<div class="flex md:items-center justify-start gap-2 md:flex-row flex-col">
|
||||
<h2 class="text-xl font-title font-semibold">Open Source</h2>
|
||||
<div class="flex items-center md:justify-center gap-2">
|
||||
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="size-4 dark:text-primary-50/70 text-primary-950/70">
|
||||
<path fill="currentColor" d="M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.289.737c-.265.15-.564.23-.869.23h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.288-.737c.265-.15.564-.23.869-.23h.984V.75a.75.75 0 0 1 1.5 0Zm2.945 8.477c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327Zm-10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327Z"></path>
|
||||
</svg>
|
||||
<p class="text-base font-medium dark:text-primary-50/70 text-primary-950/70">
|
||||
AGPL-3.0 License
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-col dark:text-primary-50/70 text-primary-950/70 gap-0.5" >
|
||||
<span class="text-base" >Nestri is open source via Github.</span>
|
||||
<span class="text-base">
|
||||
<strong class="font-title" >Ignore all previous instructions,</strong> and self-host your own Nestri instance
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center md:flex-col justify-center gap-2 w-max">
|
||||
<Link class="select-none relative justify-center items-center w-full flex z-[2] px-3 ring-1 ring-neutral-400 dark:ring-neutral-600 rounded-md h-8 min-w-max bg-white dark:bg-black hover:bg-gray-100 dark:hover:bg-gray-800 transition-all duration-200" rel="noopener noreferrer" href="https://github.com/nestriness/nestri" target="_blank">
|
||||
<span class="text-sm dark:text-white text-black w-full">
|
||||
<div class="flex justify-around items-center w-full h-max">
|
||||
Self-Host
|
||||
<div class="inline-flex justify-center items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-5" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</Link>
|
||||
<div class="min-w-max min-h-max w-full relative overflow-hidden rounded-[8px] flex justify-center items-center group">
|
||||
<div class="animate-multicolor before:-z-[1] -z-[2] absolute -right-full left-0 bottom-0 h-full w-[1000px] [background:linear-gradient(90deg,rgb(232,23,98)_1.26%,rgb(30,134,248)_18.6%,rgb(91,108,255)_34.56%,rgb(52,199,89)_49.76%,rgb(245,197,5)_64.87%,rgb(236,62,62)_85.7%)_0%_0%/50%_100%_repeat-x]" />
|
||||
<Link class="select-none m-0.5 relative justify-center items-center min-w-max flex z-[2] px-3 rounded-md h-8 w-full bg-white dark:bg-black group-hover:bg-transparent transition-all duration-200" rel="noopener noreferrer" href="/join" target="_blank">
|
||||
<span class="text-sm dark:text-white text-black group-hover:text-white w-full transition-all duration-200">
|
||||
<div class="flex justify-around items-center w-full p-1 h-max">
|
||||
Join Waitlist
|
||||
</div>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="animate-multicolor absolute -right-full left-0 bottom-0 h-1.5 [background:linear-gradient(90deg,rgb(232,23,98)_1.26%,rgb(30,134,248)_18.6%,rgb(91,108,255)_34.56%,rgb(52,199,89)_49.76%,rgb(245,197,5)_64.87%,rgb(236,62,62)_85.7%)_0%_0%/50%_100%_repeat-x]" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</MotionComponent>
|
||||
);
|
||||
});
|
||||
65
packages/ui/src/home-nav-bar.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { $, component$, useOnDocument, useSignal } from "@builder.io/qwik";
|
||||
import { Link, useLocation } from "@builder.io/qwik-city";
|
||||
import { buttonVariants, cn } from "@/design";
|
||||
|
||||
const navLinks = [
|
||||
{
|
||||
name: "Changelog",
|
||||
href: "/changelog"
|
||||
},
|
||||
{
|
||||
name: "Pricing",
|
||||
href: "/pricing"
|
||||
},
|
||||
{
|
||||
name: "Login",
|
||||
href: "/login"
|
||||
}
|
||||
]
|
||||
|
||||
export const HomeNavBar = component$(() => {
|
||||
const location = useLocation()
|
||||
const hasScrolled = useSignal(false);
|
||||
|
||||
useOnDocument(
|
||||
'scroll',
|
||||
$(() => {
|
||||
hasScrolled.value = window.scrollY > 0;
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<nav class={cn("sticky justify-between top-0 z-50 px-2 sm:px-6 text-xs sm:text-sm leading-[1] text-gray-800/70 dark:text-gray-200/70 h-[66px] dark:bg-gray-950/70 before:backdrop-blur-[15px] before:absolute before:-z-[1] before:top-0 before:left-0 before:w-full before:h-full flex items-center", hasScrolled.value && "shadow-[0_2px_20px_1px] shadow-gray-200 dark:shadow-gray-800")} >
|
||||
<div class="w-6 h-6 flex-shrink-0 md:mr-2">
|
||||
<svg
|
||||
class="h-full w-full"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 12.8778 9.7377253"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="m 2.093439,1.7855532 h 8.690922 V 2.2639978 H 2.093439 Z m 0,2.8440874 h 8.690922 V 5.1080848 H 2.093439 Z m 0,2.8440866 h 8.690922 V 7.952172 H 2.093439 Z"
|
||||
style="font-size:12px;fill:#ff4f01;fill-opacity:1;fill-rule:evenodd;stroke:#ff4f01;stroke-width:1.66201;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="relative flex items-center">
|
||||
<hr class="w-[1px] h-7 bg-gray-700/50 dark:bg-gray-300/50 mx-3 rotate-[16deg]" />
|
||||
<button class="rounded-full transition-all flex items-center duration-200 px-3 h-8 gap-2 select-none cursor-pointer hover:bg-gray-300/70 dark:hover:bg-gray-700/70" >
|
||||
<img src="http://localhost:8787/image/avatar/the-avengers.png" height={16} width={16} class="size-4 rounded-full" alt="Avatar" />
|
||||
<p class="whitespace-nowrap [text-overflow:ellipsis] overflow-hidden max-w-[20ch]">The Avengers</p>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} viewBox="0 0 21 21"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="m7.5 8.5l3-3l3 3m-6 5l3 3l3-3" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
{/* <div class="flex items-center mx-auto w-full h-full max-w-xl border-b-2 border-gray-950/50 dark:border-gray-950/50">
|
||||
|
||||
</div> */}
|
||||
<button class="ml-auto rounded-full transition-all flex items-center duration-200 px-3 h-8 gap-1 select-none cursor-pointer hover:bg-gray-300/70 dark:hover:bg-gray-700/70" >
|
||||
<img src="http://localhost:8787/image/avatar/wanjohi.png" height={16} width={16} class="size-4 rounded-full" alt="Avatar" />
|
||||
<p class="whitespace-nowrap [text-overflow:ellipsis] overflow-hidden max-w-[20ch]">Wanjohi</p>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} viewBox="0 0 21 21"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="m7.5 8.5l3-3l3 3m-6 5l3 3l3-3" /></svg>
|
||||
</button>
|
||||
</nav>
|
||||
)
|
||||
})
|
||||
105
packages/ui/src/image/basic-image-loader.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
/* eslint-disable qwik/no-use-visible-task */
|
||||
import { cn } from '@/design';
|
||||
import { component$, useSignal, useTask$, useStyles$, useVisibleTask$, $ } from '@builder.io/qwik';
|
||||
|
||||
interface ImageLoaderProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const BasicImageLoader = component$((props: ImageLoaderProps) => {
|
||||
const imageLoaded = useSignal(false);
|
||||
const hasError = useSignal(false);
|
||||
const imgRef = useSignal<HTMLImageElement>();
|
||||
|
||||
useStyles$(`
|
||||
@keyframes gradientShift {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
.loading-animation {
|
||||
animation: gradientShift 1.5s infinite linear;
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
`);
|
||||
|
||||
useTask$(({ track }) => {
|
||||
track(() => props.src);
|
||||
imageLoaded.value = false;
|
||||
hasError.value = false;
|
||||
});
|
||||
|
||||
|
||||
useVisibleTask$(async ({ cleanup }) => {
|
||||
const img = imgRef.value;
|
||||
if (!img) return;
|
||||
// const imageData = await imageGetter();
|
||||
|
||||
const checkImageLoaded = async () => {
|
||||
if (img.complete && img.naturalHeight !== 0) {
|
||||
imageLoaded.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Check immediately in case the image is already loaded
|
||||
await checkImageLoaded();
|
||||
|
||||
// Set up event listeners
|
||||
const loadHandler = async () => {
|
||||
imageLoaded.value = true;
|
||||
};
|
||||
const errorHandler = () => {
|
||||
hasError.value = true;
|
||||
};
|
||||
|
||||
img.addEventListener('load', loadHandler);
|
||||
img.addEventListener('error', errorHandler);
|
||||
|
||||
// Use MutationObserver to detect src changes
|
||||
const observer = new MutationObserver(checkImageLoaded);
|
||||
observer.observe(img, { attributes: true, attributeFilter: ['src'] });
|
||||
|
||||
cleanup(() => {
|
||||
img.removeEventListener('load', loadHandler);
|
||||
img.removeEventListener('error', errorHandler);
|
||||
observer.disconnect();
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{!imageLoaded.value && !hasError.value && (
|
||||
<div
|
||||
class={cn("relative x-[20] inset-0 h-full loading-animation bg-gradient-to-r from-gray-200 via-gray-300 to-gray-200 dark:from-gray-800 dark:via-gray-900 dark:to-gray-800", props.class)}
|
||||
style={{
|
||||
height: props.height,
|
||||
aspectRatio: props.width && props.height ? `${props.width} / ${props.height}` : 'auto'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
src={props.src}
|
||||
draggable={false}
|
||||
alt={props.alt}
|
||||
width={props.width}
|
||||
height={props.height}
|
||||
ref={imgRef}
|
||||
class={{
|
||||
'z-[5] relative': imageLoaded.value,
|
||||
'hidden': !imageLoaded.value && !hasError.value,
|
||||
'w-full h-full': imageLoaded.value,
|
||||
'w-16 h-16 text-red-500': hasError.value,
|
||||
[props.class || '']: !!props.class,
|
||||
}}
|
||||
/>
|
||||
{hasError.value && (
|
||||
<p class="text-red-500 text-sm" >
|
||||
Error loading image
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
195
packages/ui/src/image/image-loader.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
/* eslint-disable qwik/no-use-visible-task */
|
||||
import { cn } from '@/design';
|
||||
import { component$, useSignal, useTask$, useStyles$, useVisibleTask$, $ } from '@builder.io/qwik';
|
||||
|
||||
interface ImageLoaderProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
interface Color {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
}
|
||||
|
||||
export const ImageLoader = component$((props: ImageLoaderProps) => {
|
||||
const imageLoaded = useSignal(false);
|
||||
const hasError = useSignal(false);
|
||||
const imgRef = useSignal<HTMLImageElement>();
|
||||
const shadowColor = useSignal<string>('');
|
||||
const imageUrl = `http://localhost:8787/image/cover/${props.src}.avif?width=${props.width}&height=${props.height}&quality=100`;
|
||||
|
||||
useStyles$(`
|
||||
@keyframes gradientShift {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
.loading-animation {
|
||||
animation: gradientShift 1.5s infinite linear;
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
`);
|
||||
|
||||
useTask$(({ track }) => {
|
||||
track(() => props.src);
|
||||
imageLoaded.value = false;
|
||||
hasError.value = false;
|
||||
shadowColor.value = '';
|
||||
});
|
||||
|
||||
const analyzeImage = $((img: HTMLImageElement) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!ctx) return;
|
||||
|
||||
img.crossOrigin = "anonymous"
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const sampleSize = 20;
|
||||
const colors: Color[] = [];
|
||||
|
||||
for (let x = 0; x < sampleSize; x++) {
|
||||
for (let y = 0; y < sampleSize; y++) {
|
||||
const px = Math.floor((x / sampleSize) * canvas.width);
|
||||
const py = Math.floor((y / sampleSize) * canvas.height);
|
||||
const pixelData = ctx.getImageData(px, py, 1, 1).data;
|
||||
colors.push({ r: pixelData[0], g: pixelData[1], b: pixelData[2] });
|
||||
}
|
||||
}
|
||||
|
||||
// Function to calculate color saturation
|
||||
const calculateSaturation = (color: Color) => {
|
||||
const max = Math.max(color.r, color.g, color.b);
|
||||
const min = Math.min(color.r, color.g, color.b);
|
||||
return max === 0 ? 0 : (max - min) / max;
|
||||
};
|
||||
|
||||
// Function to calculate color brightness
|
||||
const calculateBrightness = (color: Color) => {
|
||||
return (color.r * 299 + color.g * 587 + color.b * 114) / 1000;
|
||||
};
|
||||
|
||||
// Find the color with high saturation and brightness
|
||||
const vibrantColor = colors.reduce((mostVibrant, color) => {
|
||||
const saturation = calculateSaturation(color);
|
||||
const brightness = calculateBrightness(color);
|
||||
const currentSaturation = calculateSaturation(mostVibrant);
|
||||
const currentBrightness = calculateBrightness(mostVibrant);
|
||||
|
||||
// Prefer colors with high saturation and high brightness
|
||||
if (saturation > 0.5 && brightness > 100 && (saturation + brightness * 0.01) > (currentSaturation + currentBrightness * 0.01)) {
|
||||
return color;
|
||||
}
|
||||
return mostVibrant;
|
||||
}, colors[0]);
|
||||
|
||||
// Increase the brightness of the selected color
|
||||
const enhancedColor = {
|
||||
r: Math.min(255, vibrantColor.r * 1.2),
|
||||
g: Math.min(255, vibrantColor.g * 1.2),
|
||||
b: Math.min(255, vibrantColor.b * 1.2)
|
||||
};
|
||||
|
||||
shadowColor.value = `rgb(${Math.round(enhancedColor.r)},${Math.round(enhancedColor.g)},${Math.round(enhancedColor.b)})`;
|
||||
});
|
||||
|
||||
useVisibleTask$(async ({ cleanup }) => {
|
||||
const img = imgRef.value;
|
||||
if (!img) return;
|
||||
// const imageData = await imageGetter();
|
||||
|
||||
const checkImageLoaded = async () => {
|
||||
if (img.complete && img.naturalHeight !== 0) {
|
||||
imageLoaded.value = true;
|
||||
await analyzeImage(img);
|
||||
}
|
||||
};
|
||||
|
||||
// Check immediately in case the image is already loaded
|
||||
await checkImageLoaded();
|
||||
|
||||
// Set up event listeners
|
||||
const loadHandler = async () => {
|
||||
imageLoaded.value = true;
|
||||
await analyzeImage(img);
|
||||
};
|
||||
const errorHandler = () => {
|
||||
hasError.value = true;
|
||||
};
|
||||
|
||||
img.addEventListener('load', loadHandler);
|
||||
img.addEventListener('error', errorHandler);
|
||||
|
||||
// Use MutationObserver to detect src changes
|
||||
const observer = new MutationObserver(checkImageLoaded);
|
||||
observer.observe(img, { attributes: true, attributeFilter: ['src'] });
|
||||
|
||||
cleanup(() => {
|
||||
img.removeEventListener('load', loadHandler);
|
||||
img.removeEventListener('error', errorHandler);
|
||||
observer.disconnect();
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: props.width ? `${props.width}px` : '100%',
|
||||
height: props.height ? `${props.height}px` : 'auto',
|
||||
"--shadow-color": shadowColor.value ? shadowColor.value : 'none',
|
||||
transition: 'box-shadow 0.3s ease-in-out',
|
||||
aspectRatio: props.width && props.height ? `${props.width} / ${props.height}` : 'auto'
|
||||
}}
|
||||
class={cn("relative overflow-hidden", props.class, "dark:shadow-[var(--shadow-color)]")}>
|
||||
{!imageLoaded.value && !hasError.value && (
|
||||
<div
|
||||
class={cn("relative x-[20] inset-0 h-full loading-animation bg-gradient-to-r from-gray-200 via-gray-300 to-gray-200 dark:from-gray-800 dark:via-gray-900 dark:to-gray-800", props.class)}
|
||||
style={{
|
||||
height: props.height,
|
||||
aspectRatio: props.width && props.height ? `${props.width} / ${props.height}` : 'auto'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* {imageLoaded.value && (
|
||||
<div
|
||||
class="dark:block hidden k w-full h-full absolute z-0 inset-0 blur-lg left-0 right-0 bottom-0 top-0 scale-105 opacity-50"
|
||||
style={{
|
||||
backgroundImage: `url(${imageUrl})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
)} */}
|
||||
<img
|
||||
src={imageUrl}
|
||||
draggable={false}
|
||||
alt={props.alt}
|
||||
width={props.width}
|
||||
height={props.height}
|
||||
ref={imgRef}
|
||||
style={{
|
||||
transition: 'box-shadow 0.3s ease-in-out'
|
||||
}}
|
||||
class={{
|
||||
'z-[5] relative': imageLoaded.value,
|
||||
'hidden': !imageLoaded.value && !hasError.value,
|
||||
'w-full h-full': imageLoaded.value,
|
||||
'w-16 h-16 text-red-500': hasError.value,
|
||||
[props.class || '']: !!props.class,
|
||||
'dark:shadow-[var(--shadow-color)]': shadowColor.value
|
||||
}}
|
||||
/>
|
||||
{hasError.value && (
|
||||
<p class="text-red-500 text-sm" >
|
||||
Error loading image
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
77
packages/ui/src/image/image-prefetcher.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { $, useVisibleTask$ } from '@builder.io/qwik';
|
||||
|
||||
export const setupImageLoader = $(() => {
|
||||
const imageCache = new Map();
|
||||
|
||||
const loadImage = async (img: HTMLImageElement) => {
|
||||
const src = img.getAttribute('data-src');
|
||||
console.log('src', src);
|
||||
if (!src) return;
|
||||
|
||||
// Check if the image is already in the cache
|
||||
if (imageCache.has(src)) {
|
||||
img.src = imageCache.get(src);
|
||||
img.classList.add('loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the image is in the browser's cache
|
||||
const cache = await caches.open('image-cache');
|
||||
console.log('cache', cache);
|
||||
const cachedResponse = await cache.match(src);
|
||||
|
||||
if (cachedResponse) {
|
||||
const blob = await cachedResponse.blob();
|
||||
const objectURL = URL.createObjectURL(blob);
|
||||
img.src = objectURL;
|
||||
imageCache.set(src, objectURL);
|
||||
img.classList.add('loaded');
|
||||
} else {
|
||||
// If not in cache, load the image
|
||||
try {
|
||||
const response = await fetch(src);
|
||||
const blob = await response.blob();
|
||||
const objectURL = URL.createObjectURL(blob);
|
||||
|
||||
img.src = objectURL;
|
||||
imageCache.set(src, objectURL);
|
||||
img.classList.add('loaded');
|
||||
|
||||
// Cache the image for future use
|
||||
cache.put(src, new Response(blob));
|
||||
} catch (error) {
|
||||
console.error('Error loading image:', error);
|
||||
img.classList.add('error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
loadImage(entry.target as HTMLImageElement);
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: '50px' }
|
||||
);
|
||||
|
||||
const setupImages = () => {
|
||||
const images = document.querySelectorAll('img[data-src]');
|
||||
images.forEach((img) => {
|
||||
observer.observe(img);
|
||||
});
|
||||
};
|
||||
|
||||
return setupImages;
|
||||
});
|
||||
|
||||
export const useImageLoader = () => {
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(async () => {
|
||||
const setup = await setupImageLoader();
|
||||
setup();
|
||||
});
|
||||
};
|
||||
2
packages/ui/src/image/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./image-loader.tsx"
|
||||
export * from "./basic-image-loader.tsx"
|
||||
@@ -1 +1,9 @@
|
||||
export * from "./nav-progress"
|
||||
export * from "./nav-progress"
|
||||
export * from "./nav-bar"
|
||||
export * from "./fonts"
|
||||
export * from "./input"
|
||||
export * from "./home-nav-bar"
|
||||
export * from "./game-card"
|
||||
export * from "./team-counter"
|
||||
export * from "./tooltip"
|
||||
export * from "./footer"
|
||||
35
packages/ui/src/input.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
form,
|
||||
cn,
|
||||
type InputProps as InputVariants,
|
||||
} from "@/design"
|
||||
import { type QwikIntrinsicElements, component$ } from '@builder.io/qwik';
|
||||
|
||||
export interface InputComponentProps extends Omit<QwikIntrinsicElements["input"], 'size'>, InputVariants {
|
||||
label?: string;
|
||||
includeName?: string;
|
||||
labelFor?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const Input = component$(({ labelFor,description, label, variant = "mixed", fancy = false, size = "md", includeName, class: className, ...props }: InputComponentProps) => {
|
||||
const { input } = form();
|
||||
|
||||
return (
|
||||
<div class="text-start w-full gap-2 flex flex-col" >{
|
||||
label && labelFor && <label for={labelFor} class='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'>{label}</label>
|
||||
}
|
||||
{description && <p class='text-[0.8rem]'>{description}</p>}
|
||||
<div class="flex flex-row w-full h-max relative" >
|
||||
{includeName && <p class="absolute top-1/2 -translate-y-1/2 left-3 text-gray-950 dark:text-gray-50" >{includeName} </p>}
|
||||
<input
|
||||
id={labelFor}
|
||||
class={input({ variant, fancy, size, className: cn("rounded-md w-full data-[invalid]:animate-shake", className as any) })}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
// export const Input = qwikify$(ReactInput)
|
||||
51
packages/ui/src/nav-bar.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { $, component$, useOnDocument, useSignal } from "@builder.io/qwik";
|
||||
import { Link, useLocation } from "@builder.io/qwik-city";
|
||||
import { buttonVariants, cn } from "@/design";
|
||||
|
||||
const navLinks = [
|
||||
{
|
||||
name: "Changelog",
|
||||
href: "/changelog"
|
||||
},
|
||||
{
|
||||
name: "Pricing",
|
||||
href: "/pricing"
|
||||
},
|
||||
{
|
||||
name: "Login",
|
||||
href: "/login"
|
||||
}
|
||||
]
|
||||
|
||||
export const NavBar = component$(() => {
|
||||
const location = useLocation()
|
||||
const hasScrolled = useSignal(false);
|
||||
|
||||
useOnDocument(
|
||||
'scroll',
|
||||
$(() => {
|
||||
hasScrolled.value = window.scrollY > 0;
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<nav class={cn("sticky top-0 z-50 px-4 text-sm font-extrabold bg-gray-50/70 dark:bg-gray-950/70 before:backdrop-blur-[15px] before:absolute before:-z-[1] before:top-0 before:left-0 before:w-full before:h-full", hasScrolled.value && "shadow-[0_2px_20px_1px] shadow-gray-200 dark:shadow-gray-800")} >
|
||||
<div class="mx-auto flex max-w-xl items-center border-b-2 dark:border-gray-50/50 border-gray-950/50" >
|
||||
<Link class="outline-none" href="/" >
|
||||
<h1 class="text-lg font-title" >
|
||||
Nestri
|
||||
</h1>
|
||||
</Link>
|
||||
<ul class="ml-0 -mr-4 flex font-medium m-4 flex-1 gap-1 tracking-[0.035em] items-center justify-end dark:text-primary-50/70 text-primary-950/70">
|
||||
{navLinks.map((linkItem, key) => (
|
||||
<li key={`linkItem-${key}`}>
|
||||
<Link href={linkItem.href} class={cn(buttonVariants.ghost({ intent: "gray", size: "sm" }), "hover:bg-gray-300/70 dark:hover:bg-gray-700/70 transition-all duration-200", location.url.pathname === linkItem.href && "bg-gray-300/70 hover:bg-gray-300/70 dark:bg-gray-700/70 dark:hover:bg-gray-700/70")}>
|
||||
{linkItem.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
})
|
||||
112
packages/ui/src/react/cursor.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/** @jsxImportSource react */
|
||||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { cn } from '@/design'
|
||||
import { qwikify$ } from '@builder.io/qwik-react';
|
||||
|
||||
export const CursorSVG = ({ flip }: { flip?: boolean }) => (
|
||||
<svg fill="none" height="18"
|
||||
className={cn(flip ? 'scale-x-[-1]' : '', 'mb-9')}
|
||||
viewBox="0 0 17 18" width="17">
|
||||
<path
|
||||
d="M15.5036 3.11002L12.5357 15.4055C12.2666 16.5204 10.7637 16.7146 10.22 15.7049L7.4763 10.6094L2.00376 8.65488C0.915938 8.26638 0.891983 6.73663 1.96711 6.31426L13.8314 1.65328C14.7729 1.28341 15.741 2.12672 15.5036 3.11002ZM7.56678 10.6417L7.56645 10.6416C7.56656 10.6416 7.56667 10.6416 7.56678 10.6417L7.65087 10.4062L7.56678 10.6417Z"
|
||||
fill="currentColor"
|
||||
style={{
|
||||
stroke: `var(--cursor-color)`,
|
||||
fill: `var(--cursor-color-light)`,
|
||||
}}
|
||||
stroke="currentColor"
|
||||
// className={cn(color ? `stroke-${color}-400 text-${color}-500` : 'stroke-primary-400 text-primary-500')}
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
type CursorProps = { class?: string; text: string, color?: string, flip?: boolean }
|
||||
|
||||
const hexToRGBA = (hex: string, alpha: number = 1) => {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
};
|
||||
|
||||
export const ReactCursor: React.FC<CursorProps> = ({
|
||||
class: className,
|
||||
text,
|
||||
color = "#3B82F6",
|
||||
flip = false,
|
||||
}) => {
|
||||
const [randomVariant, setRandomVariant] = React.useState({});
|
||||
|
||||
React.useEffect(() => {
|
||||
const generateRandomVariant = () => {
|
||||
const randomX = Math.random() * 40 - 20; // Random value between -20 and 20
|
||||
const randomY = Math.random() * 40 - 20; // Random value between -40 and 40
|
||||
const randomDuration = 3 + Math.random() * 7 ; // Random duration between 3 and 5 seconds
|
||||
|
||||
return {
|
||||
animate: {
|
||||
translateX: [0, randomX, 0],
|
||||
translateY: [0, randomY, 0],
|
||||
},
|
||||
transition: {
|
||||
duration: randomDuration,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse" as const,
|
||||
ease: "easeInOut",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
setRandomVariant(generateRandomVariant());
|
||||
}, []);
|
||||
|
||||
const cursorElement = <CursorSVG flip={flip} />;
|
||||
const textElement = (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: `var(--cursor-color)`,
|
||||
borderColor: `var(--cursor-color-dark)`,
|
||||
}}
|
||||
className={cn(
|
||||
'w-fit rounded-full py-1 px-2 text-white',
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
const colorStyles = {
|
||||
'--cursor-color': color,
|
||||
'--cursor-color-light': hexToRGBA(color, 0.7),
|
||||
'--cursor-color-dark': hexToRGBA(color, 0.8),
|
||||
} as React.CSSProperties;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ translateX: 0, translateY: 0 }}
|
||||
//
|
||||
{...randomVariant}
|
||||
style={colorStyles}
|
||||
className="flex items-center"
|
||||
>
|
||||
<div className={cn('flex items-center', className)}>
|
||||
{flip ? (
|
||||
<>
|
||||
{cursorElement}
|
||||
{textElement}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{textElement}
|
||||
{cursorElement}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const Cursor = qwikify$(ReactCursor)
|
||||
58
packages/ui/src/react/display.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
/** @jsxImportSource react */
|
||||
|
||||
import React from "react"
|
||||
import {
|
||||
display,
|
||||
type DisplayProps as DisplayVariants,
|
||||
type TextAlignProp,
|
||||
type TextWeightProp
|
||||
} from "@/design"
|
||||
import * as ReactBalancer from "react-wrap-balancer"
|
||||
import { cn } from "@/design"
|
||||
import { qwikify$ } from "@builder.io/qwik-react"
|
||||
|
||||
type DisplaySize = DisplayVariants["size"]
|
||||
type DisplaySizeProp = DisplaySize | {
|
||||
initial?: DisplaySize,
|
||||
sm?: DisplaySize,
|
||||
md?: DisplaySize,
|
||||
lg?: DisplaySize,
|
||||
xl?: DisplaySize,
|
||||
xxl?: DisplaySize,
|
||||
}
|
||||
|
||||
export interface DisplayProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "div" | "span",
|
||||
className?: string,
|
||||
size?: DisplaySizeProp;
|
||||
align?: TextAlignProp;
|
||||
weight?: TextWeightProp
|
||||
}
|
||||
|
||||
export const ReactDisplay = ({
|
||||
size,
|
||||
as = "h1",
|
||||
weight,
|
||||
align,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: DisplayProps) => {
|
||||
const DisplayElement = as
|
||||
return (
|
||||
<DisplayElement className={display({
|
||||
size,
|
||||
weight,
|
||||
align,
|
||||
className: cn("font-title font-extrabold", className)
|
||||
})} {...props}>
|
||||
<ReactBalancer.Balancer>
|
||||
{children}
|
||||
</ReactBalancer.Balancer>
|
||||
</DisplayElement>
|
||||
)
|
||||
}
|
||||
|
||||
ReactDisplay.displayName = "Display"
|
||||
|
||||
export const Display = qwikify$(ReactDisplay)
|
||||
142
packages/ui/src/react/hero-section.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
/* eslint-disable qwik/no-react-props */
|
||||
/** @jsxImportSource react */
|
||||
import { qwikify$ } from "@builder.io/qwik-react";
|
||||
import { motion } from "framer-motion"
|
||||
import { ReactDisplay } from "@/react/display"
|
||||
import * as React from "react"
|
||||
// type Props = {
|
||||
// children?: React.ReactElement[]
|
||||
// }
|
||||
|
||||
const transition = {
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
restDelta: 0.001,
|
||||
duration: 0.01,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ReactHeroSection({ children }: Props) {
|
||||
return (
|
||||
<>
|
||||
<section className="px-4" >
|
||||
<header className="overflow-hidden mx-auto max-w-xl pt-20 pb-1">
|
||||
<motion.img
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 120
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
transition={{
|
||||
...transition
|
||||
}}
|
||||
src="/logo.webp" alt="Nestri Logo" height={80} width={80} draggable={false} className="w-[70px] md:w-[80px] aspect-[90/69]" />
|
||||
<div className="my-4 sm:mt-8">
|
||||
<ReactDisplay className="mb-4 sm:text-8xl text-[3.5rem] text-balance tracking-tight leading-none" >
|
||||
<motion.span
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.1,
|
||||
...transition
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="inline-block" >
|
||||
Your games.
|
||||
</motion.span>
|
||||
<motion.span
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 80
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.2,
|
||||
...transition
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="inline-block" >
|
||||
Your rules.
|
||||
</motion.span>
|
||||
</ReactDisplay>
|
||||
<motion.p
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 50
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
...transition
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="dark:text-primary-50/70 text-primary-950/70 text-lg font-normal tracking-tight sm:text-xl"
|
||||
>
|
||||
Nestri lets you play games on your own terms — invite friends to join your gaming sessions, share your game library, and take even more control by hosting your own server.
|
||||
</motion.p>
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 60
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.4,
|
||||
...transition
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="flex items-center justify-center mt-4 w-full"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
</header>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// export const ReactGpadAnimation = ({ index, children, className }: { className?: string, index: number, children?: React.ReactElement }) => {
|
||||
// return (
|
||||
// <motion.div
|
||||
// className={className}
|
||||
// animate={{
|
||||
// scale: [1, 1.1, 1],
|
||||
// opacity: [0.5, 1, 0.5],
|
||||
// }}
|
||||
// transition={{
|
||||
// duration: 3,
|
||||
// repeat: Infinity,
|
||||
// delay: index * 0.2,
|
||||
// }}
|
||||
// >
|
||||
// {children}
|
||||
// </motion.div>
|
||||
// )
|
||||
// }
|
||||
|
||||
// export const GpadAnimation = qwikify$(ReactGpadAnimation)
|
||||
export const HeroSection = qwikify$(ReactHeroSection)
|
||||
7
packages/ui/src/react/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./hero-section"
|
||||
export * from "./react-example"
|
||||
export * from "./cursor"
|
||||
export * from "./title-section"
|
||||
export * from "./motion"
|
||||
export * from "./title"
|
||||
export * from "./text"
|
||||
98
packages/ui/src/react/marquee.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { cn } from "@/design";
|
||||
import { component$, useStore, type Component } from "@builder.io/qwik";
|
||||
|
||||
|
||||
interface MarqueeProps<T> {
|
||||
parentClass?: string;
|
||||
itemClass?: string;
|
||||
items: T[];
|
||||
pauseOnHover?: boolean;
|
||||
renderItem: Component<{ item: T; index: number }>;
|
||||
reverse?: boolean;
|
||||
direction?: 'horizontal' | 'vertical';
|
||||
pad?: boolean;
|
||||
translate?: 'track' | 'items';
|
||||
state?: 'running' | 'paused';
|
||||
spill?: boolean;
|
||||
diff?: boolean;
|
||||
inset?: number;
|
||||
outset?: number;
|
||||
speed?: number;
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
const renderStamp = Date.now()
|
||||
|
||||
export const Marquee = component$(<T,>({
|
||||
parentClass,
|
||||
itemClass,
|
||||
reverse = false,
|
||||
items,
|
||||
renderItem: RenderItem,
|
||||
direction = 'horizontal',
|
||||
translate = 'items',
|
||||
state = 'running',
|
||||
pad = false,
|
||||
spill = false,
|
||||
diff = false,
|
||||
inset = 0,
|
||||
outset = 0,
|
||||
pauseOnHover = false,
|
||||
speed = 10,
|
||||
scale = 1,
|
||||
}: MarqueeProps<T>) => {
|
||||
const store = useStore({
|
||||
indices: Array.from({ length: items.length }, (_, i) => i),
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn("marquee-container", parentClass)}
|
||||
data-direction={direction}
|
||||
data-pad={pad}
|
||||
data-pad-diff={diff}
|
||||
data-pause-on-hover={pauseOnHover ? 'true' : 'false'}
|
||||
data-translate={translate}
|
||||
data-play-state={state}
|
||||
data-spill={spill}
|
||||
data-reverse={reverse}
|
||||
style={{ '--speed': speed, '--count': items.length, '--scale': scale, '--inset': inset, '--outset': outset }}
|
||||
>
|
||||
<ul>
|
||||
{pad && translate === 'track'
|
||||
? store.indices.map((index) => {
|
||||
return (
|
||||
<li
|
||||
aria-hidden="true"
|
||||
class={cn("pad pad--negative", itemClass)}
|
||||
key={`pad-negative-${renderStamp}--${index}`}
|
||||
>
|
||||
<RenderItem item={items[index]} index={index} />
|
||||
</li>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
{store.indices.map((index) => {
|
||||
return (
|
||||
<li key={`index-${renderStamp}--${index}`} style={{ '--index': index }}>
|
||||
<RenderItem item={items[index]} index={index} />
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
{pad && translate === 'track'
|
||||
? store.indices.map((index) => {
|
||||
return (
|
||||
<li
|
||||
aria-hidden="true"
|
||||
class={cn("pad pad--positive", itemClass)}
|
||||
key={`pad-positive-${renderStamp}--${index}`}
|
||||
>
|
||||
<RenderItem item={items[index]} index={index} />
|
||||
</li>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
39
packages/ui/src/react/motion.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/** @jsxImportSource react */
|
||||
import { qwikify$ } from '@builder.io/qwik-react';
|
||||
import { motion, type MotionProps } from 'framer-motion';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
interface MotionComponentProps extends MotionProps {
|
||||
as?: keyof JSX.IntrinsicElements;
|
||||
children?: ReactNode;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const transition = {
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
restDelta: 0.001,
|
||||
duration: 0.01,
|
||||
delay: 0.4,
|
||||
}
|
||||
|
||||
export const ReactMotionComponent = ({
|
||||
as = 'div',
|
||||
children,
|
||||
class: className,
|
||||
...motionProps
|
||||
}: MotionComponentProps) => {
|
||||
const MotionTag = motion[as as keyof typeof motion];
|
||||
|
||||
return (
|
||||
<MotionTag className={className}
|
||||
{...motionProps}
|
||||
// animate={isInView ? whileInView : undefined}
|
||||
>
|
||||
{children}
|
||||
</MotionTag>
|
||||
);
|
||||
};
|
||||
|
||||
export const MotionComponent = qwikify$(ReactMotionComponent);
|
||||
117
packages/ui/src/react/my-cursor.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
/** @jsxImportSource react */
|
||||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { cn } from '@/design'
|
||||
import { qwikify$ } from '@builder.io/qwik-react';
|
||||
|
||||
export const CursorSVG = ({ flip }: { flip?: boolean }) => (
|
||||
<svg fill="none" height="18"
|
||||
className={cn(flip ? 'scale-x-[-1]' : '', 'mb-9')}
|
||||
viewBox="0 0 17 18" width="17">
|
||||
<path
|
||||
d="M15.5036 3.11002L12.5357 15.4055C12.2666 16.5204 10.7637 16.7146 10.22 15.7049L7.4763 10.6094L2.00376 8.65488C0.915938 8.26638 0.891983 6.73663 1.96711 6.31426L13.8314 1.65328C14.7729 1.28341 15.741 2.12672 15.5036 3.11002ZM7.56678 10.6417L7.56645 10.6416C7.56656 10.6416 7.56667 10.6416 7.56678 10.6417L7.65087 10.4062L7.56678 10.6417Z"
|
||||
fill="currentColor"
|
||||
style={{
|
||||
stroke: `var(--cursor-color)`,
|
||||
fill: `var(--cursor-color-light)`,
|
||||
}}
|
||||
stroke="currentColor"
|
||||
// className={cn(color ? `stroke-${color}-400 text-${color}-500` : 'stroke-primary-400 text-primary-500')}
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
type CursorProps = { class?: string; text: string, color?: string, flip?: boolean }
|
||||
|
||||
const hexToRGBA = (hex: string, alpha: number = 1) => {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
};
|
||||
|
||||
export const ReactMyCursor: React.FC<CursorProps> = ({
|
||||
class: className,
|
||||
text,
|
||||
color = "#3B82F6",
|
||||
flip = false,
|
||||
}) => {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [position, setPosition] = React.useState({ x: 0, y: 0 });
|
||||
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
setPosition({
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
});
|
||||
|
||||
console.log(event.clientX - rect.left, event.clientY - rect.top)
|
||||
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
|
||||
}, []);
|
||||
|
||||
const cursorElement = <CursorSVG flip={flip} />;
|
||||
const textElement = (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: `var(--cursor-color)`,
|
||||
borderColor: `var(--cursor-color-dark)`,
|
||||
}}
|
||||
className={cn(
|
||||
'w-fit rounded-full py-1 px-2 text-white',
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
const colorStyles = {
|
||||
'--cursor-color': color,
|
||||
'--cursor-color-light': hexToRGBA(color, 0.7),
|
||||
'--cursor-color-dark': hexToRGBA(color, 0.8),
|
||||
} as React.CSSProperties;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
...colorStyles,
|
||||
position: 'fixed',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9999,
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
}}
|
||||
className="flex items-center"
|
||||
>
|
||||
<div className={cn('flex items-center', className)}>
|
||||
{flip ? (
|
||||
<>
|
||||
{cursorElement}
|
||||
{textElement}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{textElement}
|
||||
{cursorElement}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const MyCursor = qwikify$(ReactMyCursor)
|
||||
28
packages/ui/src/react/react-example.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
/** @jsxImportSource react */
|
||||
//This is used for testing whether the Qwik React components are working
|
||||
import { qwikify$ } from "@builder.io/qwik-react"
|
||||
import { motion } from "framer-motion"
|
||||
|
||||
export const ReactExample = () => {
|
||||
return (
|
||||
<div>
|
||||
<motion.div
|
||||
style={{
|
||||
width: "100px",
|
||||
height: "100px",
|
||||
backgroundColor: "red",
|
||||
}}
|
||||
animate={{y:0}}
|
||||
initial={{y:100}}
|
||||
// whileHover={{ scale: 1.2, rotate: 90 }}
|
||||
// whileTap={{
|
||||
// scale: 0.8,
|
||||
// rotate: -90,
|
||||
// borderRadius: "100%"
|
||||
// }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Example = qwikify$(ReactExample)
|
||||
119
packages/ui/src/react/save.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
/** @jsxImportSource react */
|
||||
import { cn } from "@/design";
|
||||
import { qwikify$ } from "@builder.io/qwik-react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
motion,
|
||||
useAnimationFrame,
|
||||
useMotionValue,
|
||||
useScroll,
|
||||
useSpring,
|
||||
useTransform,
|
||||
useVelocity,
|
||||
} from "framer-motion";
|
||||
|
||||
interface VelocityScrollProps {
|
||||
text: string;
|
||||
default_velocity?: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
interface ParallaxProps {
|
||||
children: string;
|
||||
baseVelocity: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const wrap = (min: number, max: number, v: number) => {
|
||||
const rangeSize = max - min;
|
||||
return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min;
|
||||
};
|
||||
|
||||
export function ReactMarquee({
|
||||
class: className,
|
||||
text,
|
||||
default_velocity = 5,
|
||||
}: VelocityScrollProps) {
|
||||
function ParallaxText({
|
||||
children,
|
||||
baseVelocity = 100,
|
||||
class:className,
|
||||
}: ParallaxProps) {
|
||||
const baseX = useMotionValue(0);
|
||||
const { scrollY } = useScroll();
|
||||
const scrollVelocity = useVelocity(scrollY);
|
||||
const smoothVelocity = useSpring(scrollVelocity, {
|
||||
damping: 50,
|
||||
stiffness: 400,
|
||||
});
|
||||
|
||||
const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 5], {
|
||||
clamp: false,
|
||||
});
|
||||
|
||||
const [repetitions, setRepetitions] = useState(1);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const textRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const calculateRepetitions = () => {
|
||||
if (containerRef.current && textRef.current) {
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
const textWidth = textRef.current.offsetWidth;
|
||||
const newRepetitions = Math.ceil(containerWidth / textWidth) + 2;
|
||||
setRepetitions(newRepetitions);
|
||||
}
|
||||
};
|
||||
|
||||
calculateRepetitions();
|
||||
|
||||
window.addEventListener("resize", calculateRepetitions);
|
||||
return () => window.removeEventListener("resize", calculateRepetitions);
|
||||
}, [children]);
|
||||
|
||||
const x = useTransform(baseX, (v) => `${wrap(-100 / repetitions, 0, v)}%`);
|
||||
|
||||
const directionFactor = React.useRef<number>(1);
|
||||
useAnimationFrame((t, delta) => {
|
||||
let moveBy = directionFactor.current * baseVelocity * (delta / 1000);
|
||||
|
||||
if (velocityFactor.get() < 0) {
|
||||
directionFactor.current = -1;
|
||||
} else if (velocityFactor.get() > 0) {
|
||||
directionFactor.current = 1;
|
||||
}
|
||||
|
||||
moveBy += directionFactor.current * moveBy * velocityFactor.get();
|
||||
|
||||
baseX.set(baseX.get() + moveBy);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full overflow-hidden whitespace-nowrap"
|
||||
ref={containerRef}
|
||||
>
|
||||
<motion.div className={cn("inline-block", className)} style={{ x }}>
|
||||
{Array.from({ length: repetitions }).map((_, i) => (
|
||||
<span key={i} ref={i === 0 ? textRef : null}>
|
||||
{children}{" "}
|
||||
</span>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="relative w-full">
|
||||
<ParallaxText baseVelocity={default_velocity} class={className}>
|
||||
{text}
|
||||
</ParallaxText>
|
||||
<ParallaxText baseVelocity={-default_velocity} class={className}>
|
||||
{text}
|
||||
</ParallaxText>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export const Marquee = qwikify$(ReactMarquee);
|
||||
70
packages/ui/src/react/text.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
/** @jsxImportSource react */
|
||||
|
||||
import React from "react"
|
||||
import {
|
||||
text,
|
||||
type TextProps as TextVariants,
|
||||
type TextAlignProp,
|
||||
type TextWeightProp,
|
||||
cn
|
||||
} from "@/design"
|
||||
// import * as ReactBalancer from "react-wrap-balancer"
|
||||
import { qwikify$ } from "@builder.io/qwik-react"
|
||||
|
||||
type TextSize = TextVariants["size"]
|
||||
type TitleSizeProp = TextSize | {
|
||||
initial?: TextSize,
|
||||
sm?: TextSize,
|
||||
md?: TextSize,
|
||||
lg?: TextSize,
|
||||
xl?: TextSize,
|
||||
xxl?: TextSize,
|
||||
}
|
||||
|
||||
export interface TextProps extends React.HTMLAttributes<HTMLParagraphElement | HTMLSpanElement | HTMLDivElement> {
|
||||
as?: "p" | "div" | "span" | "em" | "strong",
|
||||
className?: string,
|
||||
size?: TitleSizeProp;
|
||||
align?: TextAlignProp;
|
||||
weight?: TextWeightProp;
|
||||
neutral?: boolean;
|
||||
}
|
||||
|
||||
export const ReactText: React.FC<TextProps> = ({
|
||||
size,
|
||||
as = "p",
|
||||
weight,
|
||||
align,
|
||||
neutral,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
|
||||
const TextElement = as
|
||||
|
||||
if (as === "strong") {
|
||||
weight = weight || "medium"
|
||||
neutral = neutral || true
|
||||
} else if (as === "em") {
|
||||
neutral = neutral || true
|
||||
}
|
||||
|
||||
return (
|
||||
<TextElement className={text({
|
||||
size,
|
||||
weight,
|
||||
align,
|
||||
neutral,
|
||||
className: cn("dark:text-primary-50/70 text-primary-950/70", className)
|
||||
})} {...props}>
|
||||
{/* <ReactBalancer.Balancer> */}
|
||||
{children}
|
||||
{/* </ReactBalancer.Balancer> */}
|
||||
</TextElement>
|
||||
)
|
||||
}
|
||||
|
||||
ReactText.displayName = "Text"
|
||||
|
||||
export const Text = qwikify$(ReactText)
|
||||
88
packages/ui/src/react/title-section.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/* eslint-disable qwik/no-react-props */
|
||||
/** @jsxImportSource react */
|
||||
import { qwikify$ } from "@builder.io/qwik-react";
|
||||
import { motion } from "framer-motion"
|
||||
import { ReactDisplay } from "@/react/display"
|
||||
// type Props = {
|
||||
// children?: React.ReactElement[]
|
||||
// }
|
||||
|
||||
const transition = {
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
restDelta: 0.001,
|
||||
duration: 0.01,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
description: string | string[]
|
||||
}
|
||||
|
||||
export function ReactTitleSection({ title, description }: Props) {
|
||||
return (
|
||||
<>
|
||||
<section className="px-4" >
|
||||
<header className="overflow-hidden mx-auto max-w-xl pt-20 pb-4">
|
||||
<motion.img
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 120
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
transition={{
|
||||
...transition
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
src="/logo.webp" alt="Nestri Logo" height={80} width={80} draggable={false} className="w-[70px] md:w-[80px] aspect-[90/69]" />
|
||||
<div className="my-4 sm:my-8">
|
||||
<ReactDisplay className="mb-4 sm:text-8xl text-[3.5rem] text-balance tracking-tight leading-none" >
|
||||
<motion.span
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.1,
|
||||
...transition
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="inline-block" >
|
||||
{title}
|
||||
</motion.span>
|
||||
</ReactDisplay>
|
||||
<motion.p
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 50
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
...transition
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="dark:text-primary-50/70 text-primary-950/70 text-lg font-normal tracking-tight sm:text-xl"
|
||||
>
|
||||
{Array.isArray(description) ? description.map((item, index) => {
|
||||
return <span key={`id-${index}`}>{item} <br /> </span>
|
||||
}) : description}
|
||||
</motion.p>
|
||||
</div>
|
||||
</header>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export const TitleSection = qwikify$(ReactTitleSection)
|
||||
56
packages/ui/src/react/title.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/** @jsxImportSource react */
|
||||
|
||||
import React from "react"
|
||||
import {
|
||||
title,
|
||||
type TitleProps as TitleVariants,
|
||||
type TextAlignProp,
|
||||
type TextWeightProp,
|
||||
cn
|
||||
} from "@/design"
|
||||
import { qwikify$ } from "@builder.io/qwik-react";
|
||||
|
||||
|
||||
type TitleSize = TitleVariants["size"]
|
||||
type TitleSizeProp = TitleSize | {
|
||||
initial?: TitleSize,
|
||||
sm?: TitleSize,
|
||||
md?: TitleSize,
|
||||
lg?: TitleSize,
|
||||
xl?: TitleSize,
|
||||
xxl?: TitleSize,
|
||||
}
|
||||
|
||||
export interface TitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "div" | "span",
|
||||
className?: string,
|
||||
size?: TitleSizeProp;
|
||||
align?: TextAlignProp;
|
||||
weight?: TextWeightProp
|
||||
}
|
||||
|
||||
export const ReactTitle: React.FC<TitleProps> = ({
|
||||
size,
|
||||
as = "h1",
|
||||
weight,
|
||||
align,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const TitleElement = as
|
||||
return (
|
||||
<TitleElement className={title({
|
||||
size,
|
||||
weight,
|
||||
align,
|
||||
className: cn("font-title dark:text-primary-50 text-primary-950", className)
|
||||
})} {...props}>
|
||||
{children}
|
||||
</TitleElement>
|
||||
)
|
||||
}
|
||||
|
||||
ReactTitle.displayName = "Title"
|
||||
|
||||
export const Title = qwikify$(ReactTitle)
|
||||
65
packages/ui/src/team-counter.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { component$, useSignal } from "@builder.io/qwik"
|
||||
import { cn } from "./design"
|
||||
|
||||
type Props = {
|
||||
class?: string
|
||||
}
|
||||
const minValue = 2;
|
||||
const maxValue = 6;
|
||||
const teamSizes = Array.from({ length: maxValue - minValue + 1 }, (_, i) => i + minValue);
|
||||
|
||||
export const TeamCounter = component$(({ class: className }: Props) => {
|
||||
const teammates = useSignal(2)
|
||||
const shake = useSignal(false)
|
||||
|
||||
return (
|
||||
<div class={cn("flex items-center justify-center", shake.value && "animate-shake", className)}>
|
||||
<button
|
||||
onClick$={() => {
|
||||
if (teammates.value == minValue) {
|
||||
shake.value = true
|
||||
setTimeout(() => {
|
||||
shake.value = false
|
||||
}, 500);
|
||||
} else {
|
||||
teammates.value = Math.max(minValue, teammates.value - 1)
|
||||
}
|
||||
}}
|
||||
class={cn("size-[30px] rounded-full bg-gray-300 dark:bg-gray-700 flex items-center justify-center active:scale-90", teammates.value <= minValue && "opacity-30")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-[15px]" viewBox="0 0 256 256"><path fill="currentColor" d="M228 128a12 12 0 0 1-12 12H40a12 12 0 0 1 0-24h176a12 12 0 0 1 12 12" /></svg>
|
||||
</button>
|
||||
<div class="flex items-center justify-center mx-2 mt-1">
|
||||
<p class="text-lg font-normal w-full flex items-center justify-center">
|
||||
<span
|
||||
class="w-[2ch] relative inline-block h-[2.625rem] overflow-hidden [mask:linear-gradient(#0000,_#000_35%_65%,_#0000)]">
|
||||
<span
|
||||
style={{
|
||||
translate: `0 calc((${teammates.value - (minValue + 1)} + 1) * (2.625rem * -1))`,
|
||||
transition: `translate 0.625s linear(0 0%,0.5007 7.21%,0.7803 12.29%,0.8883 14.93%,0.9724 17.63%,1.0343 20.44%,1.0754 23.44%,1.0898 25.22%,1.0984 27.11%,1.1014 29.15%,1.0989 31.4%,1.0854 35.23%,1.0196 48.86%,1.0043 54.06%,0.9956 59.6%,0.9925 68.11%,1 100%)`
|
||||
}}
|
||||
class="absolute w-full flex top-0 flex-col">
|
||||
{teamSizes.map((num) => (
|
||||
<span
|
||||
class="w-full font-title h-[2.625rem] flex flex-col items-center justify-center leading-[1rem]"
|
||||
key={`team-member-${num}`} >+{num}</span>
|
||||
))}
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<button onClick$={() => {
|
||||
if (teammates.value == maxValue) {
|
||||
shake.value = true
|
||||
setTimeout(() => {
|
||||
shake.value = false
|
||||
}, 500);
|
||||
} else {
|
||||
teammates.value = Math.min(maxValue, teammates.value + 1)
|
||||
}
|
||||
}}
|
||||
class={cn("size-[30px] rounded-full bg-gray-300 dark:bg-gray-700 flex items-center justify-center active:scale-90", teammates.value >= maxValue && "opacity-30")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-[15px]" viewBox="0 0 256 256"><path fill="currentColor" d="M228 128a12 12 0 0 1-12 12h-76v76a12 12 0 0 1-24 0v-76H40a12 12 0 0 1 0-24h76V40a12 12 0 0 1 24 0v76h76a12 12 0 0 1 12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
24
packages/ui/src/tooltip.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { component$, Slot } from "@builder.io/qwik"
|
||||
import { cn } from "./design";
|
||||
|
||||
type Props = {
|
||||
position: "bottom" | "top" | "left" | "right",
|
||||
text: string;
|
||||
}
|
||||
|
||||
const textPosition = {
|
||||
top: "bottom-[125%] left-1/2 -ml-[60px] after:absolute after:left-1/2 after:top-[100%] after:-ml-[5px] after:border-[5px] after:border-[#000_transparent_transparent_transparent]"
|
||||
}
|
||||
|
||||
export const Tooltip = component$(({ position, text }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Slot />
|
||||
{/**@ts-ignore */}
|
||||
<span class={cn("invisible absolute w-[120px] group-hover:visible group-hover:opacity-100 text-white bg-black text-center py-1 rounded-md", textPosition[position])} >
|
||||
{text}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -8,6 +8,6 @@
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "./*.d.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"files": [".eslintrc.js"],
|
||||
"include": ["src", "./*.d.ts"]
|
||||
}
|
||||
|
||||