Compare commits
No commits in common. "master" and "archive/svelte" have entirely different histories.
master
...
archive/sv
50
.gitignore
vendored
@ -1,41 +1,11 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
node_modules
|
||||
/build
|
||||
/dist
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
12
.idea/Capital.iml
generated
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
63
.idea/codeStyles/Project.xml
generated
Normal file
@ -0,0 +1,63 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<HTMLCodeStyleSettings>
|
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
</HTMLCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<VueCodeStyleSettings>
|
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||
</VueCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Vue">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/Capital.iml" filepath="$PROJECT_DIR$/.idea/Capital.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
11
.prettierrc
@ -1,7 +1,8 @@
|
||||
{
|
||||
"semi": false,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 120,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
13
.roadsignrc
@ -1,13 +0,0 @@
|
||||
{
|
||||
"sync": {
|
||||
"region": "capital",
|
||||
"configPath": "roadsign.toml"
|
||||
},
|
||||
"deployments": [
|
||||
{
|
||||
"region": "capital",
|
||||
"site": "capital-app",
|
||||
"path": ".next"
|
||||
}
|
||||
]
|
||||
}
|
3
README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Capital
|
||||
|
||||
Solsynth LLC Official Website and some Users Portal.
|
36
eslint.config.js
Normal file
@ -0,0 +1,36 @@
|
||||
import js from '@eslint/js';
|
||||
import ts from 'typescript-eslint';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import globals from 'globals';
|
||||
|
||||
/** @type {import('eslint').Linter.FlatConfig[]} */
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs['flat/recommended'],
|
||||
prettier,
|
||||
...svelte.configs['flat/prettier'],
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: ts.parser
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'css-unused-selector': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['build/', '.svelte-kit/', 'dist/']
|
||||
},
|
||||
];
|
@ -1,25 +0,0 @@
|
||||
import { dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { FlatCompat } from '@eslint/eslintrc'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
})
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends('next/core-web-vitals', 'next/typescript'),
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@next/next/no-img-element': 'off',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default eslintConfig
|
3
netifly.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[build]
|
||||
command = "npm run build"
|
||||
publish = "build"
|
@ -1,32 +0,0 @@
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
reactStrictMode: true,
|
||||
output: 'standalone',
|
||||
generateBuildId: async () => {
|
||||
return process.env.GIT_HASH ?? 'development'
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'raw.sn.solsynth.dev',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'api.sn.solsynth.dev',
|
||||
},
|
||||
],
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/.well-known/:path*',
|
||||
destination: '/api/well-known/:path*',
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
106
package.json
@ -1,63 +1,59 @@
|
||||
{
|
||||
"name": "capital",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"build-standalone": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"dev": "vite dev",
|
||||
"build": "vite build && npm run package",
|
||||
"preview": "vite preview",
|
||||
"package": "svelte-kit sync && svelte-package && publint",
|
||||
"prepublishOnly": "npm run package",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/cache": "^11.14.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@mui/icons-material": "^6.3.1",
|
||||
"@mui/material": "^6.3.1",
|
||||
"@mui/material-nextjs": "^6.3.1",
|
||||
"@mui/x-charts": "^7.23.6",
|
||||
"@next/third-parties": "^15.1.6",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@toolpad/core": "^0.11.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.7.9",
|
||||
"axios-case-converter": "^1.1.1",
|
||||
"cookies-next": "^5.0.2",
|
||||
"feed": "^4.2.2",
|
||||
"next": "^15.1.5",
|
||||
"next-nprogress-bar": "^2.4.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"sitemap": "^8.0.0",
|
||||
"solar-js-sdk": "^0.1.3",
|
||||
"unified": "^11.0.5",
|
||||
"zustand": "^5.0.3"
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"svelte": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.test.*",
|
||||
"!dist/**/*.spec.*"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"svelte": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@types/node": "^20.17.12",
|
||||
"@types/react": "^19.0.4",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"daisyui": "^4.12.23",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-next": "15.1.3",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.3"
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-netlify": "^4.2.0",
|
||||
"@sveltejs/enhanced-img": "^0.2.1",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/package": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/eslint": "^8.56.7",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"daisyui": "^4.12.2",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.36.0",
|
||||
"globals": "^15.0.0",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"publint": "^0.1.9",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.0.0-alpha.20",
|
||||
"vite": "^5.0.11"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@vercel/speed-insights",
|
||||
"esbuild",
|
||||
"sharp"
|
||||
]
|
||||
"svelte": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"type": "module"
|
||||
}
|
||||
|
175
packages/sn/.gitignore
vendored
@ -1,175 +0,0 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Caches
|
||||
|
||||
.cache
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "solar-js-sdk",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"entrypoint": "dist/index.js",
|
||||
"type": "module",
|
||||
"author": {
|
||||
"name": "LittleSheep",
|
||||
"email": "littlesheep.code@hotmail.com"
|
||||
},
|
||||
"version": "0.1.3",
|
||||
"tsup": {
|
||||
"entry": [
|
||||
"src/index.ts"
|
||||
],
|
||||
"splitting": true,
|
||||
"sourcemap": true,
|
||||
"clean": true,
|
||||
"dts": true,
|
||||
"format": "esm"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"tsup": "^8.3.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"universal-cookie": "^7.2.2",
|
||||
"zustand": "^5.0.3"
|
||||
}
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
import { sni } from './network'
|
||||
|
||||
export interface SnAttachment {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
rid: string
|
||||
uuid: string
|
||||
size: number
|
||||
name: string
|
||||
alt: string
|
||||
mimetype: string
|
||||
hash: string
|
||||
destination: number
|
||||
refCount: number
|
||||
contentRating: number
|
||||
qualityRating: number
|
||||
cleanedAt?: Date | null
|
||||
isAnalyzed: boolean
|
||||
isSelfRef: boolean
|
||||
isIndexable: boolean
|
||||
ref?: SnAttachment | null
|
||||
refId?: number | null
|
||||
poolId?: number | null
|
||||
accountId: number
|
||||
thumbnailId?: number | null
|
||||
thumbnail?: SnAttachment | null
|
||||
compressedId?: number | null
|
||||
compressed?: SnAttachment | null
|
||||
usermeta: Record<string, any>
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
|
||||
export async function getAttachment(id: string | number): Promise<SnAttachment> {
|
||||
const resp = await sni.get<SnAttachment>('/cgi/uc/attachments/' + id + '/meta')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
export async function listAttachment(id: string[]): Promise<SnAttachment[]> {
|
||||
const resp = await sni.get<{ data: SnAttachment[] }>('/cgi/uc/attachments', {
|
||||
params: {
|
||||
id: id.join(','),
|
||||
take: id.length,
|
||||
},
|
||||
})
|
||||
return resp.data.data
|
||||
}
|
||||
|
||||
export type MultipartProgress = {
|
||||
value: number | null
|
||||
current: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type MultipartInfo = {
|
||||
rid: string
|
||||
fileChunks: Record<string, number>
|
||||
}
|
||||
|
||||
export class UploadAttachmentTask {
|
||||
private content: File
|
||||
private pool: string
|
||||
private multipartSize: number = 0
|
||||
private multipartInfo: MultipartInfo | null = null
|
||||
private multipartProgress: MultipartProgress = { value: null, current: 0, total: 0 }
|
||||
|
||||
onProgress?: (progress: MultipartProgress) => void
|
||||
onSuccess?: (success: boolean) => void
|
||||
onError?: (error: string) => void
|
||||
|
||||
constructor(content: File, pool: string) {
|
||||
if (!content || !pool) {
|
||||
throw new Error('Content and pool are required.')
|
||||
}
|
||||
this.content = content
|
||||
this.pool = pool
|
||||
}
|
||||
|
||||
public async submit(): Promise<SnAttachment> {
|
||||
const limit = 3
|
||||
|
||||
try {
|
||||
await this.createFragment()
|
||||
console.log(`[Paperclip] Multipart placeholder has been created with rid ${this.multipartInfo?.rid}`)
|
||||
|
||||
this.multipartProgress.value = 0
|
||||
this.multipartProgress.current = 0
|
||||
|
||||
const chunks = Object.keys(this.multipartInfo?.fileChunks || {})
|
||||
this.multipartProgress.total = chunks.length
|
||||
|
||||
let result: SnAttachment | null = null
|
||||
|
||||
const uploadChunks = async (chunk: string): Promise<void> => {
|
||||
try {
|
||||
const resp = await this.uploadOneChunk(chunk)
|
||||
this.multipartProgress.current++
|
||||
console.log(
|
||||
`[Paperclip] Uploaded multipart ${this.multipartProgress.current}/${this.multipartProgress.total}`,
|
||||
)
|
||||
this.multipartProgress.value = this.multipartProgress.current / this.multipartProgress.total
|
||||
|
||||
if (this.onProgress) this.onProgress(this.multipartProgress)
|
||||
|
||||
result = resp
|
||||
} catch (err) {
|
||||
console.log(`[Paperclip] Upload multipart ${chunk} failed, retrying in 3 seconds...`)
|
||||
await this.delay(3000)
|
||||
await uploadChunks(chunk)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < chunks.length; i += limit) {
|
||||
const chunkSlice = chunks.slice(i, i + limit)
|
||||
await Promise.all(chunkSlice.map(uploadChunks))
|
||||
}
|
||||
|
||||
console.log(`[Paperclip] Entire file has been uploaded in ${this.multipartProgress.total} chunk(s)`)
|
||||
if (this.onSuccess) this.onSuccess(true)
|
||||
|
||||
return result!
|
||||
} catch (err: any) {
|
||||
if (this.onError) this.onError(err.toString())
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
private async createFragment(): Promise<void> {
|
||||
const mimetypeMap: Record<string, string> = {
|
||||
mp4: 'video/mp4',
|
||||
mov: 'video/quicktime',
|
||||
mp3: 'audio/mp3',
|
||||
wav: 'audio/wav',
|
||||
m4a: 'audio/m4a',
|
||||
}
|
||||
|
||||
const fileExtension = this.content.name.split('.').pop() || ''
|
||||
const mimetype = mimetypeMap[fileExtension]
|
||||
|
||||
const nameArray = this.content.name.split('.')
|
||||
nameArray.pop()
|
||||
|
||||
const resp = await sni.post('/cgi/uc/fragments', {
|
||||
pool: this.pool,
|
||||
size: this.content.size,
|
||||
name: this.content.name,
|
||||
alt: nameArray.join('.'),
|
||||
mimetype,
|
||||
})
|
||||
|
||||
const data = await resp.data
|
||||
|
||||
this.multipartSize = data.chunkSize
|
||||
this.multipartInfo = data.meta
|
||||
}
|
||||
|
||||
private async uploadOneChunk(chunkId: string): Promise<SnAttachment | null> {
|
||||
if (!this.multipartInfo) return null
|
||||
|
||||
const chunkIdx = this.multipartInfo.fileChunks[chunkId]
|
||||
const chunk = this.content.slice(chunkIdx * this.multipartSize, (chunkIdx + 1) * this.multipartSize)
|
||||
|
||||
const resp = await sni.post(`/cgi/uc/fragments/${this.multipartInfo.rid}/${chunkId}`, chunk, {
|
||||
headers: { 'Content-Type': 'application/octet-stream' },
|
||||
timeout: 3 * 60 * 1000,
|
||||
})
|
||||
|
||||
if (resp.data['attachment']) {
|
||||
return resp.data['attachment'] as SnAttachment
|
||||
}
|
||||
this.multipartInfo = resp.data['fragment']
|
||||
return null
|
||||
}
|
||||
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
public static formatBytes(bytes: number, decimals = 2): string {
|
||||
if (!+bytes) return '0 Bytes'
|
||||
|
||||
const k = 1024
|
||||
const dm = decimals < 0 ? 0 : decimals
|
||||
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import Cookies from 'universal-cookie'
|
||||
|
||||
export interface SnAuthResult {
|
||||
isFinished: boolean
|
||||
ticket: SnAuthTicket
|
||||
}
|
||||
|
||||
export interface SnAuthTicket {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
stepRemain: number
|
||||
grantToken?: string | null
|
||||
accessToken?: string | null
|
||||
refreshToken?: string | null
|
||||
ipAddress: string
|
||||
location: string
|
||||
userAgent: string
|
||||
expiredAt?: Date | null
|
||||
lastGrantAt?: Date | null
|
||||
availableAt?: Date | null
|
||||
nonce?: string | null
|
||||
accountId?: number | null
|
||||
factorTrail: number[]
|
||||
}
|
||||
|
||||
export interface SnAuthFactor {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
type: number
|
||||
config?: Record<string, any> | null
|
||||
accountId?: number | null
|
||||
}
|
||||
|
||||
export function setTokenCookies(atk: string, rtk: string) {
|
||||
const cookies = new Cookies()
|
||||
cookies.set('nex_user_atk', atk, { path: '/', maxAge: 2592000 })
|
||||
cookies.set('nex_user_rtk', rtk, { path: '/', maxAge: 2592000 })
|
||||
}
|
||||
|
||||
export function removeTokenCookies() {
|
||||
const cookies = new Cookies()
|
||||
cookies.remove('nex_user_atk')
|
||||
cookies.remove('nex_user_rtk')
|
||||
}
|
||||
|
||||
export function checkAuthenticatedClient(): boolean {
|
||||
const cookies = new Cookies()
|
||||
return !!cookies.get('nex_user_atk')
|
||||
}
|
||||
|
||||
export function redirectToLogin() {
|
||||
window.open('/auth/login?redirect_uri=' + encodeURIComponent(window.location.pathname), '_self')
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
export interface SnCheckInRecord {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
resultTier: number
|
||||
resultExperience: number
|
||||
resultModifiers: number[]
|
||||
accountId: number
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
export * from './matrix/product'
|
||||
export * from './matrix/release'
|
||||
export * from './attachment'
|
||||
export * from './auth'
|
||||
export * from './checkIn'
|
||||
export * from './network'
|
||||
export * from './post'
|
||||
export * from './user'
|
@ -1,25 +0,0 @@
|
||||
export interface MaProduct {
|
||||
id: number
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
deleted_at?: Date
|
||||
icon: string
|
||||
name: string
|
||||
alias: string
|
||||
description: string
|
||||
previews: string[]
|
||||
tags: string[]
|
||||
meta: MaProductMeta
|
||||
releases: null
|
||||
account_id: number
|
||||
}
|
||||
|
||||
export interface MaProductMeta {
|
||||
id: number
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
deleted_at?: Date
|
||||
introduction: string
|
||||
attachments: string[]
|
||||
product_id: number
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
export interface MaRelease {
|
||||
id: number
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
deleted_at?: Date
|
||||
version: string
|
||||
type: number
|
||||
channel: string
|
||||
assets: Record<string, MaReleaseAsset>
|
||||
installers: Record<string, MaReleaseInstaller>
|
||||
runners: Record<string, MaReleaseRunner>
|
||||
product_id: number
|
||||
meta: MaReleaseMeta
|
||||
}
|
||||
|
||||
export interface MaReleaseMeta {
|
||||
id: number
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
deleted_at?: Date
|
||||
title: string
|
||||
description: string
|
||||
content: string
|
||||
attachments: string[]
|
||||
release_id: number
|
||||
}
|
||||
|
||||
export interface MaReleaseAsset {
|
||||
uri: string
|
||||
contentType: string
|
||||
}
|
||||
|
||||
export interface MaReleaseInstallerPatch {
|
||||
action: string
|
||||
glob: string
|
||||
}
|
||||
|
||||
export interface MaReleaseInstaller {
|
||||
workdir?: string
|
||||
script?: string
|
||||
patches: MaReleaseInstallerPatch[]
|
||||
}
|
||||
|
||||
export interface MaReleaseRunner {
|
||||
workdir?: string
|
||||
script: string
|
||||
label: string
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
import axios, { type AxiosInstance } from 'axios'
|
||||
import Cookies from 'universal-cookie'
|
||||
import { setTokenCookies } from './auth'
|
||||
|
||||
function toCamelCase(obj: any): any {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(toCamelCase)
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
return Object.keys(obj).reduce((result: any, key) => {
|
||||
const camelKey = key.replace(/_([a-z])/g, (_, char) => char.toUpperCase())
|
||||
result[camelKey] = toCamelCase(obj[key])
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
function toSnakeCase(obj: any): any {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(toSnakeCase)
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
return Object.keys(obj).reduce((result: any, key) => {
|
||||
const snakeKey = key.replace(/[A-Z]/g, (char) => `_${char.toLowerCase()}`)
|
||||
result[snakeKey] = toSnakeCase(obj[key])
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
const baseURL = 'https://api.sn.solsynth.dev'
|
||||
|
||||
export const sni: AxiosInstance = (() => {
|
||||
const inst = axios.create({
|
||||
baseURL,
|
||||
})
|
||||
|
||||
inst.interceptors.request.use(
|
||||
async (config) => {
|
||||
const tk = await refreshToken()
|
||||
if (tk) config.headers['Authorization'] = `Bearer ${tk}`
|
||||
return config
|
||||
},
|
||||
(error) => error,
|
||||
)
|
||||
|
||||
/// Case convertor
|
||||
|
||||
inst.interceptors.request.use(
|
||||
(config) => {
|
||||
if (config.data) {
|
||||
config.data = toSnakeCase(config.data)
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
)
|
||||
|
||||
inst.interceptors.response.use(
|
||||
(response) => {
|
||||
if (response.data) {
|
||||
response.data = toCamelCase(response.data)
|
||||
}
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
if (error.response && error.response.data) {
|
||||
error.response.data = toCamelCase(error.response.data)
|
||||
}
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
return inst
|
||||
})()
|
||||
|
||||
async function refreshToken(): Promise<string | undefined> {
|
||||
const cookies = new Cookies()
|
||||
if (!cookies.get('nex_user_atk') || !cookies.get('nex_user_rtk')) return
|
||||
|
||||
const ogTk: string = cookies.get('nex_user_atk')!
|
||||
if (!isTokenExpired(ogTk)) return ogTk
|
||||
|
||||
const resp = await axios.post(
|
||||
'/cgi/id/auth/token',
|
||||
{
|
||||
refresh_token: cookies.get('nex_user_rtk')!,
|
||||
grant_type: 'refresh_token',
|
||||
},
|
||||
{ baseURL },
|
||||
)
|
||||
const atk: string = resp.data['access_token']
|
||||
const rtk: string = resp.data['refresh_token']
|
||||
setTokenCookies(atk, rtk)
|
||||
|
||||
console.log('[Authenticator] Refreshed token...')
|
||||
|
||||
return atk
|
||||
}
|
||||
|
||||
function isTokenExpired(token: string): boolean {
|
||||
try {
|
||||
const parts = token.split('.')
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('Invalid JWT format')
|
||||
}
|
||||
|
||||
const payload = JSON.parse(atob(parts[1]))
|
||||
|
||||
if (!payload.exp) {
|
||||
throw new Error("'exp' claim is missing in the JWT payload")
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
|
||||
return now >= payload.exp
|
||||
} catch (error) {
|
||||
console.error('[Authenticator] Something went wrong with token: ', error)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export function getAttachmentUrl(identifer: string): string {
|
||||
if (identifer.startsWith('http')) return identifer
|
||||
return `${baseURL}/cgi/uc/attachments/${identifer}`
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
export interface SnPost {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
type: string
|
||||
body: SnPostBody & Record<string, any>
|
||||
language: string
|
||||
alias?: string | null
|
||||
aliasPrefix?: string | null
|
||||
tags: SnPostTag[]
|
||||
categories: SnPostCategory[]
|
||||
replies?: SnPost[] | null
|
||||
replyId?: number | null
|
||||
repostId?: number | null
|
||||
replyTo?: SnPost | null
|
||||
repostTo?: SnPost | null
|
||||
visibleUsersList?: number[] | null
|
||||
invisibleUsersList?: number[] | null
|
||||
visibility: number
|
||||
editedAt?: Date | null
|
||||
pinnedAt?: Date | null
|
||||
lockedAt?: Date | null
|
||||
isDraft: boolean
|
||||
publishedAt?: Date | null
|
||||
publishedUntil?: Date | null
|
||||
totalUpvote: number
|
||||
totalDownvote: number
|
||||
publisherId: number
|
||||
publisher: SnPublisher
|
||||
metric: SnMetric
|
||||
}
|
||||
|
||||
export interface SnPostTag {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date
|
||||
alias: string
|
||||
name: string
|
||||
description: string
|
||||
posts?: SnPost[]
|
||||
}
|
||||
|
||||
export interface SnPostCategory {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date
|
||||
alias: string
|
||||
name: string
|
||||
description: string
|
||||
posts?: SnPost[]
|
||||
}
|
||||
|
||||
export interface SnPostBody {
|
||||
attachments: string[]
|
||||
content: string
|
||||
location?: string
|
||||
thumbnail?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface SnMetric {
|
||||
replyCount: number
|
||||
reactionCount: number
|
||||
reactionList: Record<string, number>
|
||||
}
|
||||
|
||||
export interface SnPublisher {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
type: number
|
||||
name: string
|
||||
nick: string
|
||||
description: string
|
||||
avatar: string
|
||||
banner: string
|
||||
totalUpvote: number
|
||||
totalDownvote: number
|
||||
realmId?: number | null
|
||||
accountId: number
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import { create } from 'zustand'
|
||||
import { sni } from './network'
|
||||
import Cookies from 'universal-cookie'
|
||||
|
||||
export interface SnAccount {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
confirmedAt?: Date | null
|
||||
contacts?: SnAccountContact[] | null
|
||||
avatar: string
|
||||
banner: string
|
||||
description: string
|
||||
name: string
|
||||
nick: string
|
||||
permNodes: Record<string, any>
|
||||
profile?: SnAccountProfile | null
|
||||
badges: SnAccountBadge[]
|
||||
suspendedAt?: Date | null
|
||||
affiliatedId?: number | null
|
||||
affiliatedTo?: number | null
|
||||
automatedBy?: number | null
|
||||
automatedId?: number | null
|
||||
}
|
||||
|
||||
export interface SnAccountContact {
|
||||
accountId: number
|
||||
content: string
|
||||
createdAt: Date
|
||||
deletedAt?: Date | null
|
||||
id: number
|
||||
isPrimary: boolean
|
||||
isPublic: boolean
|
||||
type: number
|
||||
updatedAt: Date
|
||||
verifiedAt?: Date | null
|
||||
}
|
||||
|
||||
export interface SnAccountProfile {
|
||||
id: number
|
||||
accountId: number
|
||||
birthday?: Date | null
|
||||
createdAt: Date
|
||||
deletedAt?: Date | null
|
||||
experience: number
|
||||
firstName: string
|
||||
lastName: string
|
||||
lastSeenAt?: Date | null
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export interface SnAccountBadge {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
type: string
|
||||
accountId: number
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
|
||||
export interface UserStore {
|
||||
account: SnAccount | undefined
|
||||
fetchUser: () => Promise<SnAccount | undefined>
|
||||
}
|
||||
|
||||
export const useUserStore = create<UserStore>((set) => ({
|
||||
account: undefined,
|
||||
fetchUser: async (): Promise<SnAccount | undefined> => {
|
||||
const cookies = new Cookies()
|
||||
if (!cookies.get('nex_user_atk')) return
|
||||
try {
|
||||
const resp = await sni.get<SnAccount>('/cgi/id/users/me')
|
||||
set({ account: resp.data })
|
||||
console.log('[Authenticator] Logged in as @' + resp.data.name)
|
||||
return resp.data
|
||||
} catch (err) {
|
||||
console.error('[Authenticator] Unable to get user profile: ', err)
|
||||
return
|
||||
}
|
||||
},
|
||||
}))
|
@ -1,27 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
Before Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 74 KiB |
BIN
public/logo.png
Before Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 28 KiB |
@ -1,15 +0,0 @@
|
||||
id = "capital"
|
||||
|
||||
[[locations]]
|
||||
id = "capital"
|
||||
hosts = ["solsynth.dev", "www.solsynth.dev"]
|
||||
paths = ["/"]
|
||||
[[locations.destinations]]
|
||||
id = "capital-destination"
|
||||
uri = "http://localhost:3000"
|
||||
|
||||
[[applications]]
|
||||
id = "capital-app"
|
||||
workdir = "/workdir/capital"
|
||||
command = ["node", "standalone/server.js"]
|
||||
environment = ["HOSTNAME=0.0.0.0"]
|
3
src/app.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
13
src/app.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
12
src/app.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div>%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
Before Width: | Height: | Size: 696 KiB |
Before Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 2.0 MiB |
14
src/assets/solar.svg
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
src/assets/solian.png
Normal file
After Width: | Height: | Size: 537 KiB |
@ -1,90 +0,0 @@
|
||||
import { useUserStore } from 'solar-js-sdk'
|
||||
import { AppBar, AppBarProps, Avatar, IconButton, Toolbar, Typography, useScrollTrigger, useTheme } from '@mui/material'
|
||||
import { getAttachmentUrl } from 'solar-js-sdk'
|
||||
import MenuIcon from '@mui/icons-material/Menu'
|
||||
import AccountCircle from '@mui/icons-material/AccountCircle'
|
||||
import Link from 'next/link'
|
||||
import React, { useState } from 'react'
|
||||
import { CapDrawer } from './CapDrawer'
|
||||
|
||||
interface AppBarScrollProps {
|
||||
elevation?: number
|
||||
children?: React.ReactElement<{ elevation?: number } & AppBarProps>
|
||||
}
|
||||
|
||||
function AppBarScroll(props: AppBarScrollProps) {
|
||||
if (typeof window === 'undefined') return props.children
|
||||
|
||||
const trigger = useScrollTrigger({
|
||||
disableHysteresis: true,
|
||||
threshold: 0,
|
||||
target: window,
|
||||
})
|
||||
|
||||
const commonStyle = {
|
||||
transition: 'all',
|
||||
transitionDuration: '300ms',
|
||||
}
|
||||
|
||||
return props.children
|
||||
? React.cloneElement(props.children, {
|
||||
elevation: trigger ? props.elevation : 0,
|
||||
sx: trigger ? { borderBottom: 1, borderColor: 'divider', ...commonStyle } : { ...commonStyle },
|
||||
})
|
||||
: null
|
||||
}
|
||||
|
||||
export function CapAppBar() {
|
||||
const userStore = useUserStore()
|
||||
const theme = useTheme()
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const drawerWidth = 280
|
||||
|
||||
return (
|
||||
<>
|
||||
<CapDrawer width={drawerWidth} open={open} onClose={() => setOpen(false)} />
|
||||
|
||||
<AppBarScroll elevation={0}>
|
||||
<AppBar position="sticky" elevation={0} color="transparent" className="backdrop-blur-md z-10">
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
sx={{ mr: 2 }}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Link href="/" passHref style={{ flexGrow: 1 }}>
|
||||
<Typography variant="h6" component="div">
|
||||
Capital
|
||||
</Typography>
|
||||
</Link>
|
||||
|
||||
<Link href={userStore.account ? '/users/me' : '/auth/login'} passHref>
|
||||
<IconButton
|
||||
size="large"
|
||||
aria-label="account of current user"
|
||||
aria-controls="primary-search-account-menu"
|
||||
aria-haspopup="true"
|
||||
color="inherit"
|
||||
>
|
||||
{userStore.account ? (
|
||||
<Avatar sx={{ backgroundColor: 'transparent' }} src={getAttachmentUrl(userStore.account.avatar)} />
|
||||
) : (
|
||||
<Avatar sx={{ backgroundColor: 'transparent', color: theme.palette.text.primary }}>
|
||||
<AccountCircle />
|
||||
</Avatar>
|
||||
)}
|
||||
</IconButton>
|
||||
</Link>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</AppBarScroll>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Divider,
|
||||
Drawer,
|
||||
Toolbar,
|
||||
Typography,
|
||||
Link,
|
||||
} from '@mui/material'
|
||||
import { JSX } from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
import ExploreIcon from '@mui/icons-material/Explore'
|
||||
import PhotoLibraryIcon from '@mui/icons-material/PhotoLibrary'
|
||||
import AppsIcon from '@mui/icons-material/Apps'
|
||||
import NextLink from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export interface NavLink {
|
||||
title: string
|
||||
icon?: JSX.Element
|
||||
href: string
|
||||
}
|
||||
|
||||
export function CapDrawer({ width, open, onClose }: { width: number; open: boolean; onClose: () => void }) {
|
||||
const router = useRouter()
|
||||
|
||||
const functionLinks: NavLink[] = [
|
||||
{
|
||||
title: 'Explore',
|
||||
icon: <ExploreIcon />,
|
||||
href: '/posts',
|
||||
},
|
||||
{
|
||||
title: 'Gallery',
|
||||
icon: <PhotoLibraryIcon />,
|
||||
href: '/attachments',
|
||||
},
|
||||
{
|
||||
title: 'Matrix',
|
||||
icon: <AppsIcon />,
|
||||
href: '/matrix',
|
||||
},
|
||||
]
|
||||
|
||||
const additionalLinks: NavLink[] = [
|
||||
{
|
||||
title: 'Terms & Conditions',
|
||||
href: '/terms',
|
||||
},
|
||||
{
|
||||
title: 'Solar Console',
|
||||
href: '/console',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Drawer open={open} onClose={onClose}>
|
||||
<Box sx={{ width: width }} role="presentation" onClick={onClose}>
|
||||
<Toolbar style={{ padding: 0 }}>
|
||||
<Box display="flex" gap={2} sx={{ mx: 2 }}>
|
||||
<Image
|
||||
src="/logo.png"
|
||||
width={28}
|
||||
height={28}
|
||||
alt="company logo"
|
||||
style={{ objectFit: 'contain' }}
|
||||
className="dark:invert"
|
||||
/>
|
||||
|
||||
<Box display="flex" flexDirection="column" justifyContent="center">
|
||||
<Typography variant="body2" component="h2" fontWeight="bold" lineHeight={1.4}>
|
||||
Solsynth LLC
|
||||
</Typography>
|
||||
<Typography variant="caption" component="h3" lineHeight={1} fontFamily="monospace">
|
||||
Capital
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
|
||||
<List>
|
||||
{functionLinks.map((l) => (
|
||||
<NextLink passHref href={l.href} key={l.href}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton selected={router.pathname == l.href}>
|
||||
<ListItemIcon>{l.icon}</ListItemIcon>
|
||||
<ListItemText primary={l.title} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</NextLink>
|
||||
))}
|
||||
</List>
|
||||
<Divider />
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', px: 2, py: 1.5, gap: 1 }}>
|
||||
{additionalLinks.map((l) => (
|
||||
<NextLink passHref href={l.href} key={l.href}>
|
||||
<Link variant="body2" color={'textSecondary'} fontSize={13}>
|
||||
{l.title}
|
||||
</Link>
|
||||
</NextLink>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import { SnAttachment } from 'solar-js-sdk'
|
||||
import { getAttachmentUrl } from 'solar-js-sdk'
|
||||
import { QuestionMark } from '@mui/icons-material'
|
||||
import { Link, Paper, Typography } from '@mui/material'
|
||||
import { ComponentProps } from 'react'
|
||||
|
||||
export function AttachmentItem({
|
||||
item,
|
||||
borderRadius,
|
||||
...rest
|
||||
}: { item: SnAttachment; borderRadius?: string } & ComponentProps<'div'>) {
|
||||
switch (item.mimetype.split('/')[0]) {
|
||||
case 'image':
|
||||
return (
|
||||
<Paper {...rest}>
|
||||
<img
|
||||
src={getAttachmentUrl(item.rid)}
|
||||
alt={item.alt}
|
||||
style={{ objectFit: 'cover', borderRadius: borderRadius ?? '8px' }}
|
||||
/>
|
||||
</Paper>
|
||||
)
|
||||
case 'video':
|
||||
return (
|
||||
<Paper {...rest}>
|
||||
<video src={getAttachmentUrl(item.rid)} controls style={{ borderRadius: borderRadius ?? '8px' }} />
|
||||
</Paper>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<Paper sx={{ width: '100%', height: '100%', p: 5, textAlign: 'center' }} {...rest}>
|
||||
<QuestionMark sx={{ mb: 2 }} />
|
||||
<Typography>Unknown</Typography>
|
||||
<Typography gutterBottom>{item.name}</Typography>
|
||||
<Typography fontFamily="monospace" gutterBottom>
|
||||
{item.mimetype}
|
||||
</Typography>
|
||||
<Link href={getAttachmentUrl(item.rid)} target="_blank" rel="noreferrer" fontSize={13}>
|
||||
Open in browser
|
||||
</Link>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { setTokenCookies, SnAuthFactor, SnAuthResult, SnAuthTicket } from 'solar-js-sdk'
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { ArrowForward } from '@mui/icons-material'
|
||||
import { Collapse, Alert, Box, TextField, Button } from '@mui/material'
|
||||
import { useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
|
||||
export interface SnLoginCheckpointForm {
|
||||
password: string
|
||||
}
|
||||
|
||||
export function SnLoginCheckpoint({
|
||||
ticket,
|
||||
factor,
|
||||
onNext,
|
||||
}: {
|
||||
ticket: SnAuthTicket
|
||||
factor: SnAuthFactor
|
||||
onNext: (val: SnAuthTicket, done: boolean) => void
|
||||
}) {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
|
||||
const { handleSubmit, register } = useForm<SnLoginCheckpointForm>()
|
||||
|
||||
async function onSubmit(data: any) {
|
||||
try {
|
||||
setLoading(true)
|
||||
const resp = await sni.patch<SnAuthResult>('/cgi/id/auth', {
|
||||
ticket_id: ticket.id,
|
||||
factor_id: factor.id,
|
||||
code: data.password,
|
||||
})
|
||||
|
||||
if (resp.data.isFinished) {
|
||||
const tokenResp = await sni.post('/cgi/id/auth/token', {
|
||||
grant_type: 'grant_token',
|
||||
code: resp.data.ticket.grantToken!,
|
||||
})
|
||||
const atk: string = tokenResp.data['accessToken']
|
||||
const rtk: string = tokenResp.data['refreshToken']
|
||||
setTokenCookies(atk, rtk)
|
||||
console.log('[Authenticator] User has been logged in. Result atk: ', atk)
|
||||
}
|
||||
|
||||
onNext(resp.data.ticket, resp.data.isFinished)
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert sx={{ mb: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', width: '100%', gap: 2, textAlign: 'center' }}>
|
||||
<TextField
|
||||
label={factor.type == 0 ? 'Password' : 'Verification code'}
|
||||
type="password"
|
||||
{...register('password', { required: true })}
|
||||
/>
|
||||
|
||||
<Button variant="contained" endIcon={<ArrowForward />} disabled={loading} type="submit">
|
||||
Next
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { SnAuthFactor, SnAuthTicket } from 'solar-js-sdk'
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { Collapse, Alert, Box, Button, Typography, ButtonGroup } from '@mui/material'
|
||||
import { useState } from 'react'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import PasswordIcon from '@mui/icons-material/Password'
|
||||
import EmailIcon from '@mui/icons-material/Email'
|
||||
|
||||
export function SnLoginRouter({
|
||||
ticket,
|
||||
factorList,
|
||||
onNext,
|
||||
}: {
|
||||
ticket: SnAuthTicket
|
||||
factorList: SnAuthFactor[]
|
||||
onNext: (val: SnAuthFactor) => void
|
||||
}) {
|
||||
const factorTypeIcons = [<PasswordIcon key="password-icon" />, <EmailIcon key="email-icon" />]
|
||||
const factorTypeLabels = ['Password', 'Email verification code']
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
|
||||
async function onSubmit(factor: SnAuthFactor) {
|
||||
try {
|
||||
setLoading(true)
|
||||
await sni.post('/cgi/id/auth/factors/' + factor.id)
|
||||
onNext(factor)
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse in={!!error} sx={{ width: 320 }}>
|
||||
<Alert sx={{ mb: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', width: '100%', gap: 2, textAlign: 'center' }}>
|
||||
<ButtonGroup orientation="vertical" aria-label="Vertical button group">
|
||||
{factorList.map((factor) => (
|
||||
<Button
|
||||
sx={{ py: 1 }}
|
||||
key={factor.id}
|
||||
onClick={() => onSubmit(factor)}
|
||||
disabled={loading || ticket.factorTrail?.includes(factor.id)}
|
||||
startIcon={factorTypeIcons[factor.type]}
|
||||
>
|
||||
{factorTypeLabels[factor.type]}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
|
||||
<Typography variant="caption" sx={{ opacity: 0.75, mx: 2 }}>
|
||||
{ticket.stepRemain} step(s) left
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { ArrowForward } from '@mui/icons-material'
|
||||
import { Alert, Box, Button, Collapse, Link, TextField, Typography } from '@mui/material'
|
||||
import { SnAuthFactor, SnAuthResult, SnAuthTicket } from 'solar-js-sdk'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import NextLink from 'next/link'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
|
||||
export type SnLoginStartForm = {
|
||||
username: string
|
||||
}
|
||||
|
||||
export function SnLoginStart({ onNext }: { onNext: (val: SnAuthTicket, fcs: SnAuthFactor[]) => void }) {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
|
||||
const { handleSubmit, register } = useForm<SnLoginStartForm>()
|
||||
|
||||
async function onSubmit(data: any) {
|
||||
try {
|
||||
setLoading(true)
|
||||
const resp = await sni.post<SnAuthResult>('/cgi/id/auth', data)
|
||||
const factorResp = await sni.get<SnAuthFactor[]>('/cgi/id/auth/factors', {
|
||||
params: {
|
||||
ticketId: resp.data.ticket.id,
|
||||
},
|
||||
})
|
||||
onNext(resp.data.ticket, factorResp.data)
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert sx={{ mb: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', width: '100%', gap: 2, textAlign: 'center' }}>
|
||||
<TextField
|
||||
label="Username"
|
||||
helperText="You can also use email address and phone number"
|
||||
{...register('username', { required: true })}
|
||||
/>
|
||||
|
||||
<Button variant="contained" endIcon={<ArrowForward />} disabled={loading} type="submit">
|
||||
Next
|
||||
</Button>
|
||||
|
||||
<Typography variant="caption" sx={{ opacity: 0.75, mx: 2 }}>
|
||||
By continuing means you agree to our{' '}
|
||||
<NextLink href="/terms/privacy-policy" passHref>
|
||||
<Link component="span">Privacy Policy</Link>
|
||||
</NextLink>{' '}
|
||||
and{' '}
|
||||
<NextLink href="/terms/user-agreements" passHref>
|
||||
<Link component="span">User Agreements</Link>
|
||||
</NextLink>
|
||||
</Typography>
|
||||
</Box>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
import { Noto_Serif_TC } from 'next/font/google'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export interface TimeDiff {
|
||||
days: number
|
||||
hours: number
|
||||
minutes: number
|
||||
seconds: number
|
||||
isCountdown: boolean
|
||||
}
|
||||
|
||||
const serifFont = Noto_Serif_TC({
|
||||
weight: ['400', '500', '700'],
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
export function CountdownTimer({ targetDate, onUpdate }: { targetDate: Date; onUpdate: (diff: TimeDiff) => void }) {
|
||||
const [timeDiff, setTimeDiff] = useState({
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isCountdown: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const updateTimeDiff = () => {
|
||||
const now = new Date()
|
||||
const diff = targetDate.getTime() - now.getTime()
|
||||
|
||||
const absDiff = Math.abs(diff)
|
||||
const isCountdown = diff > 0
|
||||
|
||||
const days = Math.floor(absDiff / (1000 * 60 * 60 * 24))
|
||||
const hours = Math.floor((absDiff / (1000 * 60 * 60)) % 24)
|
||||
const minutes = Math.floor((absDiff / (1000 * 60)) % 60)
|
||||
const seconds = Math.floor((absDiff / 1000) % 60)
|
||||
|
||||
setTimeDiff({ days, hours, minutes, seconds, isCountdown })
|
||||
onUpdate({ days, hours, minutes, seconds, isCountdown })
|
||||
}
|
||||
|
||||
const intervalId = setInterval(updateTimeDiff, 1000)
|
||||
|
||||
return () => clearInterval(intervalId)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex gap-5">
|
||||
<div>
|
||||
<span className="countdown font-mono text-4xl">
|
||||
<span style={{ '--value': timeDiff.days } as any}></span>
|
||||
</span>
|
||||
<span className={serifFont.className}>天</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="countdown font-mono text-4xl">
|
||||
<span style={{ '--value': timeDiff.hours } as any}></span>
|
||||
</span>
|
||||
<span className={serifFont.className}>小时</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="countdown font-mono text-4xl">
|
||||
<span style={{ '--value': timeDiff.minutes } as any}></span>
|
||||
</span>
|
||||
<span className={serifFont.className}>分钟</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="countdown font-mono text-4xl">
|
||||
<span style={{ '--value': timeDiff.seconds } as any}></span>
|
||||
</span>
|
||||
<span className={serifFont.className}>秒</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import { checkAuthenticatedClient, redirectToLogin } from 'solar-js-sdk'
|
||||
import { JSX, useEffect } from 'react'
|
||||
import { DashboardLayout, Navigation } from '@toolpad/core'
|
||||
import { Box, Stack, Typography } from '@mui/material'
|
||||
import NextLink from 'next/link'
|
||||
|
||||
import HomeIcon from '@mui/icons-material/Home'
|
||||
import AppsIcon from '@mui/icons-material/Apps'
|
||||
|
||||
export function ConsoleLayout({ children }: { children: JSX.Element }) {
|
||||
useEffect(() => {
|
||||
if (!checkAuthenticatedClient()) redirectToLogin()
|
||||
}, [])
|
||||
|
||||
const navigation: Navigation = [
|
||||
{
|
||||
segment: '',
|
||||
title: 'Home',
|
||||
icon: <HomeIcon />,
|
||||
},
|
||||
{
|
||||
segment: 'console/matrix',
|
||||
title: 'Matrix',
|
||||
icon: <AppsIcon />,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<DashboardLayout
|
||||
navigation={navigation}
|
||||
branding={{
|
||||
homeUrl: '/console',
|
||||
}}
|
||||
slots={{
|
||||
appTitle(_) {
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
<NextLink passHref href="/console">
|
||||
<Typography variant="h6">Solar Network Console</Typography>
|
||||
</NextLink>
|
||||
</Stack>
|
||||
)
|
||||
},
|
||||
toolbarActions(_) {
|
||||
return <Box />
|
||||
},
|
||||
}}
|
||||
sidebarExpandedWidth={300}
|
||||
defaultSidebarCollapsed
|
||||
>
|
||||
{children}
|
||||
</DashboardLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export function getConsoleStaticProps(original: any) {
|
||||
if (original.props.title) {
|
||||
original.props.title = 'Solar Console | ' + original.props.title
|
||||
}
|
||||
original.props.showAppBar = false
|
||||
|
||||
return original
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
import { Collapse, Alert, TextField, Button, Box } from '@mui/material'
|
||||
import { useRouter } from 'next-nprogress-bar'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { MaProduct } from 'solar-js-sdk'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
|
||||
export interface MatrixProductForm {
|
||||
name: string
|
||||
alias: string
|
||||
description: string
|
||||
introduction: string
|
||||
icon: string
|
||||
previews: string[]
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export default function MaProductForm({
|
||||
onSubmit,
|
||||
onSuccess,
|
||||
defaultValue,
|
||||
}: {
|
||||
onSubmit: (data: MatrixProductForm) => Promise<any>
|
||||
onSuccess?: () => void
|
||||
defaultValue?: MaProduct
|
||||
}) {
|
||||
const { handleSubmit, register } = useForm<MatrixProductForm>({
|
||||
defaultValues: {
|
||||
name: defaultValue?.name ?? '',
|
||||
alias: defaultValue?.alias ?? '',
|
||||
description: defaultValue?.description ?? '',
|
||||
introduction: defaultValue?.meta?.introduction ?? '',
|
||||
icon: defaultValue?.icon ?? '',
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const [previews, setPreviews] = useState<string[]>([])
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue?.previews) {
|
||||
setPreviews(defaultValue.previews)
|
||||
}
|
||||
if (defaultValue?.tags) {
|
||||
setTags(defaultValue.tags)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [busy, setBusy] = useState<boolean>(false)
|
||||
|
||||
function callback() {
|
||||
if (onSuccess) {
|
||||
onSuccess()
|
||||
} else {
|
||||
router.push('/console/matrix')
|
||||
}
|
||||
}
|
||||
|
||||
async function submit(data: MatrixProductForm) {
|
||||
try {
|
||||
setBusy(true)
|
||||
await onSubmit({
|
||||
...data,
|
||||
previews,
|
||||
tags,
|
||||
})
|
||||
callback()
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(submit)}>
|
||||
<Box display="flex" flexDirection="column" maxWidth="sm" gap={2.5}>
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<TextField label="Icon" placeholder="Image URL or Attachment RID" {...register('icon')} />
|
||||
|
||||
<TextField
|
||||
label="Previews"
|
||||
placeholder="Comma separated, Image URL or Attachment RID, the first one will be used as the banner"
|
||||
value={previews.join(',')}
|
||||
onChange={(val) => setPreviews(val.target.value.split(',').map((v) => v.trim()))}
|
||||
/>
|
||||
|
||||
<TextField label="Name" {...register('name')} />
|
||||
|
||||
<TextField label="Alias" {...register('alias')} />
|
||||
|
||||
<TextField
|
||||
label="Tags"
|
||||
placeholder="Comma separated"
|
||||
value={tags.join(',')}
|
||||
onChange={(val) => setTags(val.target.value.split(',').map((v) => v.trim()))}
|
||||
/>
|
||||
|
||||
<TextField minRows={3} maxRows={3} multiline label="Description" {...register('description')} />
|
||||
|
||||
<TextField minRows={5} multiline label="Introduction" {...register('introduction')} />
|
||||
|
||||
<Box sx={{ mt: 5 }} display="flex" gap={2}>
|
||||
<Button variant="contained" type="submit" disabled={busy}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button onClick={callback} variant="outlined" disabled={busy}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
)
|
||||
}
|
@ -1,428 +0,0 @@
|
||||
import {
|
||||
Collapse,
|
||||
Alert,
|
||||
TextField,
|
||||
Button,
|
||||
Box,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Typography,
|
||||
Grid2 as Grid,
|
||||
IconButton,
|
||||
Card,
|
||||
} from '@mui/material'
|
||||
import { useRouter } from 'next-nprogress-bar'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import {
|
||||
MaProduct,
|
||||
MaRelease,
|
||||
MaReleaseAsset,
|
||||
MaReleaseInstaller,
|
||||
MaReleaseInstallerPatch,
|
||||
MaReleaseRunner,
|
||||
} from 'solar-js-sdk'
|
||||
import MonacoEditor from '@monaco-editor/react'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
|
||||
export interface MatrixReleaseForm {
|
||||
version: string
|
||||
type: number
|
||||
channel: string
|
||||
title: string
|
||||
description: string
|
||||
content: string
|
||||
assets: Record<string, MaReleaseAsset>
|
||||
installers: Record<string, MaReleaseInstaller>
|
||||
runners: Record<string, MaReleaseRunner>
|
||||
attachments: string[]
|
||||
}
|
||||
|
||||
export default function MaReleaseForm({
|
||||
onSubmit,
|
||||
onSuccess,
|
||||
parent,
|
||||
defaultValue,
|
||||
}: {
|
||||
onSubmit: (data: MatrixReleaseForm) => Promise<any>
|
||||
onSuccess?: () => void
|
||||
parent: Partial<MaProduct>
|
||||
defaultValue?: MaRelease
|
||||
}) {
|
||||
const { handleSubmit, register } = useForm<MatrixReleaseForm>({
|
||||
defaultValues: {
|
||||
title: defaultValue?.meta.title,
|
||||
version: defaultValue?.version,
|
||||
type: defaultValue?.type ?? 0,
|
||||
channel: defaultValue?.channel,
|
||||
description: defaultValue?.meta.description,
|
||||
content: defaultValue?.meta.content,
|
||||
attachments: defaultValue?.meta.attachments,
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue?.assets) {
|
||||
setAssets(Object.keys(defaultValue.assets).map((k) => ({ k, v: defaultValue.assets[k] })))
|
||||
}
|
||||
if (defaultValue?.installers) {
|
||||
setInstallers(Object.keys(defaultValue.installers).map((k) => ({ k, v: defaultValue.installers[k] })))
|
||||
}
|
||||
if (defaultValue?.runners) {
|
||||
setRunners(Object.keys(defaultValue.runners).map((k) => ({ k, v: defaultValue.runners[k] })))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const [assets, setAssets] = useState<{ k: string; v: MaReleaseAsset }[]>([])
|
||||
const [installers, setInstallers] = useState<{ k: string; v: MaReleaseInstaller }[]>([])
|
||||
const [runners, setRunners] = useState<{ k: string; v: MaReleaseRunner }[]>([])
|
||||
|
||||
function addAsset() {
|
||||
setAssets((val) => [...val, { k: '', v: { uri: '', contentType: '' } }])
|
||||
}
|
||||
|
||||
function addInstaller() {
|
||||
setInstallers((val) => [...val, { k: '', v: { workdir: '', script: '', patches: [] } }])
|
||||
}
|
||||
|
||||
function addRunner() {
|
||||
setRunners((val) => [...val, { k: '', v: { workdir: '', script: '', label: '' } }])
|
||||
}
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [busy, setBusy] = useState<boolean>(false)
|
||||
|
||||
function callback() {
|
||||
if (onSuccess) {
|
||||
onSuccess()
|
||||
} else {
|
||||
router.push(`/console/matrix/products/${parent?.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function submit(data: MatrixReleaseForm) {
|
||||
try {
|
||||
setBusy(true)
|
||||
await onSubmit({
|
||||
...data,
|
||||
assets: assets.reduce((a, { k, v }) => ({ ...a, [k]: v }), {}),
|
||||
installers: installers.reduce((a, { k, v }) => ({ ...a, [k]: v }), {}),
|
||||
runners: runners.reduce((a, { k, v }) => ({ ...a, [k]: v }), {}),
|
||||
})
|
||||
callback()
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(submit)}>
|
||||
<Box display="flex" flexDirection="column" maxWidth="sm" gap={2.5}>
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<TextField label="Version" placeholder="Major.Minor.Patch" {...register('version', { required: true })} />
|
||||
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="release-type">Type</InputLabel>
|
||||
<Select
|
||||
labelId="release-type"
|
||||
label="Type"
|
||||
defaultValue={defaultValue?.type}
|
||||
{...register('type', { required: true })}
|
||||
>
|
||||
<MenuItem value={0}>Full Release</MenuItem>
|
||||
<MenuItem value={1}>Patch Release</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField label="Title" {...register('title')} />
|
||||
|
||||
<TextField label="Alias" {...register('channel')} />
|
||||
|
||||
<TextField minRows={3} maxRows={3} multiline label="Description" {...register('description')} />
|
||||
|
||||
<TextField minRows={5} multiline label="Content" {...register('content')} />
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="h5">Assets</Typography>
|
||||
|
||||
{assets.map(({ k, v }, idx) => (
|
||||
<Card variant="outlined" key={idx}>
|
||||
<Box sx={{ pl: 2, pr: 4, py: 2 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={11}>
|
||||
<TextField
|
||||
label="Platform"
|
||||
sx={{ width: '100%' }}
|
||||
value={k}
|
||||
onChange={(val) => {
|
||||
setAssets((data) =>
|
||||
data.map((ele, index) => (index == idx ? { k: val.target.value, v: ele.v } : ele)),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={1} sx={{ display: 'grid', placeItems: 'center' }}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setAssets((data) => data.filter((_, index) => index != idx))
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<TextField
|
||||
label="URI"
|
||||
sx={{ width: '100%' }}
|
||||
value={v.uri}
|
||||
onChange={(val) => {
|
||||
setAssets((data) =>
|
||||
data.map((ele, index) =>
|
||||
index == idx ? { v: { ...ele.v, uri: val.target.value }, k: ele.k } : ele,
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<TextField
|
||||
label="Content Type"
|
||||
sx={{ width: '100%' }}
|
||||
value={v.contentType}
|
||||
onChange={(val) => {
|
||||
setAssets((data) =>
|
||||
data.map((ele, index) =>
|
||||
index == idx ? { v: { ...ele.v, contentType: val.target.value }, k: ele.k } : ele,
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<Box>
|
||||
<Button variant="outlined" onClick={addAsset}>
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="h5">Installers</Typography>
|
||||
|
||||
{installers.map(({ k, v }, idx) => (
|
||||
<Card variant="outlined" key={idx}>
|
||||
<Box sx={{ pl: 2, pr: 4, py: 2 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={4}>
|
||||
<TextField
|
||||
label="Platform"
|
||||
sx={{ width: '100%' }}
|
||||
value={k}
|
||||
onChange={(val) => {
|
||||
setInstallers((data) =>
|
||||
data.map((ele, index) => (index == idx ? { k: val.target.value, v: ele.v } : ele)),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={7}>
|
||||
<TextField
|
||||
label="Working Directory"
|
||||
sx={{ width: '100%' }}
|
||||
value={v.workdir}
|
||||
onChange={(val) => {
|
||||
setInstallers((data) =>
|
||||
data.map((ele, index) =>
|
||||
index == idx ? { k: ele.k, v: { ...ele.v, workdir: val.target.value } } : ele,
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={1} sx={{ display: 'grid', placeItems: 'center' }}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setInstallers((data) => data.filter((_, index) => index != idx))
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Typography variant="subtitle1" sx={{ mx: 1 }}>
|
||||
Script
|
||||
</Typography>
|
||||
<Card variant="outlined">
|
||||
<MonacoEditor
|
||||
height="140px"
|
||||
width="100%"
|
||||
options={{ minimap: { enabled: false } }}
|
||||
defaultValue={v.script}
|
||||
onChange={(val) =>
|
||||
setInstallers((data) =>
|
||||
data.map((ele, index) => (index == idx ? { v: { ...ele.v, script: val }, k: ele.k } : ele)),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Typography variant="subtitle1" sx={{ mx: 1 }}>
|
||||
Patches
|
||||
</Typography>
|
||||
<Card variant="outlined">
|
||||
<MonacoEditor
|
||||
height="140px"
|
||||
width="100%"
|
||||
options={{ minimap: { enabled: false } }}
|
||||
defaultValue={v.patches.map((p) => `${p.action}:${p.glob}`).join('\n')}
|
||||
onChange={(val) =>
|
||||
setInstallers((data) =>
|
||||
data.map((ele, index) =>
|
||||
index == idx
|
||||
? {
|
||||
v: {
|
||||
...ele.v,
|
||||
patches: val?.split('\n')?.map((p) => ({
|
||||
action: p.split(':')[0],
|
||||
glob: p.split(':')[1],
|
||||
})) as MaReleaseInstallerPatch[],
|
||||
},
|
||||
k: ele.k,
|
||||
}
|
||||
: ele,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<Box>
|
||||
<Button variant="outlined" onClick={addInstaller}>
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="h5">Runners</Typography>
|
||||
|
||||
{runners.map(({ k, v }, idx) => (
|
||||
<Card variant="outlined" key={idx}>
|
||||
<Box sx={{ pl: 2, pr: 4, py: 2 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={4}>
|
||||
<TextField
|
||||
label="Platform"
|
||||
sx={{ width: '100%' }}
|
||||
value={k}
|
||||
onChange={(val) => {
|
||||
setRunners((data) =>
|
||||
data.map((ele, index) => (index == idx ? { k: val.target.value, v: ele.v } : ele)),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={7}>
|
||||
<TextField
|
||||
label="Working Directory"
|
||||
sx={{ width: '100%' }}
|
||||
value={v.workdir}
|
||||
onChange={(val) => {
|
||||
setRunners((data) =>
|
||||
data.map((ele, index) =>
|
||||
index == idx ? { k: ele.k, v: { ...ele.v, workdir: val.target.value } } : ele,
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={1} sx={{ display: 'grid', placeItems: 'center' }}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setRunners((data) => data.filter((_, index) => index != idx))
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
label="Label"
|
||||
sx={{ width: '100%' }}
|
||||
value={v.label}
|
||||
onChange={(val) => {
|
||||
setRunners((data) =>
|
||||
data.map((ele, index) =>
|
||||
index == idx ? { k: ele.k, v: { ...ele.v, label: val.target.value } } : ele,
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Typography variant="subtitle1" sx={{ mx: 1 }}>
|
||||
Script
|
||||
</Typography>
|
||||
<Card variant="outlined">
|
||||
<MonacoEditor
|
||||
height="280px"
|
||||
width="100%"
|
||||
options={{ minimap: { enabled: false } }}
|
||||
defaultValue={v.script}
|
||||
onChange={(val) =>
|
||||
setRunners((data) =>
|
||||
data.map((ele, index) =>
|
||||
index == idx ? { v: { ...ele.v, script: val ?? '' }, k: ele.k } : ele,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<Box>
|
||||
<Button variant="outlined" onClick={addRunner}>
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 5 }} display="flex" gap={2}>
|
||||
<Button variant="contained" type="submit" disabled={busy}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button onClick={callback} variant="outlined" disabled={busy}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
)
|
||||
}
|
11
src/components/navigation/Footer.svelte
Normal file
@ -0,0 +1,11 @@
|
||||
<footer class="footer footer-center p-5 border-t border-footer">
|
||||
<aside>
|
||||
<p>Copyright © {new Date().getFullYear()} Solsynth LLC</p>
|
||||
</aside>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.border-footer {
|
||||
border-color: var(--fallback-bc, oklch(var(--bc) / 0.1));
|
||||
}
|
||||
</style>
|
28
src/components/navigation/NavBar.svelte
Normal file
@ -0,0 +1,28 @@
|
||||
<div
|
||||
class="navbar bg-base-100 text-base-content bg-opacity-90 backdrop-blur transition-shadow duration-100 [transform:translate3d(0,0,0)] shadow-sm"
|
||||
>
|
||||
<div class="px-5 w-full flex justify-between">
|
||||
<a href="/" class="btn btn-ghost text-xl">Solsynth</a>
|
||||
|
||||
<label id="theme-switch" class="swap swap-rotate me-5">
|
||||
<input type="checkbox" class="theme-controller" value="light" />
|
||||
|
||||
<svg
|
||||
class="swap-off fill-current w-6 h-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z"
|
||||
></path></svg
|
||||
>
|
||||
<svg
|
||||
class="swap-on fill-current w-6 h-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z"
|
||||
></path></svg
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
1
src/lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
// Reexport your entry components here
|
@ -1,82 +0,0 @@
|
||||
import '@/styles/globals.css'
|
||||
import type { AppProps } from 'next/app'
|
||||
import { Box, createTheme, CssBaseline, ThemeProvider } from '@mui/material'
|
||||
import { Roboto } from 'next/font/google'
|
||||
import { CapAppBar } from '@/components/CapAppBar'
|
||||
import { PagesProgressBar as ProgressBar } from 'next-nprogress-bar'
|
||||
import { AppProvider } from '@toolpad/core/nextjs'
|
||||
import { useUserStore } from 'solar-js-sdk'
|
||||
import { useEffect } from 'react'
|
||||
import Head from 'next/head'
|
||||
|
||||
const fontRoboto = Roboto({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '500', '700'],
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
const siteTheme = createTheme({
|
||||
cssVariables: true,
|
||||
colorSchemes: {
|
||||
dark: true,
|
||||
},
|
||||
palette: {
|
||||
mode: 'light',
|
||||
primary: {
|
||||
main: '#3949ab',
|
||||
},
|
||||
secondary: {
|
||||
main: '#1e88e5',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const userStore = useUserStore()
|
||||
|
||||
useEffect(() => {
|
||||
userStore.fetchUser()
|
||||
}, [])
|
||||
|
||||
const title = pageProps.title
|
||||
? pageProps.title.startsWith('Solar Console')
|
||||
? pageProps.title
|
||||
: `${pageProps.title} | Solsynth LLC`
|
||||
: 'Solsynth LLC'
|
||||
|
||||
return (
|
||||
<>
|
||||
<style jsx global>{`
|
||||
html {
|
||||
font-family: ${fontRoboto.style.fontFamily};
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link rel="icon" href="/favicon.png" type="image/png" />
|
||||
<link rel="apple-touch-icon" href="/apple-icon.png" type="image/png" />
|
||||
</Head>
|
||||
|
||||
<AppProvider>
|
||||
<ThemeProvider theme={siteTheme}>
|
||||
<CssBaseline />
|
||||
<ProgressBar
|
||||
height="4px"
|
||||
color={siteTheme.palette.primary.main}
|
||||
options={{ showSpinner: false }}
|
||||
shallowRouting
|
||||
/>
|
||||
|
||||
{(pageProps.showAppBar ?? true) && <CapAppBar />}
|
||||
<Box sx={{ minHeight: 'calc(100vh - 64px)' }}>
|
||||
<Component {...pageProps} />
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
</AppProvider>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import {
|
||||
AppCacheProvider,
|
||||
DocumentHeadTags,
|
||||
DocumentHeadTagsProps,
|
||||
documentGetInitialProps,
|
||||
} from '@mui/material-nextjs/v15-pagesRouter'
|
||||
import { Html, Head, Main, NextScript, DocumentContext, DocumentProps } from 'next/document'
|
||||
import { GoogleAnalytics } from '@next/third-parties/google'
|
||||
|
||||
export default function Document(props: DocumentProps & DocumentHeadTagsProps) {
|
||||
return (
|
||||
<AppCacheProvider {...props}>
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<DocumentHeadTags {...props} />
|
||||
</Head>
|
||||
<body className="antialiased">
|
||||
<Main />
|
||||
<NextScript />
|
||||
<GoogleAnalytics gaId="G-ZFJ7RX0JXF" />
|
||||
</body>
|
||||
</Html>
|
||||
</AppCacheProvider>
|
||||
)
|
||||
}
|
||||
|
||||
Document.getInitialProps = async (ctx: DocumentContext) => {
|
||||
const finalProps = await documentGetInitialProps(ctx)
|
||||
return finalProps
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export default async function handler(_: NextApiRequest, res: NextApiResponse) {
|
||||
const solarNetworkApi = 'https://api.sn.solsynth.dev'
|
||||
|
||||
const resp = await axios.get(solarNetworkApi + '/cgi/id/well-known/jwks')
|
||||
|
||||
res.status(200).json(resp.data)
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export default async function handler(_: NextApiRequest, res: NextApiResponse) {
|
||||
const siteUrl = 'https://solsynth.dev'
|
||||
const solarNetworkApi = 'https://api.sn.solsynth.dev'
|
||||
|
||||
const resp = await axios.get(solarNetworkApi + '/cgi/id/well-known/openid-configuration')
|
||||
const out: Record<string, any> = resp.data
|
||||
|
||||
out['authorization_endpoint'] = siteUrl + '/auth/authorize'
|
||||
out['jwks_uri'] = siteUrl + '/.well-known/jwks'
|
||||
|
||||
for (const [k, v] of Object.entries(out)) {
|
||||
if (typeof v === 'string') {
|
||||
if (v.startsWith('https://id.solsynth.dev/api')) {
|
||||
out[k] = v.replace('https://id.solsynth.dev/api', solarNetworkApi + '/cgi/id')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json(out)
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
import { AttachmentItem } from '@/components/attachments/AttachmentItem'
|
||||
import { SnAttachment } from 'solar-js-sdk'
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { Box, ImageList, ImageListItem, Pagination, useMediaQuery, useTheme } from '@mui/material'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
let page: number = parseInt(context.query.page as string)
|
||||
if (isNaN(page)) page = 1
|
||||
|
||||
const countPerPage = 20
|
||||
|
||||
const { data: resp } = await sni.get<{ data: SnAttachment[]; count: number }>('/cgi/uc/attachments', {
|
||||
params: {
|
||||
take: countPerPage,
|
||||
offset: (page - 1) * countPerPage,
|
||||
},
|
||||
})
|
||||
|
||||
const attachments = resp.data
|
||||
|
||||
return { props: { attachments, page, pages: Math.ceil(resp.count / countPerPage) } }
|
||||
}
|
||||
|
||||
export default function AttachmentsPage({
|
||||
attachments,
|
||||
page,
|
||||
pages,
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const router = useRouter()
|
||||
const theme = useTheme()
|
||||
const breakpoints = useMediaQuery(theme.breakpoints.up('md'))
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ImageList variant="masonry" cols={breakpoints ? 3 : 2} gap={8} sx={{ mx: 2 }}>
|
||||
{attachments.map((item: SnAttachment) => (
|
||||
<ImageListItem key={item.rid}>
|
||||
<AttachmentItem item={item} borderRadius="0" />
|
||||
</ImageListItem>
|
||||
))}
|
||||
</ImageList>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mx: 'auto',
|
||||
mb: 5,
|
||||
mt: 3,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
placeItems: 'center',
|
||||
gap: 1.5,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Pagination count={pages} page={page} onChange={(_, page) => router.push('/attachments?page=' + page)} />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
import {
|
||||
styled,
|
||||
Button,
|
||||
Container,
|
||||
Card,
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
IconButton,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
LinearProgress,
|
||||
Typography,
|
||||
Alert,
|
||||
Collapse,
|
||||
} from '@mui/material'
|
||||
import { MultipartProgress, SnAttachment, UploadAttachmentTask } from 'solar-js-sdk'
|
||||
import { useState } from 'react'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import CloudUploadIcon from '@mui/icons-material/CloudUpload'
|
||||
import PlayIcon from '@mui/icons-material/PlayArrow'
|
||||
|
||||
const VisuallyHiddenInput = styled('input')({
|
||||
clip: 'rect(0 0 0 0)',
|
||||
clipPath: 'inset(50%)',
|
||||
height: 1,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
width: 1,
|
||||
})
|
||||
|
||||
interface FileUploadTask {
|
||||
file: File
|
||||
attachment?: SnAttachment
|
||||
}
|
||||
|
||||
export default function AttachmentNew() {
|
||||
const [files, setFiles] = useState<FileUploadTask[]>([])
|
||||
|
||||
const [busy, setBusy] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [task, setTask] = useState<FileUploadTask>()
|
||||
const [taskProgress, setTaskProgress] = useState<MultipartProgress>()
|
||||
|
||||
const [pool, setPool] = useState<string>('interactive')
|
||||
|
||||
async function upload() {
|
||||
if (files.length == 0) return
|
||||
|
||||
setBusy(true)
|
||||
|
||||
for (let idx = 0; idx < files.length; idx++) {
|
||||
if (files[idx].attachment) continue
|
||||
|
||||
try {
|
||||
const task = new UploadAttachmentTask(files[idx].file, pool)
|
||||
setTask(files[idx])
|
||||
task.onProgress = (progress) => setTaskProgress(progress)
|
||||
task.onError = (err) => setError(err)
|
||||
const attachment = await task.submit()
|
||||
setFiles((files) => files.map((f, i) => (i == idx ? { ...f, attachment } : f)))
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
setBusy(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setBusy(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
height: 'calc(100vh - 64px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<Box sx={{ width: '100%', mx: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button
|
||||
component="label"
|
||||
role={undefined}
|
||||
variant={files.length == 0 ? 'contained' : 'text'}
|
||||
tabIndex={-1}
|
||||
startIcon={<CloudUploadIcon />}
|
||||
>
|
||||
Pick files
|
||||
<VisuallyHiddenInput
|
||||
type="file"
|
||||
onChange={(event) =>
|
||||
setFiles(
|
||||
Array.from(event.target.files ?? []).map((f) => ({
|
||||
file: f,
|
||||
})),
|
||||
)
|
||||
}
|
||||
multiple
|
||||
/>
|
||||
</Button>
|
||||
{files.length > 0 && (
|
||||
<Button
|
||||
color="success"
|
||||
variant="contained"
|
||||
startIcon={<PlayIcon />}
|
||||
disabled={busy}
|
||||
onClick={() => upload()}
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert sx={{ mb: 5 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
{taskProgress && (
|
||||
<Box sx={{ mt: 5, width: '100%', textAlign: 'center' }}>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{task?.file.name ?? 'Waiting...'}
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
value={taskProgress?.value ? taskProgress.value * 100 : 0}
|
||||
sx={{ borderRadius: 4 }}
|
||||
variant="determinate"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{files.length > 0 && (
|
||||
<Box sx={{ mt: 5, width: '100%' }}>
|
||||
<Card variant="outlined">
|
||||
<List>
|
||||
{files?.map((f, idx) => (
|
||||
<ListItem
|
||||
dense
|
||||
key={idx}
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="delete"
|
||||
disabled={busy}
|
||||
sx={{ marginRight: 1 }}
|
||||
onClick={() => setFiles((files) => files.filter((_, i) => i != idx))}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ListItemText primary={f.file.name} secondary={f.attachment?.rid} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Card>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<FormControl fullWidth sx={{ mt: 5 }}>
|
||||
<InputLabel id="attachment-pool">Attachment Pool</InputLabel>
|
||||
<Select
|
||||
labelId="attachment-pool"
|
||||
value={pool}
|
||||
disabled={busy}
|
||||
label="Attachment Pool"
|
||||
onChange={(evt) => setPool(evt.target.value)}
|
||||
>
|
||||
<MenuItem value={'interactive'}>Interactive</MenuItem>
|
||||
<MenuItem value={'messaging'}>Messaging</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { Container, Box, Typography, Alert, Collapse, Button, CircularProgress, Card, CardContent } from '@mui/material'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { checkAuthenticatedClient, redirectToLogin, SnAuthTicket } from 'solar-js-sdk'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import CheckIcon from '@mui/icons-material/Check'
|
||||
|
||||
export default function AccountAuthorize() {
|
||||
useEffect(() => {
|
||||
if (!checkAuthenticatedClient()) redirectToLogin()
|
||||
}, [])
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const [thirdClient, setThirdClient] = useState<any>(null)
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [reverting, setReverting] = useState(false)
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
function doCallback(ticket: SnAuthTicket) {
|
||||
const url = `${router.query['redirect_uri']}?code=${ticket.grantToken}&state=${router.query['state']}`
|
||||
window.open(url, '_self')
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
try {
|
||||
setReverting(true)
|
||||
const resp = await sni.get<{ ticket: SnAuthTicket; client: any }>(
|
||||
'/cgi/id/auth/o/authorize' + window.location.search,
|
||||
{},
|
||||
)
|
||||
if (resp.data.ticket) {
|
||||
return doCallback(resp.data.ticket)
|
||||
}
|
||||
setThirdClient(resp.data.client)
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setReverting(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetch()
|
||||
}, [])
|
||||
|
||||
async function confirm() {
|
||||
try {
|
||||
setBusy(true)
|
||||
const resp = await sni.post<{ ticket: SnAuthTicket }>('/cgi/id/auth/o/authorize' + window.location.search)
|
||||
return doCallback(resp.data.ticket)
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
function decline() {
|
||||
if (window.history.length > 0) {
|
||||
window.history.back()
|
||||
} else {
|
||||
window.close()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
height: 'calc(100vh - 64px)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Typography variant="h5" component="h1">
|
||||
Connect with Solarpass
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" component="h2">
|
||||
Connect third-party services with Solar Network
|
||||
</Typography>
|
||||
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert sx={{ mt: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
{reverting && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!reverting && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Card variant="outlined" sx={{ width: '100%' }}>
|
||||
<CardContent sx={{ textAlign: 'left', px: 2.5 }}>
|
||||
<Typography variant="h6">{thirdClient?.name}</Typography>
|
||||
<Typography variant="body2">{thirdClient?.description}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Button sx={{ mt: 3 }} startIcon={<CloseIcon />} onClick={() => decline()} disabled={busy} color="error">
|
||||
Decline
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ mt: 3 }}
|
||||
startIcon={<CheckIcon />}
|
||||
onClick={() => confirm()}
|
||||
disabled={busy}
|
||||
color="success"
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
import { SnLoginCheckpoint } from '@/components/auth/SnLoginCheckpoint'
|
||||
import { SnLoginRouter } from '@/components/auth/SnLoginRouter'
|
||||
import { SnLoginStart } from '@/components/auth/SnLoginStart'
|
||||
import { SnAuthFactor, SnAuthTicket } from 'solar-js-sdk'
|
||||
import { useUserStore } from 'solar-js-sdk'
|
||||
import { Box, Container, Typography } from '@mui/material'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function Login() {
|
||||
const [period, setPeriod] = useState<number>(0)
|
||||
const [ticket, setTicket] = useState<SnAuthTicket | null>(null)
|
||||
const [factorList, setFactorList] = useState<SnAuthFactor[]>([])
|
||||
const [factor, setFactor] = useState<SnAuthFactor | null>(null)
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
function doCallback() {
|
||||
if (router.query['redirect_url']) {
|
||||
let redirectUrl: string
|
||||
if (Array.isArray(router.query['redirect_url'])) {
|
||||
redirectUrl = router.query['redirect_url'][0]
|
||||
} else {
|
||||
redirectUrl = router.query['redirect_url'].toString()
|
||||
}
|
||||
|
||||
if (redirectUrl.startsWith('/')) {
|
||||
router.push(redirectUrl)
|
||||
} else {
|
||||
window.open(redirectUrl, '_self')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
router.push('/users/me')
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
switch (period) {
|
||||
case 1:
|
||||
return (
|
||||
<SnLoginRouter
|
||||
ticket={ticket!}
|
||||
factorList={factorList}
|
||||
onNext={(val) => {
|
||||
setPeriod(period + 1)
|
||||
setFactor(val)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
case 2:
|
||||
return (
|
||||
<SnLoginCheckpoint
|
||||
ticket={ticket!}
|
||||
factor={factor!}
|
||||
onNext={(val, done) => {
|
||||
if (!done) {
|
||||
setTicket(val)
|
||||
setPeriod(1)
|
||||
return
|
||||
}
|
||||
userStore.fetchUser()
|
||||
doCallback()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<SnLoginStart
|
||||
onNext={(val, fcs) => {
|
||||
setPeriod(period + 1)
|
||||
setTicket(val)
|
||||
setFactorList(fcs)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
height: 'calc(100vh - 64px)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Typography variant="h5" component="h1">
|
||||
Login
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" component="h2">
|
||||
Login via Solarpass
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mt: 3 }}>{renderForm()}</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
||||
import { Typography, Container, Box, Grid2 as Grid, Card, CardContent, CardActionArea } from '@mui/material'
|
||||
import NextLink from 'next/link'
|
||||
|
||||
import DynamicFormIcon from '@mui/icons-material/DynamicForm'
|
||||
import AppsIcon from '@mui/icons-material/Apps'
|
||||
|
||||
export function getStaticProps() {
|
||||
return getConsoleStaticProps({
|
||||
props: {
|
||||
title: 'Welcome',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default function ConsoleLanding() {
|
||||
return (
|
||||
<ConsoleLayout>
|
||||
<Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<Box>
|
||||
<DynamicFormIcon sx={{ fontSize: 64, mb: 2 }} />
|
||||
<Typography variant="subtitle2">Welcome to the</Typography>
|
||||
<Typography variant="h3" component="h1">
|
||||
Console
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">of the Solar Network</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container columns={{ xs: 1, sm: 2, md: 3 }} spacing={4}>
|
||||
<Grid size={1}>
|
||||
<NextLink passHref href="/console/matrix">
|
||||
<CardActionArea>
|
||||
<Card sx={{ width: '100%' }}>
|
||||
<CardContent>
|
||||
<AppsIcon sx={{ fontSize: 32, mb: 1.5 }} />
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Matrix
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
Publish and versioning your application with Matrix Marketplace.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardActionArea>
|
||||
</NextLink>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</ConsoleLayout>
|
||||
)
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
||||
import { MaProduct } from 'solar-js-sdk'
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { Typography, Container, Box, Button, Grid2 as Grid, Card, CardContent, CardActions } from '@mui/material'
|
||||
import NextLink from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export async function getStaticProps() {
|
||||
return getConsoleStaticProps({
|
||||
props: {
|
||||
title: 'Matrix Marketplace',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default function MatrixMarketplace() {
|
||||
const [products, setProducts] = useState<MaProduct[]>([])
|
||||
|
||||
async function fetchProducts() {
|
||||
const { data: resp } = await sni.get<{ data: MaProduct[] }>('/cgi/ma/products/created', {
|
||||
params: {
|
||||
take: 10,
|
||||
},
|
||||
})
|
||||
setProducts(resp.data)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts()
|
||||
}, [])
|
||||
|
||||
async function deleteProduct(id: number) {
|
||||
const yes = confirm(`Are you sure you want to delete this product #${id}?`)
|
||||
if (!yes) return
|
||||
|
||||
await sni.delete('/cgi/ma/products/' + id)
|
||||
await fetchProducts()
|
||||
}
|
||||
|
||||
return (
|
||||
<ConsoleLayout>
|
||||
<Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<Box>
|
||||
<Typography variant="h3" component="h1">
|
||||
Matrix Marketplace
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
The new way to release your app, implement version check and auto updating.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" flexDirection="column" gap={2}>
|
||||
<Box>
|
||||
<NextLink passHref href="/console/matrix/products/new">
|
||||
<Button variant="contained">Create a product</Button>
|
||||
</NextLink>
|
||||
</Box>
|
||||
|
||||
<Grid container columns={{ xs: 1, sm: 2, md: 3 }} spacing={4}>
|
||||
{products.map((p) => (
|
||||
<Grid size={1} key={p.id}>
|
||||
<Card sx={{ width: '100%' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
{p.name}
|
||||
</Typography>
|
||||
<Typography variant="body1">{p.description}</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<NextLink passHref href={`/console/matrix/products/${p.id}`}>
|
||||
<Button size="small">Details</Button>
|
||||
</NextLink>
|
||||
<NextLink passHref href={`/console/matrix/products/${p.id}/edit`}>
|
||||
<Button size="small">Edit</Button>
|
||||
</NextLink>
|
||||
<Button size="small" color="error" onClick={() => deleteProduct(p.id)}>
|
||||
Delete
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Container>
|
||||
</ConsoleLayout>
|
||||
)
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
||||
import { Typography, Container, Box } from '@mui/material'
|
||||
import { MaProduct, sni } from 'solar-js-sdk'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
import MaProductForm, { MatrixProductForm } from '@/components/matrix/MaProductForm'
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = (async (context) => {
|
||||
const id = context.params!.id
|
||||
|
||||
const { data } = await sni.get<MaProduct>('/cgi/ma/products/' + id)
|
||||
|
||||
return getConsoleStaticProps({
|
||||
props: {
|
||||
title: `Edit Product "${data.name}"`,
|
||||
product: data,
|
||||
},
|
||||
})
|
||||
}) satisfies GetServerSideProps<{ product: MaProduct }>
|
||||
|
||||
export default function ProductEdit({ product }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
async function onSubmit(data: MatrixProductForm) {
|
||||
await sni.put('/cgi/ma/products/' + product.id, data)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConsoleLayout>
|
||||
<Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<Box>
|
||||
<Typography variant="h3" component="h1">
|
||||
Edit product
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">{product.name}</Typography>
|
||||
</Box>
|
||||
|
||||
<MaProductForm onSubmit={onSubmit} defaultValue={product} />
|
||||
</Container>
|
||||
</ConsoleLayout>
|
||||
)
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
||||
import { Box, Button, Container, Typography, Grid2 as Grid, Card, CardContent, CardActions } from '@mui/material'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
import { sni, MaProduct, MaRelease, getAttachmentUrl } from 'solar-js-sdk'
|
||||
import { useEffect, useState } from 'react'
|
||||
import NextLink from 'next/link'
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = (async (context) => {
|
||||
const id = context.params!.id
|
||||
|
||||
const { data } = await sni.get<MaProduct>('/cgi/ma/products/' + id)
|
||||
|
||||
return getConsoleStaticProps({
|
||||
props: {
|
||||
title: `Product "${data.name}"`,
|
||||
product: data,
|
||||
},
|
||||
})
|
||||
}) satisfies GetServerSideProps<{ product: MaProduct }>
|
||||
|
||||
export default function ProductDetails({ product }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const [releases, setReleases] = useState<MaRelease[]>([])
|
||||
|
||||
async function fetchReleases() {
|
||||
const { data: resp } = await sni.get<{ data: MaRelease[] }>('/cgi/ma/products/' + product.id + '/releases', {
|
||||
params: {
|
||||
take: 10,
|
||||
},
|
||||
})
|
||||
|
||||
setReleases(resp.data)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchReleases()
|
||||
}, [])
|
||||
|
||||
async function deleteRelease(id: number) {
|
||||
const yes = confirm(`Are you sure you want to delete this release #${id}?`)
|
||||
if (!yes) return
|
||||
|
||||
await sni.delete('/cgi/ma/products/' + product.id + '/releases/' + id)
|
||||
await fetchReleases()
|
||||
}
|
||||
|
||||
return (
|
||||
<ConsoleLayout>
|
||||
<>
|
||||
{product.previews && (
|
||||
<img
|
||||
src={getAttachmentUrl(product.previews[0])}
|
||||
alt={product.name}
|
||||
style={{ objectFit: 'cover', aspectRatio: 16 / 5 }}
|
||||
className='border-b border-1'
|
||||
/>
|
||||
)}
|
||||
|
||||
<Container sx={{ pt: (product.previews ? 8 : 16), pb: 16, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<Box maxWidth="sm">
|
||||
<Typography variant="h3" component="h1">
|
||||
{product.name}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">{product.description}</Typography>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" flexDirection="column" gap={2}>
|
||||
<Typography variant="h4" component="h2">
|
||||
Releases
|
||||
</Typography>
|
||||
|
||||
<NextLink passHref href={`/console/matrix/products/${product.id}/releases/new`}>
|
||||
<Button variant="contained">Create a release</Button>
|
||||
</NextLink>
|
||||
|
||||
<Grid container columns={{ xs: 1, sm: 2, md: 3 }} spacing={2}>
|
||||
{releases.map((r: any) => (
|
||||
<Grid size={1} key={r.id}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="caption">{r.version}</Typography>
|
||||
<Typography variant="h5" component="h2">
|
||||
{r.meta.title}
|
||||
</Typography>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
{r.type == 0 ? 'Full Release' : 'Patch Release'}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1">{r.meta.description}</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<NextLink passHref href={`/console/matrix/products/${r.productId}/releases/${r.id}/edit`}>
|
||||
<Button size="small">Edit</Button>
|
||||
</NextLink>
|
||||
<Button size="small" color="error" onClick={() => deleteRelease(r.id)}>
|
||||
Delete
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Container>
|
||||
</>
|
||||
</ConsoleLayout>
|
||||
)
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
||||
import { Typography, Container, Box } from '@mui/material'
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
import MaReleaseForm, { MatrixReleaseForm } from '@/components/matrix/MaReleaseForm'
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = (async (context) => {
|
||||
const id = context.params!.id
|
||||
const releaseId = context.params!.release
|
||||
|
||||
const { data } = await sni.get<any>('/cgi/ma/products/' + id + '/releases/' + releaseId)
|
||||
|
||||
return getConsoleStaticProps({
|
||||
props: {
|
||||
title: `Edit Release v${data.version}`,
|
||||
release: data,
|
||||
},
|
||||
})
|
||||
}) satisfies GetServerSideProps<{ release: any }>
|
||||
|
||||
export default function ReleaseEdit({ release }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
async function onSubmit(data: MatrixReleaseForm) {
|
||||
await sni.put('/cgi/ma/products/' + release.productId + '/releases/' + release.id, data)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConsoleLayout>
|
||||
<Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<Box>
|
||||
<Typography variant="h3" component="h1">
|
||||
Edit releases
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">{release.meta.title}</Typography>
|
||||
</Box>
|
||||
|
||||
<MaReleaseForm onSubmit={onSubmit} defaultValue={release} parent={{ id: release.productId }} />
|
||||
</Container>
|
||||
</ConsoleLayout>
|
||||
)
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
||||
import { Typography, Container, Box } from '@mui/material'
|
||||
import { MaProduct, sni } from 'solar-js-sdk'
|
||||
|
||||
import MaReleaseForm, { MatrixReleaseForm } from '@/components/matrix/MaReleaseForm'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = (async (context) => {
|
||||
const id = context.params!.id
|
||||
|
||||
const { data } = await sni.get<MaProduct>('/cgi/ma/products/' + id)
|
||||
|
||||
return getConsoleStaticProps({
|
||||
props: {
|
||||
title: `New Release for "${data.name}"`,
|
||||
product: data,
|
||||
},
|
||||
})
|
||||
}) satisfies GetServerSideProps<{ product: MaProduct }>
|
||||
|
||||
export default function ReleaseNew({ product }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
async function onSubmit(data: MatrixReleaseForm) {
|
||||
await sni.post(`/cgi/ma/products/${product.id}/releases`, data)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConsoleLayout>
|
||||
<Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<Box>
|
||||
<Typography variant="h3" component="h1">
|
||||
Create a release
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">for {product.name}</Typography>
|
||||
</Box>
|
||||
|
||||
<MaReleaseForm onSubmit={onSubmit} parent={product} />
|
||||
</Container>
|
||||
</ConsoleLayout>
|
||||
)
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
||||
import { Typography, Container } from '@mui/material'
|
||||
import { sni } from 'solar-js-sdk'
|
||||
|
||||
import MaProductForm, { MatrixProductForm } from '@/components/matrix/MaProductForm'
|
||||
|
||||
export async function getStaticProps() {
|
||||
return getConsoleStaticProps({
|
||||
props: {
|
||||
title: 'New Product',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default function ProductNew() {
|
||||
async function onSubmit(data: MatrixProductForm) {
|
||||
await sni.post('/cgi/ma/products', data)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConsoleLayout>
|
||||
<Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<Typography variant="h3" component="h1">
|
||||
Create a product
|
||||
</Typography>
|
||||
|
||||
<MaProductForm onSubmit={onSubmit} />
|
||||
</Container>
|
||||
</ConsoleLayout>
|
||||
)
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
import { CountdownTimer } from '@/components/events/CountdownTimer'
|
||||
import { Box, Container, Typography } from '@mui/material'
|
||||
import { Noto_Serif_TC } from 'next/font/google'
|
||||
import { useState } from 'react'
|
||||
|
||||
const serifFont = Noto_Serif_TC({
|
||||
weight: ['400', '500', '700'],
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
export default function LunarCountdownFor2025() {
|
||||
const [isCountdown, setIsCountdown] = useState(true)
|
||||
|
||||
return (
|
||||
<Container sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: 'calc(100vh - 64px)' }}>
|
||||
<Box>
|
||||
<Typography style={serifFont.style} sx={{ textAlign: 'center' }}>
|
||||
距离
|
||||
</Typography>
|
||||
<Typography variant="h5" style={serifFont.style} sx={{ textAlign: 'center', fontWeight: 'bold' }}>
|
||||
二〇二五乙巳年
|
||||
</Typography>
|
||||
<Typography style={serifFont.style} sx={{ textAlign: 'center', mb: 3 }}>
|
||||
{isCountdown ? '还有' : '已经'}
|
||||
</Typography>
|
||||
<CountdownTimer
|
||||
targetDate={new Date('2025-01-29T00:00:00')}
|
||||
onUpdate={({ isCountdown }) => setIsCountdown(isCountdown)}
|
||||
/>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { Container, Box, Typography, CircularProgress, Alert, Collapse } from '@mui/material'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
|
||||
import 'animate.css'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
|
||||
export default function AccountConfirm() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
async function confirm() {
|
||||
try {
|
||||
await sni.post('/cgi/id/users/me/confirm', {
|
||||
code: searchParams.get('code'),
|
||||
})
|
||||
router.push('/')
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
confirm()
|
||||
}, [searchParams])
|
||||
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
height: 'calc(100vh - 64px)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Typography variant="h5" component="h1">
|
||||
Confirm Account
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" component="h2">
|
||||
Confirm your registeration on Solar Network
|
||||
</Typography>
|
||||
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert sx={{ mt: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
{!error && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<CircularProgress />
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ mt: 3 }}
|
||||
className="animate__animated animate__flash animate__infinite"
|
||||
style={{ '--animate-duration': '3s' } as any}
|
||||
>
|
||||
Hold on a moment, we're working on it...
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { Container, Box, Typography, Alert, Collapse, Button } from '@mui/material'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
|
||||
export default function AccountDeletion() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
async function confirm() {
|
||||
try {
|
||||
setBusy(true)
|
||||
await sni.patch('/cgi/id/users/me/deletion', {
|
||||
code: searchParams.get('code'),
|
||||
})
|
||||
router.push('/')
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
height: 'calc(100vh - 64px)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Typography variant="h5" component="h1">
|
||||
Delete Account
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" component="h2">
|
||||
Confirm delete your account from Solar Network
|
||||
</Typography>
|
||||
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert sx={{ mt: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Are you sure you want to delete your account? This action is irreversible. All the resources created by you
|
||||
or related to your account will be deleted.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2">
|
||||
If you have changed your mind, you can close this tab at any time, nothing will be affected.
|
||||
</Typography>
|
||||
|
||||
<Button sx={{ mt: 3 }} onClick={() => confirm()} disabled={busy}>
|
||||
Confirm
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { Container, Box, Typography, Alert, Collapse, Button, TextField } from '@mui/material'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
|
||||
export type SnResetPasswordForm = {
|
||||
password: string
|
||||
}
|
||||
|
||||
export default function AccountPasswordReset() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const { handleSubmit, register } = useForm<SnResetPasswordForm>()
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
async function confirm(data: any) {
|
||||
try {
|
||||
setBusy(true)
|
||||
await sni.patch('/cgi/id/users/me/password-reset', {
|
||||
code: searchParams.get('code'),
|
||||
new_password: data.password,
|
||||
})
|
||||
router.push('/')
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
height: 'calc(100vh - 64px)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Typography variant="h5" component="h1">
|
||||
Reset Password
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" component="h2">
|
||||
Reset your password on Solar Network
|
||||
</Typography>
|
||||
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert sx={{ mt: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<form onSubmit={handleSubmit(confirm)}>
|
||||
<Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', width: '100%', gap: 2, textAlign: 'center' }}>
|
||||
<TextField
|
||||
label="New Password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
{...register('password', { required: true })}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={busy}>
|
||||
Next
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
import { Alert, AlertTitle, Box, Button, Chip, Container, Grid2 as Grid, Link, Paper, Typography } from '@mui/material'
|
||||
import { Roboto_Serif } from 'next/font/google'
|
||||
import NextLink from 'next/link'
|
||||
import Image from 'next/image'
|
||||
|
||||
import StarIcon from '@mui/icons-material/Star'
|
||||
import ArrowForward from '@mui/icons-material/ArrowForward'
|
||||
import ArrowDownward from '@mui/icons-material/ArrowDownward'
|
||||
import LaunchIcon from '@mui/icons-material/Launch'
|
||||
|
||||
import ImgSolarNetworkLabeled from '@/assets/products/solar-network/labeled.webp'
|
||||
|
||||
const fontSerif = Roboto_Serif({
|
||||
subsets: ['latin'],
|
||||
weight: ['300'],
|
||||
display: 'swap',
|
||||
style: 'italic',
|
||||
})
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Container sx={{ py: 24, display: 'flex', flexDirection: 'column', gap: 32 }}>
|
||||
<Alert
|
||||
variant="filled"
|
||||
icon={<span>🎉</span>}
|
||||
severity="info"
|
||||
action={
|
||||
<NextLink href="/events/2025-lunar-countdown" passHref>
|
||||
<Button color="inherit" size="small">
|
||||
立即前往
|
||||
</Button>
|
||||
</NextLink>
|
||||
}
|
||||
>
|
||||
<AlertTitle gutterBottom={false}>预祝农历新年</AlertTitle>
|
||||
索尔幸茨的 2025 农历新年倒计时现已开启!
|
||||
</Alert>
|
||||
|
||||
<Box>
|
||||
<Image src="/logo.png" width={128} height={128} alt="company logo" className="mb-2 dark:invert" />
|
||||
<Typography variant="h3" component="h1" gutterBottom>
|
||||
Welcome to <br />
|
||||
the Solsynth Capital.
|
||||
</Typography>
|
||||
<Typography variant="body1" fontSize={20} sx={{ mb: 2 }}>
|
||||
A vibrant creating wonderful software and hope the future will be brighter.
|
||||
</Typography>
|
||||
|
||||
<Link href="#products">
|
||||
Explore our works
|
||||
<ArrowDownward sx={{ fontSize: 15, marginLeft: 0.5 }} />
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
<Box id="products">
|
||||
<Grid container columns={{ xs: 1, sm: 1, md: 2 }} spacing={4}>
|
||||
<Grid size={1}>
|
||||
<Chip
|
||||
label="Featured Project"
|
||||
variant="outlined"
|
||||
icon={<StarIcon sx={{ fontSize: 18 }} />}
|
||||
sx={{ px: 1, mb: 2 }}
|
||||
/>
|
||||
<Typography variant="h4" component="h2">
|
||||
Solar Network
|
||||
</Typography>
|
||||
<Typography variant="body1" fontSize={16} sx={{ mb: 2 }}>
|
||||
The next generation social network. But not only a social media.
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
fontSize={26}
|
||||
fontFamily={fontSerif.style.fontFamily}
|
||||
sx={{ mb: 2, width: 'fit-content', fontStyle: 'italic' }}
|
||||
className="textmarker-effect active"
|
||||
>
|
||||
Social Network, Redefined.
|
||||
</Typography>
|
||||
|
||||
<NextLink passHref href="/products/solar-network">
|
||||
<Link component="span">
|
||||
Explore more you can do with Solar Network
|
||||
<ArrowForward sx={{ fontSize: 15, marginLeft: 0.5 }} />
|
||||
</Link>
|
||||
</NextLink>
|
||||
</Grid>
|
||||
<Grid size={1}>
|
||||
<Paper sx={{ position: 'relative', aspectRatio: 16 / 10, width: '100%' }}>
|
||||
<Image
|
||||
src={ImgSolarNetworkLabeled}
|
||||
alt="solian the app preview"
|
||||
fill
|
||||
style={{ objectFit: 'cover', borderRadius: '8px' }}
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Box id="production">
|
||||
<Grid container columns={{ xs: 1, sm: 1, md: 2 }} spacing={4}>
|
||||
<Grid size={1}>
|
||||
<Image
|
||||
src="/product-branding.webp"
|
||||
alt="product branding mark"
|
||||
width={256}
|
||||
height={80}
|
||||
style={{ marginLeft: '-20px' }}
|
||||
className="dark:invert"
|
||||
/>
|
||||
<Typography variant="h4" component="h2" sx={{ my: 2 }}>
|
||||
Made by Solsynth
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||
There are a lot of other great projects was made by Solsynth, and most of them are open sourced. You can
|
||||
check them on our GitHub organization.
|
||||
</Typography>
|
||||
|
||||
<NextLink passHref href="https://www.github.com/Solsynth" target="_blank">
|
||||
<Link component="span">
|
||||
Check out our GitHub page
|
||||
<LaunchIcon sx={{ fontSize: 15, marginLeft: 0.5 }} />
|
||||
</Link>
|
||||
</NextLink>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import { Typography, Container } from '@mui/material'
|
||||
|
||||
export default function MatrixMarketplace() {
|
||||
return (
|
||||
<Container sx={{ py: 24, display: 'flex', flexDirection: 'column', gap: 32 }}>
|
||||
<Typography variant="h3" component="h1">
|
||||
Matrix Marketplace
|
||||
</Typography>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,245 +0,0 @@
|
||||
import { getAttachmentUrl, sni } from 'solar-js-sdk'
|
||||
import { SnPost } from 'solar-js-sdk'
|
||||
import { listAttachment, SnAttachment } from 'solar-js-sdk'
|
||||
import {
|
||||
Grid2 as Grid,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Avatar,
|
||||
Box,
|
||||
Collapse,
|
||||
Container,
|
||||
IconButton,
|
||||
Link,
|
||||
Typography,
|
||||
Divider,
|
||||
} from '@mui/material'
|
||||
import { AttachmentItem } from '@/components/attachments/AttachmentItem'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { unified } from 'unified'
|
||||
import Head from 'next/head'
|
||||
import Image from 'next/image'
|
||||
import rehypeSanitize from 'rehype-sanitize'
|
||||
import rehypeStringify from 'rehype-stringify'
|
||||
import remarkBreaks from 'remark-breaks'
|
||||
import remarkParse from 'remark-parse'
|
||||
import remarkRehype from 'remark-rehype'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
|
||||
export const getServerSideProps = (async (context) => {
|
||||
const id = context.params!.id as string[]
|
||||
try {
|
||||
const { data: post } = await sni.get<SnPost>('/cgi/co/posts/' + id.join(':'))
|
||||
if (post.body.content) {
|
||||
let processor: any = unified().use(remarkParse)
|
||||
if (post.type != 'article') {
|
||||
processor = processor.use(remarkBreaks)
|
||||
}
|
||||
post.body.content = post.body.content.replace(
|
||||
/!\[.*?\]\(solink:\/\/attachments\/([\w-]+)\)/g,
|
||||
'![alt](https://api.sn.solsynth.dev/cgi/uc/attachments/$1)',
|
||||
)
|
||||
const out = await processor
|
||||
.use(remarkRehype)
|
||||
.use(remarkGfm)
|
||||
.use(rehypeSanitize)
|
||||
.use(rehypeStringify)
|
||||
.process(post.body.content)
|
||||
post.body.rawContent = post.body.content
|
||||
post.body.content = String(out)
|
||||
}
|
||||
let attachments: SnAttachment[] = []
|
||||
if (post.body.attachments) {
|
||||
attachments = await listAttachment(post.body.attachments)
|
||||
}
|
||||
return { props: { post, attachments } }
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}) satisfies GetServerSideProps<{ post: SnPost; attachments: SnAttachment[] }>
|
||||
|
||||
export default function Post({ post, attachments }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const appLink = useMemo(() => `https://sn.solsynth.dev/posts/${post.id}`, [post])
|
||||
const link = useMemo(
|
||||
() =>
|
||||
post.alias && post.aliasPrefix
|
||||
? `https://solsynth.dev/posts/${post.aliasPrefix}/${post.alias}`
|
||||
: `https://solsynth.dev/posts/${post.id}`,
|
||||
[post],
|
||||
)
|
||||
|
||||
const title = useMemo(
|
||||
() =>
|
||||
post.body.title
|
||||
? `${post.body.title} / @${post.publisher.name} / Solar Network`
|
||||
: `Post #${post.id} / @${post.publisher.name} / Solar Network`,
|
||||
[post],
|
||||
)
|
||||
const description = useMemo(
|
||||
() =>
|
||||
post.body.description ? post.body.description : post.body.rawContent.replaceAll('\n', ' ').substring(0, 200),
|
||||
[post],
|
||||
)
|
||||
|
||||
const image = useMemo(() => {
|
||||
if (post.body.thumbnail) {
|
||||
return getAttachmentUrl(post.body.thumbnail)
|
||||
}
|
||||
if (attachments) {
|
||||
const images = attachments.filter((a) => a.mimetype.startsWith('image'))
|
||||
if (images && images[0]) return getAttachmentUrl(images[0].rid)
|
||||
}
|
||||
return null
|
||||
}, [post])
|
||||
const video = useMemo(() => {
|
||||
if (attachments) {
|
||||
const videos = attachments.filter((a) => a.mimetype.startsWith('video'))
|
||||
if (videos && videos[0]) return getAttachmentUrl(videos[0].rid)
|
||||
}
|
||||
return null
|
||||
}, [post])
|
||||
const audio = useMemo(() => {
|
||||
if (attachments) {
|
||||
const audios = attachments.filter((a) => a.mimetype.startsWith('audio'))
|
||||
if (audios && audios[0]) return getAttachmentUrl(audios[0].rid)
|
||||
}
|
||||
return null
|
||||
}, [post])
|
||||
|
||||
const displayableAttachments = useMemo(() => {
|
||||
if (post.type == 'article') {
|
||||
return attachments.filter((a) => !a.mimetype.startsWith('image'))
|
||||
}
|
||||
return attachments
|
||||
}, [post])
|
||||
|
||||
const [openAppHint, setOpenAppHint] = useState<boolean>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem('sol-hide-app-hint')) {
|
||||
setOpenAppHint(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (openAppHint === false) {
|
||||
localStorage.setItem('sol-hide-app-hint', 'yes')
|
||||
}
|
||||
}, [openAppHint])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta name="author" content={`@${post.publisher.name}`} />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:creator" content={`@${post.publisher.name}`} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
|
||||
<meta property="og:url" content={link} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:site_name" content="Solar Network" />
|
||||
{image && <meta property="og:image" content={image} />}
|
||||
{video && <meta property="og:video" content={video} />}
|
||||
{audio && <meta property="og:audio" content={audio} />}
|
||||
</Head>
|
||||
|
||||
<Collapse in={openAppHint}>
|
||||
<Alert
|
||||
variant="filled"
|
||||
severity="info"
|
||||
sx={{ borderRadius: 0, px: 3 }}
|
||||
action={
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setOpenAppHint(false)
|
||||
}}
|
||||
>
|
||||
<CloseIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<AlertTitle gutterBottom={false}>Open in Solian</AlertTitle>
|
||||
All feature supported, cross-platform, the official app of Solar Network.{' '}
|
||||
<Link href={appLink} color="#ffffff">
|
||||
Launch
|
||||
</Link>
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
{post.body.thumbnail && (
|
||||
<Box sx={{ aspectRatio: 16 / 9, position: 'relative', borderBottom: 1, borderTop: 1, borderColor: 'divider' }}>
|
||||
<Image src={getAttachmentUrl(post.body.thumbnail)} alt="post thumbnail" fill />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Container sx={{ mt: 3, pb: 5 }} maxWidth="md" component="article">
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Avatar src={getAttachmentUrl(post.publisher.avatar)} />
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography fontWeight="bold">{post.publisher.nick}</Typography>
|
||||
<Typography fontFamily="monospace" fontSize={13} lineHeight={1.2}>
|
||||
@{post.publisher.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ my: 2.5 }} display="flex" flexDirection="column" gap={1}>
|
||||
{(post.body.title || post.body.content) && (
|
||||
<Box>
|
||||
{post.body.title && <Typography variant="h6">{post.body.title}</Typography>}
|
||||
{post.body.description && <Typography variant="subtitle1">{post.body.description}</Typography>}
|
||||
</Box>
|
||||
)}
|
||||
<Box display="flex" gap={2} sx={{ opacity: 0.8 }}>
|
||||
<Typography variant="body2">
|
||||
Published at {new Date(post.publishedAt ?? post.createdAt).toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box sx={{ mt: 2.5, maxWidth: 'unset' }} className="prose prose-lg dark:prose-invert">
|
||||
{post.body.content && <div dangerouslySetInnerHTML={{ __html: post.body.content }} />}
|
||||
</Box>
|
||||
|
||||
{displayableAttachments && (
|
||||
<Grid
|
||||
container
|
||||
spacing={2}
|
||||
sx={{ mt: 3 }}
|
||||
columns={{
|
||||
xs: 1,
|
||||
sm: Math.min(2, attachments.length),
|
||||
md: Math.min(3, attachments.length),
|
||||
lg: Math.min(4, attachments.length),
|
||||
}}
|
||||
>
|
||||
{displayableAttachments.map((a) => (
|
||||
<Grid size={1} key={a.id}>
|
||||
<AttachmentItem item={a} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { SnPost } from 'solar-js-sdk'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { Feed } from 'feed'
|
||||
|
||||
function generateFeed(posts: SnPost[]): string {
|
||||
const feed = new Feed({
|
||||
title: 'Solar Network Posts',
|
||||
description: 'All posts on the Solar Network platform, and now you can view them via the RSS feed!',
|
||||
id: 'https://solsynth.dev/posts',
|
||||
link: 'https://solsynth.dev/posts',
|
||||
favicon: 'https://solsynth.dev/favicon.png',
|
||||
copyright: `All rights reserved ${new Date().getFullYear()} © Solsynth LLC & Post Publishers`,
|
||||
updated: new Date(posts[0].createdAt),
|
||||
generator: 'Capital',
|
||||
feedLinks: {},
|
||||
})
|
||||
|
||||
for (const p of posts) {
|
||||
feed.addItem({
|
||||
id: p.id.toString(),
|
||||
title: p.body.title ?? `Post #${p.id}`,
|
||||
description: p.body.description,
|
||||
link:
|
||||
p.alias && p.aliasPrefix
|
||||
? `https://solsynth.dev/posts/${p.aliasPrefix}/${p.alias}`
|
||||
: `https://solsynth.dev/posts/${p.id}`,
|
||||
content: p.body.content,
|
||||
date: new Date(p.publishedAt ?? p.createdAt),
|
||||
published: new Date(p.publishedAt ?? p.createdAt),
|
||||
copyright: `All right reserved ${new Date().getFullYear()} © @${p.publisher.name}`,
|
||||
author: [
|
||||
{
|
||||
name: `@${p.publisher.name}`,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
return feed.rss2()
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ res, query }) => {
|
||||
let page: number = parseInt(query.page as string)
|
||||
if (isNaN(page)) page = 1
|
||||
|
||||
const countPerPage = 20
|
||||
|
||||
const { data: resp } = await sni.get<{ data: SnPost[] }>('/cgi/co/posts', {
|
||||
params: {
|
||||
take: countPerPage,
|
||||
offset: (page - 1) * countPerPage,
|
||||
},
|
||||
})
|
||||
|
||||
const sitemap = generateFeed(resp.data)
|
||||
|
||||
res.setHeader('Content-Type', 'text/xml')
|
||||
res.write(sitemap)
|
||||
res.end()
|
||||
|
||||
return {
|
||||
props: {},
|
||||
}
|
||||
}
|
||||
|
||||
export default function PostSiteMap() {
|
||||
// getServerSideProps will do the heavy lifting
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
import { AttachmentItem } from '@/components/attachments/AttachmentItem'
|
||||
import { SnAttachment, listAttachment } from 'solar-js-sdk'
|
||||
import { getAttachmentUrl, sni } from 'solar-js-sdk'
|
||||
import { SnPost } from 'solar-js-sdk'
|
||||
import { Avatar, Box, Container, Divider, Grid2 as Grid, Link, Pagination, Paper, Typography } from '@mui/material'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
import NextLink from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import rehypeSanitize from 'rehype-sanitize'
|
||||
import rehypeStringify from 'rehype-stringify'
|
||||
import remarkBreaks from 'remark-breaks'
|
||||
import remarkParse from 'remark-parse'
|
||||
import remarkRehype from 'remark-rehype'
|
||||
import { unified } from 'unified'
|
||||
|
||||
type SnPostWithAttachments = SnPost & { attachments: SnAttachment[] }
|
||||
|
||||
export const getServerSideProps = (async (context) => {
|
||||
let page: number = parseInt(context.query.page as string)
|
||||
if (isNaN(page)) page = 1
|
||||
|
||||
const countPerPage = 10
|
||||
|
||||
try {
|
||||
const { data: resp } = await sni.get<{ data: SnPost[]; count: number }>('/cgi/co/posts', {
|
||||
params: {
|
||||
take: countPerPage,
|
||||
offset: (page - 1) * countPerPage,
|
||||
},
|
||||
})
|
||||
|
||||
const posts: SnPostWithAttachments[] = resp.data as SnPostWithAttachments[]
|
||||
for (let idx = 0; idx < posts.length; idx++) {
|
||||
const post = posts[idx]
|
||||
if (post.body.content) {
|
||||
let processor: any = unified().use(remarkParse)
|
||||
if (post.type != 'article') {
|
||||
processor = processor.use(remarkBreaks)
|
||||
}
|
||||
post.body.content = post.body.content.replace(
|
||||
/!\[.*?\]\(solink:\/\/attachments\/([\w-]+)\)/g,
|
||||
'![alt](https://api.sn.solsynth.dev/cgi/uc/attachments/$1)',
|
||||
)
|
||||
const out = await processor
|
||||
.use(remarkRehype)
|
||||
.use(rehypeSanitize)
|
||||
.use(rehypeStringify)
|
||||
.process(post.body.content)
|
||||
post.body.rawContent = post.body.content
|
||||
post.body.content = String(out)
|
||||
}
|
||||
if (post.body.attachments) {
|
||||
post.attachments = await listAttachment(post.body.attachments)
|
||||
if (post.type == 'article') {
|
||||
post.attachments = post.attachments.filter((a) => !a.mimetype.startsWith('image'))
|
||||
}
|
||||
}
|
||||
posts[idx] = post
|
||||
}
|
||||
|
||||
return { props: { posts, page, pages: Math.ceil(resp.count / countPerPage) } }
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}) satisfies GetServerSideProps<{ posts: SnPostWithAttachments[]; page: number; pages: number }>
|
||||
|
||||
export default function PostList({ posts, page, pages }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<Container sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 2 }} maxWidth="md">
|
||||
{posts.map((p) => (
|
||||
<Paper key={p.id} sx={{ px: 2, py: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Avatar src={getAttachmentUrl(p.publisher.avatar)} />
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography fontWeight="bold">{p.publisher.nick}</Typography>
|
||||
<Typography fontFamily="monospace" fontSize={13} lineHeight={1.2}>
|
||||
@{p.publisher.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<NextLink href={`/posts/${p.id}`} passHref>
|
||||
<Box>
|
||||
<Box sx={{ mt: 1.5, mb: 1 }} display="flex" flexDirection="column" gap={0.5}>
|
||||
{(p.body.title || p.body.content) && (
|
||||
<Box>
|
||||
{p.body.title && <Typography variant="h6">{p.body.title}</Typography>}
|
||||
{p.body.description && <Typography variant="subtitle1">{p.body.description}</Typography>}
|
||||
</Box>
|
||||
)}
|
||||
<Box display="flex" gap={2} sx={{ opacity: 0.8 }}>
|
||||
<Typography variant="body2">
|
||||
Published at {new Date(p.publishedAt ?? p.createdAt).toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box sx={{ maxWidth: 'unset' }} className="prose prose-md dark:prose-invert">
|
||||
{p.body.content && <div dangerouslySetInnerHTML={{ __html: p.body.content }} />}
|
||||
</Box>
|
||||
</Box>
|
||||
</NextLink>
|
||||
|
||||
{p.attachments && (
|
||||
<Grid
|
||||
container
|
||||
spacing={2}
|
||||
columns={{
|
||||
xs: 1,
|
||||
sm: Math.min(2, p.attachments.length),
|
||||
md: Math.min(3, p.attachments.length),
|
||||
lg: Math.min(4, p.attachments.length),
|
||||
}}
|
||||
>
|
||||
{p.attachments.map((a) => (
|
||||
<Grid size={1} key={a.id}>
|
||||
<AttachmentItem item={a} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Paper>
|
||||
))}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mx: 'auto',
|
||||
mb: 5,
|
||||
mt: 3,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
placeItems: 'center',
|
||||
gap: 1.5,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Pagination count={pages} page={page} onChange={(_, page) => router.push('/posts?page=' + page)} />
|
||||
<NextLink passHref href="/posts/feed" target="_blank" prefetch={false}>
|
||||
<Link fontSize={13}>RSS Feed</Link>
|
||||
</NextLink>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { SnPost } from 'solar-js-sdk'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { EnumChangefreq, SitemapItem, SitemapStream, streamToPromise } from 'sitemap'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
function generateSiteMap(posts: SnPost[]): Promise<string> {
|
||||
const links: SitemapItem[] = posts.map((p) => ({
|
||||
url: p.alias && p.aliasPrefix ? `/posts/${p.aliasPrefix}/${p.alias}` : `/posts/${p.id}`,
|
||||
lastmod: (p.editedAt ?? p.publishedAt ?? p.editedAt)?.toString(),
|
||||
changefreq: EnumChangefreq.DAILY,
|
||||
priority: 0.9,
|
||||
img: [],
|
||||
video: [],
|
||||
links: [],
|
||||
}))
|
||||
|
||||
const stream = new SitemapStream({ hostname: 'https://solsynth.dev' })
|
||||
|
||||
return streamToPromise(Readable.from(links).pipe(stream)).then((data) => data.toString())
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ res, query }) => {
|
||||
let page: number = parseInt(query.page as string)
|
||||
if (isNaN(page)) page = 1
|
||||
|
||||
const countPerPage = 500
|
||||
|
||||
const { data: resp } = await sni.get<{ data: SnPost[] }>('/cgi/co/posts/minimal', {
|
||||
params: {
|
||||
take: countPerPage,
|
||||
offset: (page - 1) * countPerPage,
|
||||
},
|
||||
})
|
||||
|
||||
const sitemap = await generateSiteMap(resp.data)
|
||||
|
||||
res.setHeader('Content-Type', 'text/xml')
|
||||
res.write(sitemap)
|
||||
res.end()
|
||||
|
||||
return {
|
||||
props: {},
|
||||
}
|
||||
}
|
||||
|
||||
export default function PostSiteMap() {
|
||||
// getServerSideProps will do the heavy lifting
|
||||
}
|
@ -1,230 +0,0 @@
|
||||
import {
|
||||
Link,
|
||||
Container,
|
||||
Box,
|
||||
Typography,
|
||||
Chip,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
} from '@mui/material'
|
||||
import { JSX } from 'react'
|
||||
import { Roboto_Serif } from 'next/font/google'
|
||||
import Image from 'next/image'
|
||||
import NextLink from 'next/link'
|
||||
|
||||
import ArrowDownward from '@mui/icons-material/ArrowDownward'
|
||||
import DownloadIcon from '@mui/icons-material/Download'
|
||||
import LaunchIcon from '@mui/icons-material/Launch'
|
||||
import AppleIcon from '@mui/icons-material/Apple'
|
||||
import AndroidIcon from '@mui/icons-material/Android'
|
||||
import WindowIcon from '@mui/icons-material/Window'
|
||||
import WebIcon from '@mui/icons-material/Public'
|
||||
import CodeIcon from '@mui/icons-material/Code'
|
||||
|
||||
import ImgSolarNetworkIcon from '@/assets/products/solar-network/icon.png'
|
||||
import ImgSolarNetworkAlpha from '@/assets/products/solar-network/alpha.webp'
|
||||
|
||||
import 'animate.css'
|
||||
|
||||
interface DownloadableAsset {
|
||||
icon: JSX.Element
|
||||
title: string
|
||||
href: string
|
||||
open?: boolean
|
||||
}
|
||||
|
||||
interface AskableQuestion {
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
const fontSerif = Roboto_Serif({
|
||||
subsets: ['latin'],
|
||||
weight: ['300'],
|
||||
display: 'swap',
|
||||
style: 'italic',
|
||||
})
|
||||
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
title: 'Solar Network',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function ProductSolarNetwork() {
|
||||
const downloadableAssets: DownloadableAsset[] = [
|
||||
{
|
||||
icon: <AppleIcon />,
|
||||
title: 'iOS / macOS (App Store)',
|
||||
href: 'https://apps.apple.com/us/app/solian/id6499032345?itscg=30200&itsct=apps_box_link&mttnsubad=6499032345',
|
||||
},
|
||||
{
|
||||
icon: <AppleIcon />,
|
||||
title: 'iOS / macOS (TestFlight)',
|
||||
href: 'https://testflight.apple.com/join/YJ0lmN6O',
|
||||
},
|
||||
{
|
||||
icon: <AndroidIcon />,
|
||||
title: 'Android',
|
||||
href: 'https://files.solsynth.dev/production01/solian/app-arm64-v8a-release.apk',
|
||||
},
|
||||
{
|
||||
icon: <WindowIcon />,
|
||||
title: 'Windows',
|
||||
href: 'https://files.solsynth.dev/production01/solian/windows-x86_64-release.zip',
|
||||
},
|
||||
{
|
||||
icon: <WebIcon />,
|
||||
title: 'Web',
|
||||
href: 'https://sn.solsynth.dev',
|
||||
open: true,
|
||||
},
|
||||
{
|
||||
icon: <CodeIcon />,
|
||||
title: 'Source Code',
|
||||
href: 'https://github.com/Solsynth/HyperNet.Surface',
|
||||
},
|
||||
]
|
||||
|
||||
const askableQuestions: AskableQuestion[] = [
|
||||
{
|
||||
question: "What's the relationship between Solar Network and Solian?",
|
||||
answer:
|
||||
'Solian is the official app made for Solar Network. And the Solar Network is the official HyperNet instance hosted by Solsynth LLC. For simple, Solian is the app, and the Solar Network is the platform.',
|
||||
},
|
||||
{
|
||||
question: "What's the relationship between Solar Network and HyperNet?",
|
||||
answer:
|
||||
'HyperNet is the entire project including frontend app (also knowns as Solian for public) and the backend server. And the Solar Network is the official HyperNet instance which hosted and managed by Solsynth LLC who developed the HyperNet Project.',
|
||||
},
|
||||
{
|
||||
question: 'Which rules do I need to follow while using Solar Network?',
|
||||
answer:
|
||||
'Check out our Terms & Conditions for a detailed explanation of what you can do and cannot do on Solar Network. If you violate any of these rules, we have the right to suspend or terminate your account., you can see them in the drawer.',
|
||||
},
|
||||
{
|
||||
question: 'If I have any question about Solar Network, where can I get help?',
|
||||
answer: 'Feel free to email as at lily@solsynth.dev',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 24, display: 'flex', flexDirection: 'column', gap: 32 }}>
|
||||
<Box sx={{ textAlign: 'center', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Image
|
||||
src={ImgSolarNetworkIcon}
|
||||
width={128}
|
||||
height={128}
|
||||
style={{ objectFit: 'cover' }}
|
||||
className="shadow-xl rounded-2xl mx-auto mb-8 border border-1 border-gray-200 dark:invert"
|
||||
alt="solar network icon"
|
||||
/>
|
||||
<Box position="relative" width="fit-content" className="animate__animated animate__fadeInUp">
|
||||
<Typography variant="h4" component="h1">
|
||||
Solar Network
|
||||
</Typography>
|
||||
<Box
|
||||
position="absolute"
|
||||
top={-14}
|
||||
right={-24}
|
||||
sx={{ rotate: '30deg' }}
|
||||
className="animate__animated animate__pulse animate__infinite"
|
||||
>
|
||||
<Chip
|
||||
label="2.0"
|
||||
variant="outlined"
|
||||
sx={{ fontFamily: 'monospace', backgroundColor: 'background.default', fontSize: 12 }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="subtitle1" component="h1" className="animate__animated animate__fadeInUp">
|
||||
The next generation Social Network platform.
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
fontSize={26}
|
||||
fontFamily={fontSerif.style.fontFamily}
|
||||
sx={{ mt: 2.5, width: 'fit-content', fontStyle: 'italic' }}
|
||||
className="textmarker-effect animate__animated animate__fadeInUp"
|
||||
>
|
||||
Social Network, Redefined.
|
||||
</Typography>
|
||||
|
||||
<Link href="#download" sx={{ my: 2.5 }}>
|
||||
Download <DownloadIcon sx={{ fontSize: 15, marginLeft: 0.5 }} />
|
||||
</Link>
|
||||
|
||||
<Box position="relative" width="100%" sx={{ aspectRatio: 16 / 10, mt: 5 }}>
|
||||
<Image src={ImgSolarNetworkAlpha} fill alt="solar network screenshot" style={{ objectFit: 'cover' }} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box id="download">
|
||||
<Typography variant="h5" component="h2" textAlign="center" sx={{ mb: 5 }}>
|
||||
Download
|
||||
</Typography>
|
||||
|
||||
<Table sx={{ maxWidth: '800px', marginX: 'auto' }} aria-label="download table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell />
|
||||
<TableCell>Platform</TableCell>
|
||||
<TableCell align="right">Distribution</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{downloadableAssets.map((a) => (
|
||||
<TableRow key={a.href}>
|
||||
<TableCell>{a.icon}</TableCell>
|
||||
<TableCell>{a.title}</TableCell>
|
||||
<TableCell align="right">
|
||||
<NextLink passHref href={a.href} target="_blank">
|
||||
{a.open ? (
|
||||
<Link component="span">
|
||||
Open now
|
||||
<LaunchIcon sx={{ fontSize: 15, marginLeft: 0.5 }} />
|
||||
</Link>
|
||||
) : (
|
||||
<Link component="span">
|
||||
Download now
|
||||
<DownloadIcon sx={{ fontSize: 15, marginLeft: 0.5 }} />
|
||||
</Link>
|
||||
)}
|
||||
</NextLink>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
<Box id="faq">
|
||||
<Typography variant="h5" component="h2" textAlign="center" sx={{ mb: 5 }}>
|
||||
Frequently Asked Questions
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ maxWidth: '800px', marginX: 'auto' }}>
|
||||
{askableQuestions.map((q) => (
|
||||
<Accordion key={q.question}>
|
||||
<AccordionSummary expandIcon={<ArrowDownward />} aria-controls="panel1-content" id="panel1-header">
|
||||
<Typography component="span">{q.question}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography variant="body1">{q.answer}</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { EnumChangefreq, SitemapItem, SitemapStream, streamToPromise } from 'sitemap'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
function generateSiteMap(): Promise<string> {
|
||||
const links: SitemapItem[] = [
|
||||
{
|
||||
url: '/',
|
||||
lastmod: new Date().toString(),
|
||||
changefreq: EnumChangefreq.WEEKLY,
|
||||
priority: 0.7,
|
||||
img: [],
|
||||
video: [],
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
url: '/products/solar-network',
|
||||
lastmod: new Date().toString(),
|
||||
changefreq: EnumChangefreq.WEEKLY,
|
||||
priority: 0.7,
|
||||
img: [],
|
||||
video: [],
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
url: '/posts',
|
||||
lastmod: new Date().toString(),
|
||||
changefreq: EnumChangefreq.HOURLY,
|
||||
priority: 0.8,
|
||||
img: [],
|
||||
video: [],
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
url: '/posts/sitemap.xml',
|
||||
lastmod: new Date().toString(),
|
||||
changefreq: EnumChangefreq.HOURLY,
|
||||
priority: 0.8,
|
||||
img: [],
|
||||
video: [],
|
||||
links: [],
|
||||
},
|
||||
]
|
||||
|
||||
const stream = new SitemapStream({ hostname: 'https://solsynth.dev' })
|
||||
|
||||
return streamToPromise(Readable.from(links).pipe(stream)).then((data) => data.toString())
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ res, query }) => {
|
||||
const sitemap = await generateSiteMap()
|
||||
|
||||
res.setHeader('Content-Type', 'text/xml')
|
||||
res.write(sitemap)
|
||||
res.end()
|
||||
|
||||
return {
|
||||
props: {},
|
||||
}
|
||||
}
|
||||
|
||||
export default function PostSiteMap() {
|
||||
// getServerSideProps will do the heavy lifting
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import { Box, Button, Card, CardActions, CardContent, Container, Typography } from '@mui/material'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function Terms() {
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Typography variant="h4" component="h1" gutterBottom sx={{ mt: 2 }}>
|
||||
Terms & Conditions
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1">
|
||||
Nothing too special here, just some legal files which make our lawyers happy.{' '}
|
||||
<del>Do we really have a lawyer?</del>
|
||||
</Typography>
|
||||
|
||||
<Box display="flex" flexDirection="column" gap={2} sx={{ mt: 2 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h5" component="h2" gutterBottom>
|
||||
Privacy Policy
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2">Learn about how do we protect your data and privacy.</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Link href="/terms/privacy-policy" passHref>
|
||||
<Button size="small">Read this policy</Button>
|
||||
</Link>
|
||||
</CardActions>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h5" component="h2" gutterBottom>
|
||||
User Agreements
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2">
|
||||
Learn about how do we dealing the user generated content on Solar Network, distrubution of our products
|
||||
and more.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Link href="/terms/user-agreements" passHref>
|
||||
<Button size="small">Read this agreements</Button>
|
||||
</Link>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import { Box, Container, Divider, Typography } from '@mui/material'
|
||||
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
title: 'Privacy Policy',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function PrivacyPolicy() {
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Typography variant="h3" component="h1" sx={{ mt: 2, mb: 5 }}>
|
||||
Privacy Policy
|
||||
</Typography>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box component="article" sx={{ my: 5, maxWidth: 'unset' }} className="prose prose-lg dark:prose-invert">
|
||||
<h2 id="introduction">Introduction</h2>
|
||||
<p>
|
||||
We take your privacy seriously. This privacy policy outlines the types of personal information we collect, how
|
||||
we use it, and the measures we take to protect your data.
|
||||
</p>
|
||||
<h2 id="information-collection">Information Collection</h2>
|
||||
<p>
|
||||
We collect personal information only when necessary to provide our services. This may include your name, email
|
||||
address, and other relevant details.
|
||||
</p>
|
||||
<h2 id="use-of-information">Use of Information</h2>
|
||||
<p>We use your personal information to:</p>
|
||||
<ul>
|
||||
<li>Provide and improve our services</li>
|
||||
<li>Communicate with you about updates or important information</li>
|
||||
<li>Ensure compliance with legal obligations</li>
|
||||
</ul>
|
||||
<h2 id="data-sharing">Data Sharing</h2>
|
||||
<p>We do not sell, trade, or share your personal information with third parties except as required by law.</p>
|
||||
<h2 id="data-security">Data Security</h2>
|
||||
<p>
|
||||
We implement robust security measures to protect your personal information from unauthorized access,
|
||||
alteration, disclosure, or destruction.
|
||||
</p>
|
||||
<h2 id="your-rights">Your Rights</h2>
|
||||
<p>You have the right to:</p>
|
||||
<ul>
|
||||
<li>Access the personal information we hold about you</li>
|
||||
<li>Request corrections to your personal information</li>
|
||||
<li>Request the deletion of your personal information</li>
|
||||
</ul>
|
||||
<h2 id="contact-us">Contact Us</h2>
|
||||
<p>
|
||||
If you have any questions or concerns about this privacy policy or our data practices, please contact us at
|
||||
lily@solsynth.dev.
|
||||
</p>
|
||||
<h2 id="changes-to-this-policy">Changes to This Policy</h2>
|
||||
<p>
|
||||
We may update this privacy policy from time to time. Any changes will be posted on this page, and we will
|
||||
notify you of any significant changes.
|
||||
</p>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
import { Box, Container, Divider, Typography } from '@mui/material'
|
||||
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
title: 'User Agreements',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function PrivacyPolicy() {
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Typography variant="h3" component="h1" sx={{ mt: 2, mb: 5 }}>
|
||||
User Agreements
|
||||
</Typography>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box component="article" sx={{ my: 5, maxWidth: 'unset' }} className="prose prose-lg dark:prose-invert">
|
||||
<p>
|
||||
This Agreement applies to all Solsynth LLC products, including but not limited to Solar Network, Solian,
|
||||
DietaryGuard, AceField.
|
||||
</p>
|
||||
<h2 id="provision-and-discontinuance-of-service">Provision and Discontinuance of Service</h2>
|
||||
<p>
|
||||
Solsynth LLC will provide equal service to all living things in the world, including grasshoppers. We also
|
||||
reserve the right to stop service to any user. We do not require prior notice for discontinuing services to
|
||||
some users.
|
||||
</p>
|
||||
<h2 id="user-generated-content">User Generated Content</h2>
|
||||
<p>
|
||||
Any content posted on Solar Network (including but not limited to posts, articles, attachments) grants
|
||||
Solsynth LLC the right to display it by default. Unless otherwise stated by the user, all rights are reserved
|
||||
by the original poster, and reprints should be authorized by the original poster.
|
||||
</p>
|
||||
<h3 id="reproduction-recognition">Reproduction Recognition</h3>
|
||||
<p>
|
||||
Unless specifically stated by the poster, all content is subject to the definition of reprint in this section.
|
||||
</p>
|
||||
<p>
|
||||
Republishing means uploading the content of the original post to another platform or to the Solar Network,
|
||||
either unchanged or with minor modifications, provided that simultaneous reposting of the post, embedded
|
||||
components, and links to the presentation do not constitute republishing. Republishing also requires
|
||||
attribution when authorized by the original poster.
|
||||
</p>
|
||||
<h3 id="freedom-of-speech">Freedom of Speech</h3>
|
||||
<p>
|
||||
We do not remove user-generated content except in cases of misuse of resources. We will not ask any user to
|
||||
remove any content.
|
||||
</p>
|
||||
<p>
|
||||
However, Solsynth LLC reserves the right to restrict and stop the display of content to the public that
|
||||
violates community guidelines (e.g., obscenity, violence, gore, anti-social, terrorist organizations, etc.).
|
||||
</p>
|
||||
<p>
|
||||
Although you have 100% freedom of speech on Solar Network. However, please be aware that freedom of speech
|
||||
does not mean that you will not be held accountable for what you say.
|
||||
</p>
|
||||
<h4 id="restriction-and-discontinuation">Restriction and Discontinuation</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
Restriction of Display: Discontinuation of related tweets, while retaining the right to access them
|
||||
directly through resource identifiers and sharing links.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Cease display: stop all access to the resource by anyone other than the author.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 id="resource-misuse-prevention-policy">Resource Misuse Prevention Policy</h2>
|
||||
<p>
|
||||
Although there are no capacity limitations for using Solar Network's data hosting services, resources
|
||||
determined to be abusive will be disenfranchised from some features. Solsynth LLC reserves the right to
|
||||
reclaim space on previously uploaded resources for deletion.
|
||||
</p>
|
||||
<h3 id="determination-of-misuse">Determination of Misuse</h3>
|
||||
<ul>
|
||||
<li>
|
||||
Uploading without using: e.g. uploading excessive attachments in Solar Network's Interactive Attachment
|
||||
Pool and not linking them to posts.
|
||||
</li>
|
||||
<li>Meaningless Posts: meaningless shuffling or wasting of Solar Network's storage resources</li>
|
||||
<li>
|
||||
Misuse: using Solar Network's public resources as if they were your own dedicated pool (see the
|
||||
Wiki's Dedicated Pools page for details).
|
||||
</li>
|
||||
</ul>
|
||||
<p>The Solsynth Trust & Safety Team is ultimately responsible for determining misuse.</p>
|
||||
<h2 id="secondary-releases">Secondary Releases</h2>
|
||||
<p>A secondary release is when our assets are downloaded and re-hosted on another site.</p>
|
||||
<h3 id="product-secondary-release">Product Secondary Release</h3>
|
||||
<p>
|
||||
Unless otherwise stated, Solsynth LLC products are not available for secondary distribution, please do not
|
||||
download our product builds and upload them twice to another site. Please do not download our product builds
|
||||
and upload them to other sites. <strong>Secondary distribution for commercial use is not permitted.</strong>.
|
||||
</p>
|
||||
<p>
|
||||
What you should do is post a link to our product on another site. Or use the embedded component. And indicate
|
||||
Solsynth LLC All Rights Reserved.
|
||||
</p>
|
||||
<p>If you want to build a mirror site of our products, please contact us to waive this rule.</p>
|
||||
<h3 id="secondary-distribution-of-source-code">Secondary distribution of source code</h3>
|
||||
<p>
|
||||
We do not allow any form of redistribution of source code (except for Forks). This includes, but is not
|
||||
limited to, mirroring code repositories on GitHub or the Solsynth Code Repository to other Git providers such
|
||||
as GitLab, Gitee, and so on.
|
||||
<strong>Selling source code twice is not allowed. </strong>
|
||||
</p>
|
||||
<p>
|
||||
For more information on source code usage regulations, please follow the open source license used by the
|
||||
project.
|
||||
</p>
|
||||
<p>If you would like to set up a mirror of our source code, please contact us to waive this policy.</p>
|
||||
<hr />
|
||||
<p>Solsynth LLC reserves the right of final interpretation of this agreement.</p>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,153 +0,0 @@
|
||||
import { SnCheckInRecord } from 'solar-js-sdk'
|
||||
import { getAttachmentUrl, sni } from 'solar-js-sdk'
|
||||
import { SnAccount } from 'solar-js-sdk'
|
||||
import { Avatar, Box, Card, CardContent, Container, Grid2 as Grid, Typography } from '@mui/material'
|
||||
import { LineChart } from '@mui/x-charts'
|
||||
import type { InferGetServerSidePropsType, GetServerSideProps } from 'next'
|
||||
import { SnAccountBadgeMapping } from '@/services/user'
|
||||
import Image from 'next/image'
|
||||
|
||||
export const getServerSideProps = (async (context) => {
|
||||
const name = context.params!.name as string
|
||||
try {
|
||||
const { data: user } = await sni.get<SnAccount>('/cgi/id/users/' + name)
|
||||
const { data: checkIn } = await sni.get<{ data: SnCheckInRecord[] }>('/cgi/id/users/' + name + '/check-in', {
|
||||
params: { take: 14 },
|
||||
})
|
||||
return { props: { user, checkIn: checkIn.data } }
|
||||
} catch (err) {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}) satisfies GetServerSideProps<{ user: SnAccount; checkIn: SnCheckInRecord[] }>
|
||||
|
||||
export default function UserProfile({ user, checkIn }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
return (
|
||||
<>
|
||||
{user.banner && (
|
||||
<Box sx={{ aspectRatio: 16 / 5, position: 'relative' }}>
|
||||
<Image src={getAttachmentUrl(user.banner)} alt="account banner" style={{ objectFit: 'cover' }} fill />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Container sx={{ mt: 4, px: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
{user && <Avatar src={getAttachmentUrl(user.avatar)} />}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography fontWeight="bold">{user.nick}</Typography>
|
||||
<Typography fontFamily="monospace" fontSize={13} lineHeight={1.2}>
|
||||
@{user.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={2} sx={{ mt: 3 }}>
|
||||
<Grid size={{ xs: 12, sm: 12, md: 8 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Fortune History
|
||||
</Typography>
|
||||
<LineChart
|
||||
yAxis={[
|
||||
{
|
||||
data: [1, 2, 3, 4, 5],
|
||||
tickMinStep: 1,
|
||||
tickMaxStep: 1,
|
||||
valueFormatter(value, _) {
|
||||
const resultTierList = ['大凶', '凶', '中平', '吉', '大吉']
|
||||
return resultTierList[value]
|
||||
},
|
||||
},
|
||||
]}
|
||||
xAxis={[
|
||||
{
|
||||
scaleType: 'band',
|
||||
data: checkIn.map((c) => {
|
||||
const og = new Date(c.createdAt)
|
||||
og.setHours(0, 0, 0, 0)
|
||||
return og
|
||||
}),
|
||||
valueFormatter(value, _) {
|
||||
return new Date(value).toLocaleDateString('en-US', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
})
|
||||
},
|
||||
},
|
||||
]}
|
||||
series={[
|
||||
{
|
||||
data: checkIn.map((c) => c.resultTier),
|
||||
valueFormatter(value, _) {
|
||||
const resultTierList = ['大凶', '凶', '中平', '吉', '大吉']
|
||||
return resultTierList[value ?? 0]
|
||||
},
|
||||
},
|
||||
]}
|
||||
height={300}
|
||||
margin={{ top: 16, bottom: 24 }}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid
|
||||
size={{ xs: 12, sm: 12, md: 4 }}
|
||||
order={{ xs: -1, sm: -1, md: 1 }}
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
|
||||
>
|
||||
{user.badges && (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Badges
|
||||
</Typography>
|
||||
|
||||
<Box display="flex" flexDirection="column" gap={0.5}>
|
||||
{user.badges.map((b) => (
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'start' }} key={b.id}>
|
||||
{SnAccountBadgeMapping[b.type].icon}
|
||||
<Box>
|
||||
<Typography variant="body2">{SnAccountBadgeMapping[b.type].name}</Typography>
|
||||
{b.metadata.title && <Typography variant="subtitle2">{b.metadata.title}</Typography>}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Information
|
||||
</Typography>
|
||||
|
||||
{user.description && (
|
||||
<Typography variant="body1" gutterBottom>
|
||||
{user.description}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Typography variant="body2">
|
||||
Born on {new Date(user.profile!.birthday!).toLocaleDateString()}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Joined at {new Date(user.createdAt).toLocaleDateString()}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="overline" lineHeight={1} fontFamily="monospace">
|
||||
#{user.id.toString().padStart(8, '0')}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import { checkAuthenticatedClient, redirectToLogin } from 'solar-js-sdk'
|
||||
import { useUserStore } from 'solar-js-sdk'
|
||||
import { Avatar, Box, Button, Container, Typography } from '@mui/material'
|
||||
import { getAttachmentUrl } from 'solar-js-sdk'
|
||||
import { useEffect } from 'react'
|
||||
import { removeTokenCookies } from 'solar-js-sdk'
|
||||
import Image from 'next/image'
|
||||
|
||||
import LogoutIcon from '@mui/icons-material/Logout'
|
||||
import LaunchIcon from '@mui/icons-material/Launch'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function UserItself() {
|
||||
useEffect(() => {
|
||||
if (!checkAuthenticatedClient()) redirectToLogin()
|
||||
}, [])
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
function logout() {
|
||||
removeTokenCookies()
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{userStore.account?.banner && (
|
||||
<Box sx={{ aspectRatio: 16 / 5, position: 'relative' }}>
|
||||
<Image src={getAttachmentUrl(userStore.account!.banner)} alt="account banner" objectFit="cover" fill />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Container sx={{ mt: 4, px: 2 }}>
|
||||
<Typography variant="h5" component="h1">
|
||||
Your Solarpass
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
{userStore.account && <Avatar src={getAttachmentUrl(userStore.account.avatar)} />}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography fontWeight="bold">{userStore.account?.nick}</Typography>
|
||||
<Typography fontFamily="monospace" fontSize={13} lineHeight={1.2}>
|
||||
@{userStore.account?.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
<Link passHref href="https://sn.solsynth.dev/account" target="_blank">
|
||||
<Button variant="contained" color="primary" startIcon={<LaunchIcon />}>
|
||||
Open in Solian
|
||||
</Button>
|
||||
</Link>
|
||||
<Button variant="contained" color="error" startIcon={<LogoutIcon />} onClick={logout}>
|
||||
Logout
|
||||
</Button>
|
||||
</Box>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
14
src/routes/+layout.svelte
Normal file
@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
|
||||
import NavBar from '../components/navigation/NavBar.svelte';
|
||||
import Footer from '../components/navigation/Footer.svelte';
|
||||
</script>
|
||||
|
||||
<NavBar />
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<Footer />
|
61
src/routes/+page.svelte
Normal file
@ -0,0 +1,61 @@
|
||||
<div class="container px-12 mx-auto">
|
||||
<div class="landing-slide">
|
||||
<div class="landing-slide-first">
|
||||
<div
|
||||
class="aspect-square w-[128px] bg-neutral rounded-xl mb-6 ml-[-4px] flex justify-center items-center"
|
||||
>
|
||||
<enhanced:img src="../assets/solar.svg" alt="Studio Avatar" class="w-[90px] h-[96px] block" />
|
||||
</div>
|
||||
|
||||
<h2 class="text-2xl">Hi, there!</h2>
|
||||
<h1 class="text-6xl font-bold">We're Solsynth</h1>
|
||||
|
||||
<section class="mt-8 text-lg">
|
||||
<p>
|
||||
We're the world youngest Software <strike>Company</strike> Studio who
|
||||
make the software that everyone love.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="landing-slide-second">
|
||||
<div class="card w-full md:w-5/6 bg-base-100 shadow-xl">
|
||||
<div class="card-body pb-0">
|
||||
<h2 class="card-title">Products!</h2>
|
||||
<p>Take a look of all our lovely products!</p>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="card-body pt-0 grid grid-cols-4">
|
||||
<a href="/products/solar-network">
|
||||
<enhanced:img
|
||||
src="../assets/solian.png"
|
||||
alt="Solar Network"
|
||||
class="min-w-[64px] min-h-[64px] w-[64px] h-[64px] aspect-square block"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.landing-slide {
|
||||
gap: 1rem;
|
||||
height: 40rem;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.landing-slide {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.landing-slide-second {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
</style>
|
255
src/routes/products/solar-network/+page.svelte
Normal file
@ -0,0 +1,255 @@
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-3 px-12 py-5">
|
||||
<div class="feature-card horizontal-layout">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="icon"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
<section>
|
||||
<h2 class="card-title">Open-source</h2>
|
||||
<p>Entire project has been open-sourced under <b>AGPLv3</b> license.</p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="feature-card horizontal-layout">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="icon"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
<section>
|
||||
<h2 class="card-title">Large Attachments</h2>
|
||||
<p>Up to <b>1024MB</b> per attachment upload limit.</p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="lg:col-span-2 feature-card horizontal-layout">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="icon"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8.625 9.75a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375m-13.5 3.01c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.184-4.183a1.14 1.14 0 0 1 .778-.332 48.294 48.294 0 0 0 5.83-.498c1.585-.233 2.708-1.626 2.708-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
<section>
|
||||
<h2 class="card-title">Freedom of Speech</h2>
|
||||
<p>
|
||||
According to our principles, your freedom of speech cannot be taken
|
||||
away by anyone.
|
||||
<b>
|
||||
We will never delete any post from any user, even if we are coerced
|
||||
by others.
|
||||
</b>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<div
|
||||
class="lg:row-span-2 feature-card horizontal-layout lg:vertical-layout"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="icon"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
<section>
|
||||
<h2 class="card-title">100% Recyclable</h2>
|
||||
<p>
|
||||
All of our code are completely recyclable. All of them written by
|
||||
child labor and without salary. We even already achieved the goal of
|
||||
<b>Carbon Neutral</b>. Because we didn't produce any of them at all.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<div
|
||||
class="lg:col-span-2 max-lg:order-last keypoint-card horizontal-layout items-center"
|
||||
>
|
||||
<enhanced:img
|
||||
src="../../../assets/solian.png"
|
||||
alt="Solar Network"
|
||||
class="w-[64px] h-[64px] lg:ml-12"
|
||||
/>
|
||||
|
||||
<section class="lg:mr-12">
|
||||
<h2 class="card-title text-3xl">Solar Network</h2>
|
||||
<p>Link the entire Solar System together, forever.</p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="feature-card horizontal-layout">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="icon"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.182 15.182a4.5 4.5 0 0 1-6.364 0M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Z"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
<section>
|
||||
<h2 class="card-title">Reaction</h2>
|
||||
<p>
|
||||
No more like and dislike, now you can express your emotion with our
|
||||
brand new reaction system!
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="lg:col-span-2 feature-card horizontal-layout">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="icon"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
<section>
|
||||
<h2 class="card-title">Blazing Fast</h2>
|
||||
<p>
|
||||
Our server proudly powered by Golang and App made with flutter. All of
|
||||
these stuff make your scrolling, clicking and typing smooth like
|
||||
butter. Without any lagging.
|
||||
<i>By the way, we cannot eliminate the network delay.</i>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="feature-card horizontal-layout">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="icon"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
<section>
|
||||
<h2 class="card-title">Safe and uninspectable</h2>
|
||||
<p>
|
||||
All your data has been <b>encrypted</b> during the network transmission.
|
||||
No more goverment and internet provider inspect.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-12 pb-5 flex gap-3 justify-center items-center">
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
href="https://git.solsynth.dev/Hydrogen/Solian/releases"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
Download now
|
||||
</a>
|
||||
<a class="btn btn-secondary" href="https://lian.solsynth.dev">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
Open in browser
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.feature-card {
|
||||
@apply p-5 rounded-2xl bg-base-200;
|
||||
}
|
||||
|
||||
.keypoint-card {
|
||||
@apply p-5 bg-neutral text-neutral-content rounded-2xl shadow-lg;
|
||||
}
|
||||
|
||||
.horizontal-layout {
|
||||
@apply flex gap-4;
|
||||
}
|
||||
|
||||
.vertical-layout {
|
||||
@apply flex-col gap-4;
|
||||
}
|
||||
|
||||
.vertical-layout section {
|
||||
@apply px-2.5 mt-3;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@apply w-[48px] h-[48px] m-[8px] flex-shrink-0;
|
||||
}
|
||||
</style>
|
@ -1,15 +0,0 @@
|
||||
import { JSX } from 'react'
|
||||
|
||||
import ConstructionIcon from '@mui/icons-material/Construction'
|
||||
import FlagIcon from '@mui/icons-material/Flag'
|
||||
|
||||
export const SnAccountBadgeMapping: Record<string, { icon: JSX.Element; name: string }> = {
|
||||
'company.staff': {
|
||||
icon: <ConstructionIcon />,
|
||||
name: 'Solsynth Staff',
|
||||
},
|
||||
'site.migration': {
|
||||
icon: <FlagIcon />,
|
||||
name: 'Solar Network Natives',
|
||||
},
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.textmarker-effect {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.textmarker-effect:before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
content: '';
|
||||
background: var(--mui-palette-primary-main);
|
||||
height: 20px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 0%;
|
||||
opacity: 0.5;
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.textmarker-effect:hover:before {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.textmarker-effect.active:before {
|
||||
animation: textmarker-effect-animation 0.5s ease-in-out;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes textmarker-effect-animation {
|
||||
0% {
|
||||
width: 0;
|
||||
}
|
||||
100% {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.prose img {
|
||||
border-radius: 8px;
|
||||
}
|
BIN
static/favicon.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
17
svelte.config.js
Normal file
@ -0,0 +1,17 @@
|
||||
import adapter from '@sveltejs/adapter-netlify';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
edge: true
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|