Compare commits
No commits in common. "archive/nextjs" and "8a1659aae3fda62807def81adaf79a22734c5760" have entirely different histories.
archive/ne
...
8a1659aae3
55
.gitignore
vendored
@ -1,41 +1,24 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# 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
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
8
.idea/.gitignore
generated
vendored
@ -1,8 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
12
.idea/Capital.iml
generated
@ -1,12 +0,0 @@
|
||||
<?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>
|
61
.idea/codeStyles/Project.xml
generated
@ -1,61 +0,0 @@
|
||||
<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="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
|
||||
<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="WhenMultiline" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
|
||||
<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="WhenMultiline" />
|
||||
<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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Vue">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
5
.idea/codeStyles/codeStyleConfig.xml
generated
@ -1,5 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
10
.idea/material_theme_project_new.xml
generated
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MaterialThemeProjectNewConfig">
|
||||
<option name="metadata">
|
||||
<MTProjectMetadataState>
|
||||
<option name="userId" value="14beee28:194cb09ea37:-7ffd" />
|
||||
</MTProjectMetadataState>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
generated
@ -1,8 +0,0 @@
|
||||
<?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
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"semi": false,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true
|
||||
}
|
13
.roadsignrc
@ -1,13 +0,0 @@
|
||||
{
|
||||
"sync": {
|
||||
"region": "capital",
|
||||
"configPath": "roadsign.toml"
|
||||
},
|
||||
"deployments": [
|
||||
{
|
||||
"region": "capital",
|
||||
"site": "capital-app",
|
||||
"path": ".next"
|
||||
}
|
||||
]
|
||||
}
|
5
.vscode/settings.json
vendored
@ -1,5 +0,0 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"Testflight"
|
||||
]
|
||||
}
|
75
README.md
Normal file
@ -0,0 +1,75 @@
|
||||
# Nuxt 3 Minimal Starter
|
||||
|
||||
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install the dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm run dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm run build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm run preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
8
app.vue
Normal file
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<nuxt-layout>
|
||||
<nuxt-loading-indicator />
|
||||
<nuxt-page />
|
||||
</nuxt-layout>
|
||||
</v-app>
|
||||
</template>
|
BIN
assets/products/feature.jpg
Normal file
After ![]() (image error) Size: 2.0 MiB |
17
assets/products/solar.svg
Executable file
After (image error) Size: 29 KiB |
BIN
bun.lockb
7
components/RouterLink.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<nuxt-link v-bind="props" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ to: string; target: string }>();
|
||||
</script>
|
9
content/posts/hydrogen.md
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
slug: "hy"
|
||||
title: "Projecy Hydrogen"
|
||||
description: "关于我们最新项目,Hydrogen 的详细介绍。"
|
||||
thumbnail: "https://files.solsynth.dev/d/media01/image/202403170109556.jpg"
|
||||
date: "2024-03-16T15:50:16.202Z"
|
||||
---
|
||||
|
||||
<video src="https://files.solsynth.dev/d/media01/video/devlogs/hy-intro.webm" autoplay controls style="width: 100%; height: 360px" />
|
12
content/products/solarpass.md
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
slug: "solarpass"
|
||||
icon: "https://id.solsynth.dev/favicon.svg"
|
||||
name: "Solarpass"
|
||||
code: "Hydrogen.Solarpass"
|
||||
description: "The unified identity service for Solar Network."
|
||||
link: "https://id.solsynth.dev"
|
||||
source: "https://git.solsynth.dev/Hydrogen/Identity"
|
||||
date: "2024-03-16T15:50:16.202Z"
|
||||
---
|
||||
|
||||
I have nothing to say.
|
@ -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
|
34
layouts/default.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<v-app-bar flat scroll-behavior="elevated">
|
||||
<v-container class="mx-auto d-flex align-center justify-center">
|
||||
<v-avatar
|
||||
class="me-4"
|
||||
color="transparent"
|
||||
size="32"
|
||||
image="/favicon.svg"
|
||||
></v-avatar>
|
||||
|
||||
<v-btn v-for="link in navbars" variant="text" :href="link.to">
|
||||
{{ link.label }}
|
||||
</v-btn>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-container>
|
||||
</v-app-bar>
|
||||
|
||||
<v-main class="bg-grey-lighten-3 min-h-[calc(100vh - 64px)]">
|
||||
<suspense>
|
||||
<slot />
|
||||
</suspense>
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import RouterLink from "~/components/RouterLink.vue";
|
||||
|
||||
const navbars = [
|
||||
{ label: "Home", to: "/" },
|
||||
{ label: "Posts", to: "/posts" },
|
||||
{ label: "Products", to: "/products" },
|
||||
];
|
||||
</script>
|
@ -1,7 +0,0 @@
|
||||
/** @type {import('next-i18next').UserConfig} */
|
||||
module.exports = {
|
||||
i18n: {
|
||||
defaultLocale: 'en-US',
|
||||
locales: ['en-US', 'zh-CN'],
|
||||
},
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
import { i18n } from './next-i18next.config'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
i18n,
|
||||
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
|
42
nuxt.config.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import vuetify, { transformAssetUrls } from "vite-plugin-vuetify";
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
devtools: { enabled: true },
|
||||
|
||||
app: {
|
||||
head: {
|
||||
title: "Solsynth",
|
||||
meta: [
|
||||
{
|
||||
name: "description",
|
||||
content:
|
||||
"Solsynth LLC official website. We build amazing, wonderful, open-source software.",
|
||||
},
|
||||
],
|
||||
link: [{ rel: "icon", type: "image/xml+svg", href: "/favicon.svg" }],
|
||||
},
|
||||
},
|
||||
content: {},
|
||||
|
||||
build: {
|
||||
transpile: ["vuetify"],
|
||||
},
|
||||
modules: [
|
||||
"@unocss/nuxt",
|
||||
"@nuxt/content",
|
||||
(_options, nuxt) => {
|
||||
nuxt.hooks.hook("vite:extendConfig", (config) => {
|
||||
// @ts-expect-error
|
||||
config.plugins.push(vuetify({ autoImport: true }));
|
||||
});
|
||||
},
|
||||
],
|
||||
vite: {
|
||||
vue: {
|
||||
template: {
|
||||
transformAssetUrls,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
78
package.json
@ -1,68 +1,26 @@
|
||||
{
|
||||
"name": "capital",
|
||||
"version": "0.1.0",
|
||||
"name": "nuxt-app",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"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"
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"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",
|
||||
"i18next": "^24.2.2",
|
||||
"next": "^15.1.5",
|
||||
"next-i18next": "^15.4.2",
|
||||
"next-nprogress-bar": "^2.4.3",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-i18next": "^15.4.0",
|
||||
"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"
|
||||
"@fontsource/roboto": "^5.0.12",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@nuxt/content": "^2.12.1",
|
||||
"nuxt": "^3.10.3",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.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"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@vercel/speed-insights",
|
||||
"core-js",
|
||||
"esbuild",
|
||||
"sharp"
|
||||
]
|
||||
"@unocss/nuxt": "^0.58.6",
|
||||
"@unocss/reset": "^0.58.6",
|
||||
"vite-plugin-vuetify": "^2.0.3",
|
||||
"vuetify": "^3.5.9"
|
||||
}
|
||||
}
|
||||
|
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
|
||||
}
|
||||
}
|
132
pages/index.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row class="h-fullpage" align-content="center" id="landing">
|
||||
<v-col :xs="12" :sm="6" class="max-md:text-center">
|
||||
<img
|
||||
src="/assets/products/solar.svg"
|
||||
class="w-[180px] h-[192px] max-md:mx-auto"
|
||||
/>
|
||||
<h1 class="text-6xl font-bold mt-8">
|
||||
<span class="text-primary">Internet.</span> <br />
|
||||
<span>Redefined.</span>
|
||||
</h1>
|
||||
|
||||
<p class="text-lg mt-3">This is, the Solar Network.</p>
|
||||
|
||||
<div class="mt-12 w-full flex max-md:justify-center">
|
||||
<v-btn
|
||||
append-icon="mdi-magnify"
|
||||
variant="tonal"
|
||||
size="large"
|
||||
href="#about"
|
||||
>
|
||||
Explore more
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
:xs="12"
|
||||
:sm="6"
|
||||
class="flex items-center max-md:justify-center md:justify-end"
|
||||
>
|
||||
<v-card class="w-full">
|
||||
<img src="/assets/products/feature.jpg" class="object-cover" />
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="h-fullpage" align-content="center" id="about">
|
||||
<v-col :xs="12" :sm="6" class="max-md:text-center">
|
||||
<v-icon
|
||||
icon="mdi-information-slab-symbol"
|
||||
size="48"
|
||||
color="grey-darken-3"
|
||||
class="mb-3 mx-[-16px]"
|
||||
/>
|
||||
<h1 class="text-2xl font-bold">About us</h1>
|
||||
<p>
|
||||
We are a group of developers who are love open-source. Founded in
|
||||
2019. We've been building open-source software that people love ever
|
||||
since. For us, "By open-source, for open-source" is not only a
|
||||
principle, but also the motto of our faith.
|
||||
</p>
|
||||
|
||||
<div class="mt-3 w-full flex max-md:justify-center">
|
||||
<v-btn
|
||||
class="mx-[-16px]"
|
||||
append-icon="mdi-shape"
|
||||
variant="text"
|
||||
href="#products"
|
||||
>
|
||||
Discover products
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
:xs="12"
|
||||
:sm="6"
|
||||
class="flex flex-col gap-2 max-md:items-center md:items-end"
|
||||
>
|
||||
<v-card hover class="pa-5">
|
||||
<template #text>
|
||||
<img src="/favicon.svg" width="128" height="128" />
|
||||
</template>
|
||||
</v-card>
|
||||
<p class="uppercase caption">Crystal Lotus</p>
|
||||
<p class="text-sm opacity-80 mt-[-8px] md:text-right">
|
||||
A crystal flower born in GoatLand. <br />
|
||||
Home flower of Solsynth. <br />
|
||||
Mr. Sheep felt homesick every time he saw it.
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row
|
||||
class="h-fullpage text-center"
|
||||
align-content="center"
|
||||
justify="center"
|
||||
id="products"
|
||||
>
|
||||
<v-col :cols="12">
|
||||
<h2 class="text-2xl font-bold">Products</h2>
|
||||
<p>Let's see what we got.</p>
|
||||
|
||||
<div class="mt-4 flex justify-center gap-2">
|
||||
<v-tooltip v-for="item in products" location="top">
|
||||
<template #activator="{ props }">
|
||||
<v-card v-bind="props" hover class="w-24 h-24" :href="'/products/' + item.slug">
|
||||
<div class="h-full w-full flex justify-center items-center">
|
||||
<img :src="item.icon" width="64" height="64" class="block" />
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<p class="font-bold">{{ item.title }}</p>
|
||||
<p class="font-mono text-xs">{{ item.code }}</p>
|
||||
<p class="mt-2">{{ item.description }}</p>
|
||||
</div>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { data: products } = await useAsyncData("products", () =>
|
||||
queryContent("products").limit(5).find()
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.h-fullpage {
|
||||
height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
.caption {
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
</style>
|
48
pages/posts/[slug].vue
Normal file
@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<div class="max-w-[720px] mx-auto">
|
||||
<v-card>
|
||||
<v-img
|
||||
v-if="post?.thumbnail"
|
||||
cover
|
||||
class="align-end"
|
||||
height="180"
|
||||
:src="post?.thumbnail"
|
||||
/>
|
||||
|
||||
<div class="pa-5">
|
||||
<v-card-text class="pt-0 pb-1">
|
||||
<h2 class="text-xl font-medium">{{ post?.title }}</h2>
|
||||
<p class="opacity-80">{{ post?.description }}</p>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider class="mx-[-20px] my-3 border-opacity-75" />
|
||||
|
||||
<v-card-text>
|
||||
<content-renderer :value="post">
|
||||
<template #empty>
|
||||
<p>No content found.</p>
|
||||
</template>
|
||||
</content-renderer>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider class="mx-[-20px] my-4 border-opacity-75" />
|
||||
|
||||
<div class="mt-3 flex justify-between items-center">
|
||||
<p class="ps-3.5 text-sm">
|
||||
{{ new Date(post?.date).toLocaleString() }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
|
||||
const { data: post } = await useAsyncData("post", () =>
|
||||
queryContent("posts").where({ slug: route.params.slug }).findOne()
|
||||
);
|
||||
</script>
|
34
pages/posts/index.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<div class="max-w-[720px] mx-auto flex flex-col gap-2">
|
||||
<v-card v-for="item in posts">
|
||||
<v-img
|
||||
v-if="item.thumbnail"
|
||||
cover
|
||||
class="align-end"
|
||||
height="180"
|
||||
:src="item.thumbnail"
|
||||
/>
|
||||
<div class="py-5 px-7">
|
||||
<h2 class="text-xl font-medium">{{ item.title }}</h2>
|
||||
<p class="mt-3 opacity-80">{{ item.description }}</p>
|
||||
<div class="mt-3 flex justify-end">
|
||||
<v-btn
|
||||
variant="text"
|
||||
prepend-icon="mdi-page-next"
|
||||
:href="'/posts/' + item.slug"
|
||||
>
|
||||
Read more
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { data: posts } = await useAsyncData("posts", () =>
|
||||
queryContent("posts").find()
|
||||
);
|
||||
</script>
|
61
pages/products/[slug].vue
Normal file
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<div class="max-w-[720px] mx-auto">
|
||||
<v-card class="pa-5">
|
||||
<template #title>
|
||||
<span class="me-2">{{ product?.name }}</span>
|
||||
<span class="font-mono text-xs">{{ product?.code }}</span>
|
||||
</template>
|
||||
<template #subtitle>{{ product?.description }}</template>
|
||||
|
||||
<v-divider class="mx-[-20px] my-3 border-opacity-75" />
|
||||
|
||||
<v-card-text>
|
||||
<content-renderer :value="product">
|
||||
<template #empty>
|
||||
<p>No content found.</p>
|
||||
</template>
|
||||
</content-renderer>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider class="mx-[-20px] my-4 border-opacity-75" />
|
||||
|
||||
<div class="mt-3 flex justify-between items-center">
|
||||
<p class="ps-3.5 text-sm">
|
||||
{{ new Date(product?.date).toLocaleString() }}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<v-btn
|
||||
v-if="product?.source"
|
||||
variant="text"
|
||||
color="info"
|
||||
prepend-icon="mdi-code-tags"
|
||||
target="_blank"
|
||||
:href="product?.source"
|
||||
>
|
||||
Source code
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="teal"
|
||||
prepend-icon="mdi-launch"
|
||||
target="_blank"
|
||||
:href="product?.link"
|
||||
>
|
||||
Launch
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
|
||||
const { data: product } = await useAsyncData("product", () =>
|
||||
queryContent("products").where({ slug: route.params.slug }).findOne()
|
||||
);
|
||||
</script>
|
42
pages/products/index.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<div class="max-w-[720px] mx-auto flex flex-col gap-2">
|
||||
<v-card v-for="item in products">
|
||||
<v-row class="pa-5">
|
||||
<v-col :xs="12" :md="3">
|
||||
<img :src="item.icon" width="128" height="128" class="mx-auto" />
|
||||
</v-col>
|
||||
<v-col :xs="12" :md="9">
|
||||
<h2 class="text-xl font-medium">{{ item.name }}</h2>
|
||||
<p class="font-mono text-sm">{{ item.code }}</p>
|
||||
<p class="mt-3 opacity-80">{{ item.description }}</p>
|
||||
<div class="mt-3 flex justify-end">
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="teal"
|
||||
prepend-icon="mdi-launch"
|
||||
target="_blank"
|
||||
:href="item.link"
|
||||
>
|
||||
Launch
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
prepend-icon="mdi-page-next"
|
||||
:href="'/products/' + item.slug"
|
||||
>
|
||||
Learn more
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { data: products } = await useAsyncData("products", () =>
|
||||
queryContent("products").find()
|
||||
);
|
||||
</script>
|
4
plugins/unocss.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import "@unocss/reset/tailwind.css"
|
||||
import "@fontsource/roboto/latin.css"
|
||||
|
||||
export default defineNuxtPlugin(() => {})
|
36
plugins/vuetify.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import "@mdi/font/css/materialdesignicons.min.css";
|
||||
|
||||
import "vuetify/styles";
|
||||
import { createVuetify } from "vuetify";
|
||||
import { md3 } from "vuetify/blueprints";
|
||||
import * as components from "vuetify/components";
|
||||
import * as labsComponents from "vuetify/labs/components";
|
||||
import * as directives from "vuetify/directives";
|
||||
|
||||
export default defineNuxtPlugin((app) => {
|
||||
const vuetify = createVuetify({
|
||||
directives,
|
||||
components: {
|
||||
...components,
|
||||
...labsComponents,
|
||||
},
|
||||
blueprint: md3,
|
||||
theme: {
|
||||
defaultTheme: "original",
|
||||
themes: {
|
||||
original: {
|
||||
colors: {
|
||||
primary: "#4a5099",
|
||||
secondary: "#2196f3",
|
||||
accent: "#009688",
|
||||
error: "#f44336",
|
||||
warning: "#ff9800",
|
||||
info: "#03a9f4",
|
||||
success: "#4caf50",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
app.vueApp.use(vuetify);
|
||||
});
|
@ -1,8 +0,0 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
Before ![]() (image error) Size: 86 KiB |
Before ![]() (image error) Size: 86 KiB |
12
public/favicon.svg
Executable file
After (image error) Size: 28 KiB |
@ -1,16 +0,0 @@
|
||||
{
|
||||
"actionDownload": "Download",
|
||||
"actionLearnMore": "Learn more",
|
||||
"downloadPlatform": "Platform",
|
||||
"downloadDistribution": "Distribution",
|
||||
"downloadAppleStore": "iOS / macOS (App Store)",
|
||||
"downloadAppleTestflight": "iOS / macOS (TestFlight)",
|
||||
"downloadAndroid": "Android",
|
||||
"downloadWindows": "Windows",
|
||||
"downloadWeb": "Web",
|
||||
"downloadSourceCode": "Source Code",
|
||||
"downloadLinux": "Linux Unpacked",
|
||||
"downloadLinuxDebian": "deb (Debian/Ubuntu)",
|
||||
"actionOpen": "Open",
|
||||
"faq": "Frequently Asked Questions"
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
{
|
||||
"appName": "Solar Network",
|
||||
"appDescription": "The next generation Social Network platform.",
|
||||
"appSlogan": "Social Network, Redefined.",
|
||||
"faq1": "What's the relationship between Solar Network and Solian?",
|
||||
"faq1a": "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.",
|
||||
"faq2": "What's the relationship between Solar Network and HyperNet?",
|
||||
"faq2a": "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.",
|
||||
"faq3": "Which rules do I need to follow while using Solar Network?",
|
||||
"faq3a": "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.",
|
||||
"faq4": "If I have any question about Solar Network, where can I get help?",
|
||||
"faq4a": "Feel free to email as at lily@solsynth.dev",
|
||||
"ftDashboard": "Dashboard",
|
||||
"ftDashboardDescription": "Get what happened recently, all in one place.",
|
||||
"ftExplore": "Exploring",
|
||||
"ftExploreDescription": "Content you love without the ads or algorithms.",
|
||||
"ftChat": "Chat",
|
||||
"ftChatDescription": "Keep in touch with your friends and communities, across the world.",
|
||||
"ftNews": "News",
|
||||
"ftNewsDescription": "Stay up to date with the latest news and events.",
|
||||
"ftStickers": "Stickers",
|
||||
"ftStickersDescription": "Express your feelings better with the various stickers.",
|
||||
"ftPosting": "Posting",
|
||||
"ftPostingDescription": "Share your thoughts and ideas with the world. Without limits and censorship.",
|
||||
"ftPostingDescriptionAddition": "The Solar Network team will not impose any restrictions on the content you post, but according to our User Agreement, we may reduce or limit the public display of content that violates its rules.",
|
||||
"whatsMore": "What's more",
|
||||
"ftOpenSource": "Free, Transparent, Open-source",
|
||||
"ftOpenSourceDescription": "The code powered Solar Network is open-sourced under GPLv3 license, you can check the source code down below in the download section.",
|
||||
"ftSecurity": "Security",
|
||||
"ftSecurityDescription": "Solar Network has done a lot in terms of security. We use multi-factor authentication to protect your account, while being safe and convenient.",
|
||||
"ftNoCollecting": "No data collection",
|
||||
"ftNoCollectingDescription": "Solar Network does not collect any personal information for marketing or other purposes, nor does it sell it to third parties.",
|
||||
"noWaiting": "What are you waiting for?",
|
||||
"noWaitingDescription": "Join Solar Network today, by downloading the app or open it in your browser and create an account."
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
{
|
||||
"actionDownload": "下载",
|
||||
"actionLearnMore": "了解更多",
|
||||
"downloadPlatform": "平台",
|
||||
"downloadDistribution": "分发",
|
||||
"downloadAppleStore": "iOS / macOS (App Store)",
|
||||
"downloadAppleTestflight": "iOS / macOS (TestFlight)",
|
||||
"downloadAndroid": "安卓",
|
||||
"downloadWindows": "Windows",
|
||||
"downloadWeb": "网页版",
|
||||
"downloadSourceCode": "源代码",
|
||||
"downloadLinux": "Linux 未打包",
|
||||
"downloadLinuxDebian": "deb (Debian/Ubuntu)",
|
||||
"actionOpen": "打开",
|
||||
"faq": "常见问题"
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
{
|
||||
"appName": "Solar Network",
|
||||
"appDescription": "下一代社交网络平台",
|
||||
"appSlogan": "重新定义社交网络",
|
||||
"faq1": "Solar Network 和 Solian 之间有什么关系?",
|
||||
"faq1a": "Solian 是为 Solar Network 制作的官方应用程序。而 Solar Network 是由 Solsynth LLC 托管的官方 HyperNet 实例。简单来说,Solian 是应用程序,而 Solar Network 是平台。",
|
||||
"faq2": "Solar Network 和 HyperNet 之间有什么关系?",
|
||||
"faq2a": "HyperNet 是整个项目,包括前端应用程序(公众也称 Solian)和后端服务器。而 Solar Network 是由开发 HyperNet 项目的 Solsynth LLC 托管和管理的官方 HyperNet 实例。",
|
||||
"faq3": "使用 Solar Network 时我需要遵守哪些规则?",
|
||||
"faq3a": "查看我们的条款和条件,详细了解您在 Solar Network 上可以做什么和不能做什么。如果您违反任何这些规则,我们有权暂停或终止您的帐户。您可以在抽屉中看到它们。",
|
||||
"faq4": "如果我对 Solar Network 有任何疑问,我可以在哪里获得帮助?",
|
||||
"faq4a": "你可以发邮件给我们的客户服务获取支持:lily@solsynth.dev",
|
||||
"ftDashboard": "冲浪板",
|
||||
"ftDashboardDescription": "在同一个地方,方便地了解最近发生了什么。",
|
||||
"ftExplore": "探索",
|
||||
"ftExploreDescription": "在没有广告或算法的干扰下欣赏你喜欢的内容。",
|
||||
"ftChat": "聊天",
|
||||
"ftChatDescription": "跨过地区的间隔,与你的朋友和社区进行保持联系。",
|
||||
"ftNews": "新闻",
|
||||
"ftNewsDescription": "不行千里,也能知晓天下事。",
|
||||
"ftStickers": "贴图 / 表情",
|
||||
"ftStickersDescription": "使用各种贴纸更好地表达您的感受。",
|
||||
"ftPosting": "撰写",
|
||||
"ftPostingDescription": "在没有限制和审查的环境,与世界分享你的想法和想法",
|
||||
"ftPostingDescriptionAddition": "Solar Network 团队不会对您发表的内容做任何限制,但是根据我们的《用户协议》,我们可能会对违反其规则的内容进行减少或限制公开展示。",
|
||||
"whatsMore": "还有更多",
|
||||
"ftOpenSource": "自由、透明、开源",
|
||||
"ftOpenSourceDescription": "驱动 Solar Network 的代码在 GPLv3 许可下开源,您可以在下面的下载区域查看源代码。",
|
||||
"ftSecurity": "安全",
|
||||
"ftSecurityDescription": "Solar Network 在安全性方面做了很多,我们采用多因子验证方式来保护你的帐号,同时具备安全和方便。",
|
||||
"ftNoCollecting": "不采集数据",
|
||||
"ftNoCollectingDescription": "Solar Network 不会收集任何个人信息用于营销或者其他目的,更不会出售给第三方。",
|
||||
"noWaiting": "你还在等待什么?",
|
||||
"noWaitingDescription": "通过下载 / 在浏览器中打开 Solian 并创建一个帐号,现在就加入 Solar Network 吧!"
|
||||
}
|
Before ![]() (image error) Size: 74 KiB |
BIN
public/logo.png
Before ![]() (image error) Size: 86 KiB |
Before ![]() (image error) 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
server/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
Before ![]() (image error) Size: 696 KiB |
Before ![]() (image error) Size: 441 KiB |
Before ![]() (image error) Size: 787 KiB |
Before ![]() (image error) Size: 1.4 MiB |
Before ![]() (image error) Size: 770 KiB |
Before ![]() (image error) Size: 749 KiB |
Before ![]() (image error) Size: 461 KiB |
Before ![]() (image error) Size: 118 KiB |
Before ![]() (image error) Size: 2.0 MiB |
@ -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,75 +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'
|
||||
import PinIcon from '@mui/icons-material/Pin'
|
||||
import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive'
|
||||
|
||||
export function SnLoginRouter({
|
||||
ticket,
|
||||
factorList,
|
||||
onNext,
|
||||
}: {
|
||||
ticket: SnAuthTicket
|
||||
factorList: SnAuthFactor[]
|
||||
onNext: (val: SnAuthFactor) => void
|
||||
}) {
|
||||
const factorTypeIcons = [
|
||||
<PasswordIcon key="password-icon" />,
|
||||
<EmailIcon key="email-icon" />,
|
||||
<PinIcon key="pin-icon" />,
|
||||
<NotificationsActiveIcon key="notification-icon" />,
|
||||
]
|
||||
const factorTypeLabels = ['Password', 'Email verification code', 'Time-based OTP', 'In-app 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>
|
||||
)
|
||||
}
|
@ -1,86 +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 { appWithTranslation } from 'next-i18next'
|
||||
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',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default appWithTranslation(App)
|
@ -1,33 +0,0 @@
|
||||
import {
|
||||
AppCacheProvider,
|
||||
DocumentHeadTags,
|
||||
DocumentHeadTagsProps,
|
||||
documentGetInitialProps,
|
||||
} from '@mui/material-nextjs/v15-pagesRouter'
|
||||
import { Html, Head, Main, NextScript, DocumentContext, DocumentProps } from 'next/document'
|
||||
|
||||
export default function Document(props: DocumentProps & DocumentHeadTagsProps) {
|
||||
return (
|
||||
<AppCacheProvider {...props}>
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<script
|
||||
defer
|
||||
src="https://cloud.umami.is/script.js"
|
||||
data-website-id="eef151fb-07e2-461b-8b7f-2547aab735d4"
|
||||
></script>
|
||||
<DocumentHeadTags {...props} />
|
||||
</Head>
|
||||
<body className="antialiased">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</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,99 +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'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
|
||||
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 searchParams = useSearchParams()
|
||||
const userStore = useUserStore()
|
||||
|
||||
function doCallback() {
|
||||
const redirectUrl = searchParams.get('redirect_uri')
|
||||
if (redirectUrl) {
|
||||
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,117 +0,0 @@
|
||||
import { Box, 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 }}>
|
||||
<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,118 +0,0 @@
|
||||
import { Box, Typography, Container, Button, TextField, Collapse, Alert } from '@mui/material'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
import { EventHandler, FormEvent, FormEventHandler, useEffect, useState } from 'react'
|
||||
import { checkAuthenticatedClient, redirectToLogin, sni } from 'solar-js-sdk'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import PriceCheckIcon from '@mui/icons-material/PriceCheck'
|
||||
|
||||
type SnOrder = any
|
||||
|
||||
export const getServerSideProps = (async (context) => {
|
||||
const id = context.params!.id
|
||||
try {
|
||||
const { data: order } = await sni.get<SnOrder>('/cgi/wa/orders/' + id)
|
||||
return { props: { order, title: `Order #${order.id}` } }
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}) satisfies GetServerSideProps<{ order: SnOrder }>
|
||||
|
||||
export default function Post({ order }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
useEffect(() => {
|
||||
if (!checkAuthenticatedClient()) redirectToLogin()
|
||||
}, [])
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [password, setPassword] = useState<string>('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [paid, setPaid] = useState(false)
|
||||
const [canceled, setCanceled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (order?.status === 1) {
|
||||
setPaid(true)
|
||||
} else if (order?.status === 2) {
|
||||
setCanceled(true)
|
||||
}
|
||||
}, [order])
|
||||
|
||||
async function confirmPayment() {
|
||||
try {
|
||||
setBusy(true)
|
||||
await sni.post('/cgi/wa/orders/' + order.id + '/pay', {
|
||||
wallet_password: password,
|
||||
})
|
||||
setPaid(true)
|
||||
} 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
|
||||
component="form"
|
||||
sx={{ width: '100%' }}
|
||||
onSubmit={(evt) => {
|
||||
evt.preventDefault()
|
||||
confirmPayment()
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" component="h1" gutterBottom>
|
||||
Order <code>#{order.id.toString().padStart(8, '0')}</code>
|
||||
</Typography>
|
||||
<Typography variant="body1" component="h2" gutterBottom>
|
||||
{order.remark}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" fontSize={32} pt={2} fontFamily={'monospace'} gutterBottom>
|
||||
{order.amount} SRC
|
||||
</Typography>
|
||||
|
||||
<Collapse in={!!error}>
|
||||
<Alert sx={{ mt: 3, width: '100%' }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<Box sx={{ my: 3, flexDirection: 'column', display: 'flex', gap: 2 }}>
|
||||
{paid || canceled ? (
|
||||
canceled ? (
|
||||
<Typography textAlign="center">Canceled, you are not able to pay this order any more</Typography>
|
||||
) : (
|
||||
<Typography textAlign="center">Paid, you can return to the seller now</Typography>
|
||||
)
|
||||
) : (
|
||||
<TextField
|
||||
label="Wallet Password"
|
||||
variant="outlined"
|
||||
type="password"
|
||||
onInput={(evt) => setPassword((evt.target as HTMLInputElement).value)}
|
||||
/>
|
||||
)}
|
||||
<Button type="submit" variant="contained" startIcon={<PriceCheckIcon />} disabled={busy || paid}>
|
||||
Pay
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="caption" sx={{ opacity: 0.75 }}>
|
||||
Powered by HyperNet.Wallet
|
||||
</Typography>
|
||||
</Box>
|
||||
</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,
|
||||
'',
|
||||
)
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|