Compare commits
No commits in common. "2292513efc48cb7067af7ed7b176de80ba4a9dc8" and "e670c571c7ab0908637809504148be72bba5b9b8" have entirely different histories.
2292513efc
...
e670c571c7
47
.gitignore
vendored
@ -1,24 +1,41 @@
|
||||
# build output
|
||||
dist/
|
||||
|
||||
# generated types
|
||||
.astro/
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# logs
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "es5"
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true
|
||||
}
|
||||
|
4
.vscode/extensions.json
vendored
@ -1,4 +0,0 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
11
.vscode/launch.json
vendored
@ -1,11 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config'
|
||||
|
||||
import tailwind from '@astrojs/tailwind'
|
||||
|
||||
import icon from 'astro-icon'
|
||||
|
||||
import mdx from '@astrojs/mdx'
|
||||
|
||||
import sitemap from '@astrojs/sitemap'
|
||||
|
||||
import vercel from '@astrojs/vercel';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://solsynth.dev',
|
||||
|
||||
integrations: [
|
||||
tailwind(),
|
||||
icon(),
|
||||
mdx(),
|
||||
sitemap({
|
||||
xslURL: '/sitemap.xsl',
|
||||
i18n: { defaultLocale: 'en', locales: { en: 'en-US', 'zh-cn': 'zh-CN' } },
|
||||
}),
|
||||
],
|
||||
|
||||
prefetch: true,
|
||||
|
||||
i18n: {
|
||||
locales: ['en', 'zh-cn'],
|
||||
defaultLocale: 'en',
|
||||
routing: {
|
||||
fallbackType: 'rewrite',
|
||||
prefixDefaultLocale: false,
|
||||
},
|
||||
},
|
||||
|
||||
adapter: vercel(),
|
||||
})
|
16
eslint.config.mjs
Normal file
@ -0,0 +1,16 @@
|
||||
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"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
31
next.config.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
reactStrictMode: true,
|
||||
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
|
60
package.json
@ -1,30 +1,48 @@
|
||||
{
|
||||
"name": "capital",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.0.2",
|
||||
"@astrojs/sitemap": "^3.2.1",
|
||||
"@astrojs/tailwind": "^5.1.3",
|
||||
"@astrojs/vercel": "^8.0.1",
|
||||
"@iconify-json/material-symbols": "^1.2.10",
|
||||
"@iconify-json/simple-icons": "^1.2.16",
|
||||
"astro": "^5.0.5",
|
||||
"astro-icon": "^1.1.4",
|
||||
"astro-seo": "^0.8.4",
|
||||
"marked": "^15.0.4",
|
||||
"sanitize-html": "^2.13.1",
|
||||
"tailwindcss": "^3.4.16"
|
||||
"@emotion/cache": "^11.14.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mui/icons-material": "^6.3.0",
|
||||
"@mui/material": "^6.3.0",
|
||||
"@mui/material-nextjs": "^6.3.1",
|
||||
"@mui/x-charts": "^7.23.2",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.7.9",
|
||||
"axios-case-converter": "^1.1.1",
|
||||
"cookies-next": "^5.0.2",
|
||||
"next": "15.1.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"unified": "^11.0.5",
|
||||
"zustand": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"daisyui": "^4.12.22"
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.3",
|
||||
"@eslint/eslintrc": "^3"
|
||||
}
|
||||
}
|
||||
|
8
postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
BIN
public/logo-w-padding.png
Executable file
After Width: | Height: | Size: 74 KiB |
BIN
public/logo.png
Executable file
After Width: | Height: | Size: 86 KiB |
BIN
public/product-branding.webp
Normal file
After Width: | Height: | Size: 28 KiB |
@ -1,4 +0,0 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://solsynth.dev/sitemap-index.xml
|
@ -1,46 +0,0 @@
|
||||
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
|
||||
<title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
|
||||
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
|
||||
</g>
|
||||
<g id="_Group_" data-name="<Group>">
|
||||
<g id="_Group_2" data-name="<Group>">
|
||||
<g id="_Group_3" data-name="<Group>">
|
||||
<path id="_Path_" data-name="<Path>" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
|
||||
<path id="_Path_2" data-name="<Path>" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
|
||||
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
|
||||
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
|
||||
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
|
||||
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/>
|
||||
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
|
||||
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
|
||||
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="_Group_4" data-name="<Group>">
|
||||
<g>
|
||||
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
|
||||
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
|
||||
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/>
|
||||
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
|
||||
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 4.5 MiB |
0
src/assets/images/products/solar-network-alpha.webp → src/assets/products/solar-network/alpha.webp
Executable file → Normal file
Before Width: | Height: | Size: 696 KiB After Width: | Height: | Size: 696 KiB |
BIN
src/assets/products/solar-network/icon.png
Normal file
After Width: | Height: | Size: 118 KiB |
0
src/assets/images/products/solar-network.webp → src/assets/products/solar-network/labeled.webp
Executable file → Normal file
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
@ -1,29 +0,0 @@
|
||||
---
|
||||
import { getAttachmentUrl } from '../scripts/attachment'
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
|
||||
const { data: attachment } = Astro.props
|
||||
---
|
||||
|
||||
{
|
||||
attachment.mimetype.startsWith('image') ? (
|
||||
<a href={getAttachmentUrl(attachment.rid)} target="_blank">
|
||||
<img
|
||||
src={getAttachmentUrl(attachment.rid)}
|
||||
alt={attachment.alt}
|
||||
class="rounded-lg"
|
||||
/>
|
||||
</a>
|
||||
) : attachment.mimetype.startsWith('video') ? (
|
||||
<video src={getAttachmentUrl(attachment.rid)} controls class="rounded-lg" />
|
||||
) : attachment.mimetype.startsWith('audio') ? (
|
||||
<audio src={getAttachmentUrl(attachment.rid)} controls class="rounded-lg" />
|
||||
) : (
|
||||
<a href={getAttachmentUrl(attachment.rid)} target="_blank">
|
||||
Unable to preview, but you can open it in your broswer.
|
||||
</a>
|
||||
)
|
||||
}
|
90
src/components/CapAppBar.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import { useUserStore } from '@/services/user'
|
||||
import { AppBar, AppBarProps, Avatar, IconButton, Toolbar, Typography, useScrollTrigger, useTheme } from '@mui/material'
|
||||
import { getAttachmentUrl } from '@/services/network'
|
||||
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">
|
||||
<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' }}>
|
||||
<AccountCircle sx={{ color: theme.palette.text.primary }} />
|
||||
</Avatar>
|
||||
)}
|
||||
</IconButton>
|
||||
</Link>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</AppBarScroll>
|
||||
</>
|
||||
)
|
||||
}
|
96
src/components/CapDrawer.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import {
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Divider,
|
||||
Drawer,
|
||||
Toolbar,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { JSX } from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
import FeedIcon from '@mui/icons-material/Feed'
|
||||
import PhotoLibraryIcon from '@mui/icons-material/PhotoLibrary'
|
||||
import PolicyIcon from '@mui/icons-material/Policy'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface NavLink {
|
||||
title: string
|
||||
icon: JSX.Element
|
||||
href: string
|
||||
}
|
||||
|
||||
export function CapDrawer({ width, open, onClose }: { width: number; open: boolean; onClose: () => void }) {
|
||||
const functionLinks: NavLink[] = [
|
||||
{
|
||||
title: 'Posts',
|
||||
icon: <FeedIcon />,
|
||||
href: '/posts',
|
||||
},
|
||||
{
|
||||
title: 'Gallery',
|
||||
icon: <PhotoLibraryIcon />,
|
||||
href: '/attachments',
|
||||
},
|
||||
]
|
||||
|
||||
const additionalLinks: NavLink[] = [
|
||||
{
|
||||
title: 'Terms & Conditions',
|
||||
icon: <PolicyIcon />,
|
||||
href: '/terms',
|
||||
},
|
||||
]
|
||||
|
||||
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' }} />
|
||||
|
||||
<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) => (
|
||||
<Link passHref href={l.href} key={l.href}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton>
|
||||
<ListItemIcon>{l.icon}</ListItemIcon>
|
||||
<ListItemText primary={l.title} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</Link>
|
||||
))}
|
||||
</List>
|
||||
<Divider />
|
||||
<List dense>
|
||||
{additionalLinks.map((l) => (
|
||||
<Link passHref href={l.href} key={l.href}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton>
|
||||
<ListItemIcon>{l.icon}</ListItemIcon>
|
||||
<ListItemText primary={l.title} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</Link>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
36
src/components/attachments/AttachmentItem.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { SnAttachment } from '@/services/attachment'
|
||||
import { getAttachmentUrl } from '@/services/network'
|
||||
import { QuestionMark } from '@mui/icons-material'
|
||||
import { Link, Paper, Typography } from '@mui/material'
|
||||
import { ComponentProps } from 'react'
|
||||
|
||||
export function AttachmentItem({ item, ...rest }: { item: SnAttachment } & ComponentProps<'div'>) {
|
||||
switch (item.mimetype.split('/')[0]) {
|
||||
case 'image':
|
||||
return (
|
||||
<Paper {...rest}>
|
||||
<img src={getAttachmentUrl(item.rid)} alt={item.alt} style={{ objectFit: 'cover', borderRadius: '8px' }} />
|
||||
</Paper>
|
||||
)
|
||||
case 'video':
|
||||
return (
|
||||
<Paper {...rest}>
|
||||
<video src={getAttachmentUrl(item.rid)} controls style={{ 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>
|
||||
)
|
||||
}
|
||||
}
|
83
src/components/auth/SnLoginCheckpoint.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import { SnAuthFactor, SnAuthResult, SnAuthTicket } from '@/services/auth'
|
||||
import { sni } from '@/services/network'
|
||||
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'
|
||||
import { setCookie } from 'cookies-next/client'
|
||||
|
||||
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']
|
||||
setCookie('nex_user_atk', atk, { path: '/', maxAge: 2592000 })
|
||||
setCookie('nex_user_rtk', rtk, { path: '/', maxAge: 2592000 })
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
68
src/components/auth/SnLoginRouter.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
'use client'
|
||||
|
||||
import { SnAuthFactor, SnAuthTicket } from '@/services/auth'
|
||||
import { sni } from '@/services/network'
|
||||
import { Collapse, Alert, Box, Button, Typography, ButtonGroup } from '@mui/material'
|
||||
import { useState } from 'react'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import PasswordIcon from '@mui/icons-material/Password'
|
||||
import EmailIcon from '@mui/icons-material/Email'
|
||||
|
||||
export function SnLoginRouter({
|
||||
ticket,
|
||||
factorList,
|
||||
onNext,
|
||||
}: {
|
||||
ticket: SnAuthTicket
|
||||
factorList: SnAuthFactor[]
|
||||
onNext: (val: SnAuthFactor) => void
|
||||
}) {
|
||||
const factorTypeIcons = [<PasswordIcon />, <EmailIcon />]
|
||||
const factorTypeLabels = ['Password', 'Email verification code']
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
|
||||
async function onSubmit(factor: SnAuthFactor) {
|
||||
try {
|
||||
setLoading(true)
|
||||
await sni.post('/cgi/id/auth/factors/' + factor.id)
|
||||
onNext(factor)
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse in={!!error} sx={{ width: 320 }}>
|
||||
<Alert sx={{ mb: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', width: '100%', gap: 2, textAlign: 'center' }}>
|
||||
<ButtonGroup orientation="vertical" aria-label="Vertical button group">
|
||||
{factorList.map((factor) => (
|
||||
<Button
|
||||
sx={{ py: 1 }}
|
||||
key={factor.id}
|
||||
onClick={() => onSubmit(factor)}
|
||||
disabled={loading || ticket.factorTrail?.includes(factor.id)}
|
||||
startIcon={factorTypeIcons[factor.type]}
|
||||
>
|
||||
{factorTypeLabels[factor.type]}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
|
||||
<Typography variant="caption" sx={{ opacity: 0.75, mx: 2 }}>
|
||||
{ticket.stepRemain} step(s) left
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
74
src/components/auth/SnLoginStart.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { sni } from '@/services/network'
|
||||
import { ArrowForward } from '@mui/icons-material'
|
||||
import { Alert, Box, Button, Collapse, Link, TextField, Typography } from '@mui/material'
|
||||
import { SnAuthFactor, SnAuthResult, SnAuthTicket } from '@/services/auth'
|
||||
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,121 +0,0 @@
|
||||
---
|
||||
import { Image } from 'astro:assets'
|
||||
|
||||
import { version } from '../../package.json'
|
||||
|
||||
import CompanyLogo from '../assets/images/company-logo.png'
|
||||
import { getRelativeLocaleUrl } from 'astro:i18n'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
trailingTitle?: string
|
||||
}
|
||||
|
||||
let { title, trailingTitle } = Astro.props
|
||||
if (!trailingTitle) trailingTitle = 'Solsynth LLC'
|
||||
console.log(title ? `${title} | ${trailingTitle}` : trailingTitle)
|
||||
|
||||
const locale = Astro.currentLocale ?? 'en'
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={Astro.currentLocale ?? 'en'}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<title>{title ? `${title} | ${trailingTitle}` : trailingTitle}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar backdrop-blur fixed top-0 left-0 right-0 z-10">
|
||||
<div class="px-5 flex-1">
|
||||
<div class="flex-none">
|
||||
<a class="btn btn-ghost text-xl flex items-center gap-2" href="/">
|
||||
<Image
|
||||
src={CompanyLogo}
|
||||
alt="company logo"
|
||||
class="h-8 w-8 p-1 bg-white rounded-lg shadow-sm"
|
||||
/>
|
||||
<span>Solsynth</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex justify-end">
|
||||
<ul class="menu menu-horizontal px-1">
|
||||
<li><a href="/posts">Explore</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="mt-[68px]">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<footer class="bg-neutral text-neutral-content p-10 mt-32">
|
||||
<div class="container mx-auto footer">
|
||||
<aside>
|
||||
<Image
|
||||
src={CompanyLogo}
|
||||
alt="company logo"
|
||||
class="h-12 w-12 p-1 bg-white rounded-lg shadow-sm"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold text-lg">Solsynth LLC</span>
|
||||
Building wonderful software since 2019.
|
||||
|
||||
<span class="font-mono text-xs mt-3">Powered by RoadSign v2</span>
|
||||
<span class="font-mono text-xs">Capital v{version}</span>
|
||||
|
||||
<a href="https://status.solsynth.dev" class="mt-4">
|
||||
<img
|
||||
src="https://uptime.betterstack.com/status-badges/v1/monitor/1ki5r.svg"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<nav>
|
||||
<h6 class="footer-title">Services</h6>
|
||||
<a class="link link-hover" href="https://sn.solsynth.dev"> Solian </a>
|
||||
<a class="link link-hover" href="https://files.solsynth.dev">
|
||||
Solarfiles
|
||||
</a>
|
||||
<a class="link link-hover" href="https://git.solsynth.dev">
|
||||
Solargit
|
||||
</a>
|
||||
</nav>
|
||||
<nav>
|
||||
<h6 class="footer-title">Legal</h6>
|
||||
<a
|
||||
class="link link-hover"
|
||||
href={getRelativeLocaleUrl(locale, '/terms/user-agreements')}
|
||||
>
|
||||
User Agreements
|
||||
</a>
|
||||
<a
|
||||
class="link link-hover"
|
||||
href={getRelativeLocaleUrl(locale, '/terms/privacy-policy')}
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
<a
|
||||
class="link link-hover"
|
||||
href={getRelativeLocaleUrl(locale, '/terms')}
|
||||
>
|
||||
All Terms & Conditions
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
68
src/pages/_app.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
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 { useUserStore } from '@/services/user'
|
||||
import { useEffect } from 'react'
|
||||
import Head from 'next/head'
|
||||
|
||||
const fontRoboto = Roboto({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '500', '700'],
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
const siteTheme = createTheme({
|
||||
cssVariables: true,
|
||||
// colorSchemes: {
|
||||
// dark: true,
|
||||
// },
|
||||
palette: {
|
||||
mode: 'light',
|
||||
primary: {
|
||||
main: '#3949ab',
|
||||
},
|
||||
secondary: {
|
||||
main: '#1e88e5',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const userStore = useUserStore()
|
||||
|
||||
useEffect(() => {
|
||||
userStore.fetchUser()
|
||||
}, [])
|
||||
|
||||
const title = pageProps.title ? `${pageProps.title} | 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>
|
||||
|
||||
<ThemeProvider theme={siteTheme}>
|
||||
<CssBaseline />
|
||||
|
||||
<CapAppBar />
|
||||
<Box sx={{ minHeight: 'calc(100vh - 64px)' }}>
|
||||
<Component {...pageProps} />
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
)
|
||||
}
|
28
src/pages/_document.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
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>
|
||||
<DocumentHeadTags {...props} />
|
||||
</Head>
|
||||
<body className="antialiased">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
</AppCacheProvider>
|
||||
)
|
||||
}
|
||||
|
||||
Document.getInitialProps = async (ctx: DocumentContext) => {
|
||||
const finalProps = await documentGetInitialProps(ctx)
|
||||
return finalProps
|
||||
}
|
10
src/pages/api/well-known/jwks.ts
Normal file
@ -0,0 +1,10 @@
|
||||
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)
|
||||
}
|
23
src/pages/api/well-known/openid-configuration.ts
Normal file
@ -0,0 +1,23 @@
|
||||
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 (let [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)
|
||||
}
|
125
src/pages/auth/authorize.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import { sni } from '@/services/network'
|
||||
import { Container, Box, Typography, Alert, Collapse, Button, CircularProgress, Card, CardContent } from '@mui/material'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import CheckIcon from '@mui/icons-material/Check'
|
||||
import { SnAuthTicket } from '@/services/auth'
|
||||
|
||||
export default function AccountAuthorize() {
|
||||
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>
|
||||
)
|
||||
}
|
103
src/pages/auth/login.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import { SnLoginCheckpoint } from '@/components/auth/SnLoginCheckpoint'
|
||||
import { SnLoginRouter } from '@/components/auth/SnLoginRouter'
|
||||
import { SnLoginStart } from '@/components/auth/SnLoginStart'
|
||||
import { SnAuthFactor, SnAuthTicket } from '@/services/auth'
|
||||
import { useUserStore } from '@/services/user'
|
||||
import { Box, Container, Typography } from '@mui/material'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function Login() {
|
||||
const [period, setPeriod] = useState<number>(0)
|
||||
const [ticket, setTicket] = useState<SnAuthTicket | null>(null)
|
||||
const [factorList, setFactorList] = useState<SnAuthFactor[]>([])
|
||||
const [factor, setFactor] = useState<SnAuthFactor | null>(null)
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
function doCallback() {
|
||||
if (router.query['redirect_url']) {
|
||||
let redirectUrl: string
|
||||
if (Array.isArray(router.query['redirect_url'])) {
|
||||
redirectUrl = router.query['redirect_url'][0]
|
||||
} else {
|
||||
redirectUrl = router.query['redirect_url'].toString()
|
||||
}
|
||||
|
||||
if (redirectUrl.startsWith('/')) {
|
||||
router.push(redirectUrl)
|
||||
} else {
|
||||
window.open(redirectUrl, '_self')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
router.push('/users/me')
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
switch (period) {
|
||||
case 1:
|
||||
return (
|
||||
<SnLoginRouter
|
||||
ticket={ticket!}
|
||||
factorList={factorList}
|
||||
onNext={(val) => {
|
||||
setPeriod(period + 1)
|
||||
setFactor(val)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
case 2:
|
||||
return (
|
||||
<SnLoginCheckpoint
|
||||
ticket={ticket!}
|
||||
factor={factor!}
|
||||
onNext={(val, done) => {
|
||||
if (!done) {
|
||||
setTicket(val)
|
||||
setPeriod(1)
|
||||
return
|
||||
}
|
||||
userStore.fetchUser()
|
||||
doCallback()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<SnLoginStart
|
||||
onNext={(val, fcs) => {
|
||||
setPeriod(period + 1)
|
||||
setTicket(val)
|
||||
setFactorList(fcs)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
height: 'calc(100vh - 64px)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Typography variant="h5" component="h1">
|
||||
Login
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" component="h2">
|
||||
Login via Solarpass
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mt: 3 }}>{renderForm()}</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
71
src/pages/flow/accounts/confirm.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { sni } from '@/services/network'
|
||||
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'
|
||||
|
||||
export default function AccountConfirm() {
|
||||
const router = useRouter()
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
async function confirm() {
|
||||
try {
|
||||
await sni.post('/cgi/id/users/me/confirm', {
|
||||
code: router.query['code'] as string,
|
||||
})
|
||||
router.push('/')
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
confirm()
|
||||
}, [])
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
69
src/pages/flow/accounts/deletion.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { sni } from '@/services/network'
|
||||
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'
|
||||
|
||||
export default function AccountDeletion() {
|
||||
const router = useRouter()
|
||||
|
||||
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: router.query['code'] as string,
|
||||
})
|
||||
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>
|
||||
)
|
||||
}
|
77
src/pages/flow/accounts/password-reset.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { sni } from '@/services/network'
|
||||
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'
|
||||
|
||||
export type SnResetPasswordForm = {
|
||||
password: string
|
||||
}
|
||||
|
||||
export default function AccountPasswordReset() {
|
||||
const router = useRouter()
|
||||
|
||||
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: router.query['code'] as string,
|
||||
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,110 +0,0 @@
|
||||
---
|
||||
import { Image } from 'astro:assets'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
|
||||
import CompanyAsideImage from '@/assets/images/company-aside.webp'
|
||||
import AppStoreGetImage from '@/assets/images/app-store/get-it-on-black.svg'
|
||||
import ProductSnPreviewImage from '@/assets/images/products/solar-network.webp'
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<div class="section container mx-auto grid-cols-1 lg:grid-cols-3" id="intro">
|
||||
<div
|
||||
class="sub-section flex flex-col items-end justify-center lg:col-span-1"
|
||||
>
|
||||
<h1 class="text-4xl font-bold text-right">Solsynth LLC</h1>
|
||||
<p class="text-lg text-right">
|
||||
A vibrant creating wonderful software and hope the future will be
|
||||
brighter.
|
||||
</p>
|
||||
<a class="link flex items-center gap-1 mt-2" href="#work-featured">
|
||||
<span>See some of our works</span>
|
||||
<Icon name="material-symbols:arrow-downward" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="sub-section flex items-center lg:col-span-2">
|
||||
<Image
|
||||
src={CompanyAsideImage}
|
||||
alt="galaxy"
|
||||
class="rounded-lg shadow-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-flex py-12 shadow-lg bg-base-300" id="work-featured">
|
||||
<div class="section container mx-auto grid-cols-1 lg:grid-cols-3">
|
||||
<div
|
||||
class="sub-section flex flex-col items-start lg:items-end justify-center lg:col-span-1"
|
||||
>
|
||||
<div class="badge badge-primary flex gap-1 items-center mb-2">
|
||||
<Icon name="material-symbols:star" />
|
||||
<span>Featured Project</span>
|
||||
</div>
|
||||
|
||||
<h2 class="text-3xl font-bold lg:text-right">Solar Network</h2>
|
||||
<p class="text-lg lg:text-right">
|
||||
The next generation social network. But not only for social media.
|
||||
</p>
|
||||
<blockquote
|
||||
class="text-md lg:text-right bg-neutral text-neutral-content px-7 py-5 rounded-xl font-mono mt-2"
|
||||
>
|
||||
Social Network, Redefined.
|
||||
</blockquote>
|
||||
|
||||
<div class="flex gap-3 mt-4 items-center text-right">
|
||||
<span
|
||||
class="opacity-75 text-xs max-lg:order-last"
|
||||
style="line-height: 1"
|
||||
>
|
||||
Also supports Android, Windows, and any modern web browser
|
||||
</span>
|
||||
<a
|
||||
href="https://apps.apple.com/us/app/solian/id6499032345?itscg=30200&itsct=apps_box_link&mttnsubad=6499032345"
|
||||
>
|
||||
<Image src={AppStoreGetImage} alt="get it on app store" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class="link flex items-center gap-1 mt-3"
|
||||
href="/products/solar-network"
|
||||
>
|
||||
<span>Learn more about Solar Network</span>
|
||||
<Icon name="material-symbols:arrow-right-alt" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="sub-section flex items-center lg:col-span-2">
|
||||
<Image
|
||||
src={ProductSnPreviewImage}
|
||||
alt="solar network cross-platform preview"
|
||||
class="rounded-lg shadow-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.section {
|
||||
min-height: 90vh;
|
||||
width: 100%;
|
||||
gap: 0 2rem;
|
||||
padding: 0 1rem;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.section-flex {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sub-section {
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
116
src/pages/index.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
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" />
|
||||
<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' }}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
228
src/pages/posts/[...id].tsx
Normal file
@ -0,0 +1,228 @@
|
||||
import { getAttachmentUrl, sni } from '@/services/network'
|
||||
import { SnPost } from '@/services/post'
|
||||
import { listAttachment, SnAttachment } from '@/services/attachment'
|
||||
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 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)
|
||||
}
|
||||
const out = await processor.use(remarkRehype).use(rehypeSanitize).use(rehypeStringify).process(post.body.content)
|
||||
post.body.rawContent = post.body.content
|
||||
post.body.content = String(out)
|
||||
}
|
||||
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 [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">
|
||||
{post.body.content && <div dangerouslySetInnerHTML={{ __html: post.body.content }} />}
|
||||
</Box>
|
||||
|
||||
{attachments && (
|
||||
<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),
|
||||
}}
|
||||
>
|
||||
{attachments.map((a) => (
|
||||
<Grid size={1} key={a.id}>
|
||||
<AttachmentItem item={a} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
---
|
||||
export const prerender = false
|
||||
|
||||
import sanitizeHtml from 'sanitize-html'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { SEO } from 'astro-seo'
|
||||
import { marked } from 'marked'
|
||||
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
import AttachmentRenderer from '@/components/AttachmentRenderer.astro'
|
||||
import { getAttachmentUrl, fetchAttachmentMeta } from '@/scripts/attachment'
|
||||
|
||||
const { slug } = Astro.params
|
||||
|
||||
const baseUrl = import.meta.env.PUBLIC_SOLAR_NETWORK_URL
|
||||
const resp = await fetch(`${baseUrl}/cgi/co/posts/${slug?.replace('/', ':')}`)
|
||||
|
||||
if (resp.status !== 200) {
|
||||
return new Response(null, { status: 404 })
|
||||
}
|
||||
|
||||
const data = await resp.json()
|
||||
|
||||
const rawContent = await marked(data.body.content as string, {
|
||||
breaks: data.type == 'story',
|
||||
})
|
||||
const content = sanitizeHtml(rawContent)
|
||||
|
||||
const attachments = await fetchAttachmentMeta(data.body.attachments)
|
||||
|
||||
const title = data.body?.title ? data.body.title : `Post #${data.id}`
|
||||
const description =
|
||||
data.body?.description ?? data.body?.content?.substring(0, 200) + '...'
|
||||
|
||||
const url =
|
||||
data.alias && data.alias_prefix
|
||||
? `https://solsynth.dev/posts/${data.alias_prefix}/${data.alias}`
|
||||
: `https://solsynth.dev/posts/${data.id}`
|
||||
---
|
||||
|
||||
<head>
|
||||
<SEO
|
||||
title={`${title} | Solar Network`}
|
||||
description={description}
|
||||
canonical={url}
|
||||
charset='UTF-8'
|
||||
openGraph={{
|
||||
optional: {
|
||||
siteName: 'Solar Network',
|
||||
description,
|
||||
video: getAttachmentUrl(
|
||||
attachments.find((a) => a.mimetype.startsWith('video'))?.rid
|
||||
),
|
||||
audio: getAttachmentUrl(
|
||||
attachments.find((a) => a.mimetype.startsWith('audio'))?.rid
|
||||
),
|
||||
},
|
||||
basic: {
|
||||
title,
|
||||
type: 'article',
|
||||
image: data.body?.thumbnail
|
||||
? getAttachmentUrl(data.body.thumbnail)
|
||||
: getAttachmentUrl(
|
||||
attachments.find((a) => a.mimetype.startsWith('image'))?.rid
|
||||
),
|
||||
url,
|
||||
},
|
||||
article: {
|
||||
publishedTime: new Date(data.created_at).toISOString(),
|
||||
modifiedTime: new Date(data.updated_at).toISOString(),
|
||||
authors: ['@' + data.publisher.name],
|
||||
},
|
||||
}}
|
||||
twitter={{
|
||||
card: 'summary_large_image',
|
||||
title,
|
||||
description,
|
||||
creator: '@' + data.publisher.name,
|
||||
image: data.body?.thumbnail
|
||||
? getAttachmentUrl(data.body.thumbnail)
|
||||
: getAttachmentUrl(
|
||||
attachments.find((a) => a.mimetype.startsWith('image'))?.rid
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
|
||||
<Layout title={title} trailingTitle="Solar Network">
|
||||
<div role="alert" class="alert shadow-lg px-12 m-0 rounded-none mb-5">
|
||||
<Icon
|
||||
name="material-symbols:ungroup"
|
||||
class="stroke-info fill-info h-6 w-6 shrink-0"
|
||||
/>
|
||||
<div>
|
||||
<h3 class="font-bold">Open in the Solian</h3>
|
||||
<div class="text-xs">
|
||||
The most modern, user-friendly, and official Solar Network app.
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a class="btn btn-sm" href="/products/solar-network#downloads">Get</a>
|
||||
<a class="btn btn-sm" href={`https://sn.solsynth.dev/posts/${data.id}`}
|
||||
>Open</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container lg:max-w-[75ch] px-8 mx-auto">
|
||||
<div class="flex gap-4 items-center mb-5">
|
||||
<div class="avatar">
|
||||
<div class="w-12 rounded-full">
|
||||
<img src={getAttachmentUrl(data.publisher.avatar)} alt="avatar" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="userinfo flex flex-col">
|
||||
<span class="flex gap-2 items-baseline">
|
||||
<span class="text-md font-bold">{data.publisher.nick}</span>
|
||||
<span class="text-xs font-mono">@{data.publisher.name}</span>
|
||||
</span>
|
||||
<span class="text-sm line-clamp-2 overflow-ellipsis"
|
||||
>{data.publisher.description}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
data.repost_id && (
|
||||
<div role="alert" class="alert mb-5 py-2 mx-[-4px]">
|
||||
<Icon
|
||||
name="material-symbols:format-quote"
|
||||
class="stroke-info fill-info h-6 w-6 shrink-0"
|
||||
/>
|
||||
<span>
|
||||
This post is reposting post{' '}
|
||||
<span class="font-mono">#{data.repost_id}</span>
|
||||
</span>
|
||||
<div>
|
||||
<a class="btn btn-sm" href={`/posts/${data.repost_id}`}>
|
||||
See reposted post
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<article>
|
||||
<div class="prose max-w-none max-md:prose-lg min-w-0" set:html={content} />
|
||||
|
||||
{
|
||||
attachments && (
|
||||
<div
|
||||
class="attachment-list mt-5 gap-4 grid grid-cols-1"
|
||||
class:list={
|
||||
attachments.length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'
|
||||
}
|
||||
>
|
||||
{attachments.map((attachment) => (
|
||||
<div class="attachment">
|
||||
<AttachmentRenderer data={attachment} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</article>
|
||||
</div>
|
||||
</Layout>
|
@ -1,149 +0,0 @@
|
||||
---
|
||||
export const prerender = false
|
||||
|
||||
import sanitizeHtml from 'sanitize-html'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { marked } from 'marked'
|
||||
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
import AttachmentRenderer from '@/components/AttachmentRenderer.astro'
|
||||
import { getAttachmentUrl, fetchAttachmentMeta } from '@/scripts/attachment'
|
||||
|
||||
const page = parseInt(Astro.url.searchParams.get('page') ?? '1') || 1
|
||||
|
||||
async function getPosts() {
|
||||
const baseUrl = import.meta.env.PUBLIC_SOLAR_NETWORK_URL
|
||||
const res = await fetch(
|
||||
`${baseUrl}/cgi/co/posts?take=10&offset=${Math.max(page - 1, 0) * 10}`
|
||||
)
|
||||
const data = await res.json()
|
||||
const posts = await Promise.all(
|
||||
data['data'].map(async (ele: any) => {
|
||||
if (ele.body?.content) {
|
||||
ele.body.content = await parseContent(
|
||||
ele.body.content,
|
||||
ele.type == 'story'
|
||||
)
|
||||
}
|
||||
if (ele.body?.attachments) {
|
||||
ele.body.attachments = await fetchAttachmentMeta(ele.body.attachments)
|
||||
}
|
||||
return ele
|
||||
})
|
||||
)
|
||||
|
||||
return posts
|
||||
}
|
||||
|
||||
const posts = await getPosts()
|
||||
|
||||
async function parseContent(data: string, useBreaks: boolean = false) {
|
||||
const rawContent = await marked(data, {
|
||||
breaks: useBreaks,
|
||||
})
|
||||
return sanitizeHtml(rawContent)
|
||||
}
|
||||
---
|
||||
|
||||
<Layout title="Posts" trailingTitle='Solar Network'>
|
||||
<div role="alert" class="alert shadow-lg px-12 m-0 rounded-none mb-5">
|
||||
<Icon
|
||||
name="material-symbols:ungroup"
|
||||
class="stroke-info fill-info h-6 w-6 shrink-0"
|
||||
/>
|
||||
<div>
|
||||
<h3 class="font-bold">Open in the Solian</h3>
|
||||
<div class="text-xs">
|
||||
The most modern, user-friendly, and official Solar Network app.
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a class="btn btn-sm" href="/products/solar-network#downloads">Get</a>
|
||||
<a class="btn btn-sm" href="https://sn.solsynth.dev/posts">Open</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="container max-w-[85ch] mx-auto mt-[15vh] px-12 flex flex-col gap-4"
|
||||
>
|
||||
<h1 class="text-2xl font-bold">Posts</h1>
|
||||
<p>Explore the posts all over the Solar Network.</p>
|
||||
|
||||
<div class="flex flex-col gap-4 mt-4 mx-[-16px]">
|
||||
{
|
||||
posts.map((ele: any) => (
|
||||
<a href={`/posts/${ele.id}`}>
|
||||
<div class="card bg-base-100 w-full border-neutral border">
|
||||
<div class="card-body">
|
||||
<div class="flex gap-4 items-center mb-5">
|
||||
<div class="avatar">
|
||||
<div class="w-12 rounded-full">
|
||||
<img
|
||||
src={getAttachmentUrl(ele.publisher.avatar)}
|
||||
alt="avatar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="userinfo flex flex-col">
|
||||
<span class="flex gap-2 items-baseline">
|
||||
<span class="text-md font-bold">
|
||||
{ele.publisher.nick}
|
||||
</span>
|
||||
<span class="text-xs font-mono">
|
||||
@{ele.publisher.name}
|
||||
</span>
|
||||
</span>
|
||||
<span class="text-sm overflow-ellipsis">
|
||||
{new Date(ele.created_at).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ele.body.title && (
|
||||
<h2 class="card-title">
|
||||
{ele.body?.title ?? `Post #${ele.id}`}
|
||||
</h2>
|
||||
)}
|
||||
<article>
|
||||
<div
|
||||
class="prose max-w-none max-md:prose-lg max-w-0"
|
||||
set:html={ele.body?.content ?? ''}
|
||||
/>
|
||||
|
||||
{ele.body?.attachments && (
|
||||
<div
|
||||
class="attachment-list mt-5 gap-4 grid grid-cols-1"
|
||||
class:list={
|
||||
ele.body.attachments.length >= 2
|
||||
? 'md:grid-cols-2'
|
||||
: 'md:grid-cols-1'
|
||||
}
|
||||
>
|
||||
{ele.body.attachments.map((attachment: any) => (
|
||||
<div class="attachment">
|
||||
<AttachmentRenderer data={attachment} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
|
||||
<div class="flex justify-center items-center mt-12">
|
||||
<div class="join">
|
||||
<a
|
||||
class="join-item btn"
|
||||
class:list={page == 1 ? 'btn-disabled' : ''}
|
||||
href={`/posts?page=${page - 1}`}>«</a
|
||||
>
|
||||
<button class="join-item btn">Page {page}</button>
|
||||
<a class="join-item btn" href={`/posts?page=${page + 1}`}>»</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
135
src/pages/posts/index.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import { AttachmentItem } from '@/components/attachments/AttachmentItem'
|
||||
import { SnAttachment, listAttachment } from '@/services/attachment'
|
||||
import { getAttachmentUrl, sni } from '@/services/network'
|
||||
import { SnPost } from '@/services/post'
|
||||
import { Avatar, Box, Container, Divider, Grid2 as Grid, Pagination, Paper, Typography } from '@mui/material'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import rehypeSanitize from 'rehype-sanitize'
|
||||
import rehypeStringify from 'rehype-stringify'
|
||||
import remarkBreaks from 'remark-breaks'
|
||||
import remarkParse from 'remark-parse'
|
||||
import remarkRehype from 'remark-rehype'
|
||||
import { unified } from 'unified'
|
||||
|
||||
type SnPostWithAttachments = SnPost & { attachments: SnAttachment[] }
|
||||
|
||||
export const getServerSideProps = (async (context) => {
|
||||
let page: number = parseInt(context.query.page as string)
|
||||
if (isNaN(page)) page = 1
|
||||
|
||||
const countPerPage = 10
|
||||
|
||||
try {
|
||||
const { data: resp } = await sni.get<{ data: SnPost[]; count: number }>('/cgi/co/posts', {
|
||||
params: {
|
||||
take: countPerPage,
|
||||
offset: (page - 1) * countPerPage,
|
||||
},
|
||||
})
|
||||
|
||||
let posts: SnPostWithAttachments[] = resp.data as SnPostWithAttachments[]
|
||||
for (let idx = 0; idx < posts.length; idx++) {
|
||||
let post = posts[idx]
|
||||
if (post.body.content) {
|
||||
let processor: any = unified().use(remarkParse)
|
||||
if (post.type != 'article') {
|
||||
processor = processor.use(remarkBreaks)
|
||||
}
|
||||
const out = await processor
|
||||
.use(remarkRehype)
|
||||
.use(rehypeSanitize)
|
||||
.use(rehypeStringify)
|
||||
.process(post.body.content)
|
||||
post.body.rawContent = post.body.content
|
||||
post.body.content = String(out)
|
||||
}
|
||||
if (post.body.attachments) {
|
||||
post.attachments = await listAttachment(post.body.attachments)
|
||||
}
|
||||
posts[idx] = post
|
||||
}
|
||||
|
||||
return { props: { posts, page, pages: Math.ceil(resp.count / countPerPage) } }
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}) satisfies GetServerSideProps<{ posts: SnPostWithAttachments[]; page: number; pages: number }>
|
||||
|
||||
export default function PostList({ posts, page, pages }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<Container sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 2 }} maxWidth="md">
|
||||
{posts.map((p) => (
|
||||
<Paper key={p.id} sx={{ px: 2, py: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Avatar src={getAttachmentUrl(p.publisher.avatar)} />
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography fontWeight="bold">{p.publisher.nick}</Typography>
|
||||
<Typography fontFamily="monospace" fontSize={13} lineHeight={1.2}>
|
||||
@{p.publisher.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Link href={`/posts/${p.id}`} passHref>
|
||||
<Box>
|
||||
<Box sx={{ mt: 1.5, mb: 1 }} display="flex" flexDirection="column" gap={0.5}>
|
||||
{(p.body.title || p.body.content) && (
|
||||
<Box>
|
||||
{p.body.title && <Typography variant="h6">{p.body.title}</Typography>}
|
||||
{p.body.description && <Typography variant="subtitle1">{p.body.description}</Typography>}
|
||||
</Box>
|
||||
)}
|
||||
<Box display="flex" gap={2} sx={{ opacity: 0.8 }}>
|
||||
<Typography variant="body2">
|
||||
Published at {new Date(p.publishedAt ?? p.createdAt).toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box sx={{ maxWidth: 'unset' }} className="prose prose-md">
|
||||
{p.body.content && <div dangerouslySetInnerHTML={{ __html: p.body.content }} />}
|
||||
</Box>
|
||||
</Box>
|
||||
</Link>
|
||||
|
||||
{p.attachments && (
|
||||
<Grid
|
||||
container
|
||||
spacing={2}
|
||||
columns={{
|
||||
xs: 1,
|
||||
sm: Math.min(2, p.attachments.length),
|
||||
md: Math.min(3, p.attachments.length),
|
||||
lg: Math.min(4, p.attachments.length),
|
||||
}}
|
||||
>
|
||||
{p.attachments.map((a) => (
|
||||
<Grid size={1} key={a.id}>
|
||||
<AttachmentItem item={a} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Paper>
|
||||
))}
|
||||
|
||||
<Pagination
|
||||
count={pages}
|
||||
page={page}
|
||||
sx={{ mx: 'auto', mb: 5, mt: 3 }}
|
||||
onChange={(_, page) => router.push('/posts?page=' + page)}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
---
|
||||
import { Image } from 'astro:assets'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
|
||||
import ProductSnPreviewImage from '../../assets/images/products/solar-network-alpha.webp'
|
||||
---
|
||||
|
||||
<Layout title="Solar Network">
|
||||
<div class="container mx-auto mt-[35vh]" id="intro">
|
||||
<div class="text-center">
|
||||
<h1 class="text-4xl font-bold">Solar Network</h1>
|
||||
<p>The next generation Social Network platform.</p>
|
||||
|
||||
<div class="flex justify-center gap-2 mt-5">
|
||||
<a class="btn btn-primary" href="#downloads">
|
||||
Download <Icon name="material-symbols:download" size={20} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Image
|
||||
src={ProductSnPreviewImage}
|
||||
alt="solar network cross-platform preview"
|
||||
class="mt-5"
|
||||
/>
|
||||
|
||||
<div id="downloads" class="mt-24 text-center">
|
||||
<div class="flex flex-col justify-center">
|
||||
<h2 class="text-2xl font-bold">Downloads</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap mx-8 items-center justify-center gap-4 my-5">
|
||||
<a
|
||||
href="https://apps.apple.com/us/app/solian/id6499032345?itscg=30200&itsct=apps_box_link&mttnsubad=6499032345"
|
||||
class="btn btn-secondary max-md:btn-wide"
|
||||
>
|
||||
<Icon name="simple-icons:appstore" />
|
||||
iOS / macOS (App Store)
|
||||
</a>
|
||||
<a
|
||||
href="https://testflight.apple.com/join/YJ0lmN6O"
|
||||
class="btn btn-secondary btn-outline max-md:btn-wide"
|
||||
>
|
||||
<Icon name="simple-icons:apple" />
|
||||
iOS / macOS (TestFlight)
|
||||
</a>
|
||||
<a
|
||||
href="https://files.solsynth.dev/production01/solian/app-arm64-v8a-release.apk"
|
||||
class="btn btn-secondary max-md:btn-wide"
|
||||
>
|
||||
<Icon name="simple-icons:android" />
|
||||
Android (apk file)
|
||||
</a>
|
||||
<a
|
||||
href="https://files.solsynth.dev/production01/solian/windows-x86_64-release.zip"
|
||||
class="btn btn-secondary max-md:btn-wide"
|
||||
>
|
||||
<Icon name="simple-icons:windows" />
|
||||
Windows (executable)
|
||||
</a>
|
||||
<a
|
||||
href="https://sn.solsynth.dev"
|
||||
class="btn btn-secondary btn-outline max-md:btn-wide"
|
||||
>
|
||||
<Icon name="material-symbols:globe" />
|
||||
Web (browser)
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mx-8">
|
||||
<p class="opacity-80 mb-2">
|
||||
You will download Solian, which is the official app made for Solar
|
||||
Network.
|
||||
</p>
|
||||
<p class="opacity-75 text-sm">
|
||||
Explore more platform distribution files on the{' '}
|
||||
<a class="link" href="https://files.solsynth.dev/production01/solian"
|
||||
>Solarfiles</a
|
||||
>
|
||||
</p>
|
||||
<p class="opacity-75 text-sm">
|
||||
Check out the Solian source code on{' '}
|
||||
<a class="link" href="https://git.solsynth.dev/HyperNet/Surface"
|
||||
>Solargit</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="faq" class="mt-24">
|
||||
<div class="flex text-center justify-center">
|
||||
<h2 class="text-2xl font-bold">Frequently Asked Questions</h2>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="max-w-[85ch] mx-auto bg-neutral text-neutral-content shadow-lg rounded-lg p-2 mt-5"
|
||||
>
|
||||
<div class="join join-vertical w-full">
|
||||
<div class="collapse collapse-arrow join-item">
|
||||
<input type="radio" name="my-accordion-4" checked="checked" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
What's the relationship between Solar Network and Solian?
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<p>
|
||||
Solian is the official app made for Solar Network. And the Solar
|
||||
Network is the official HyperNet instance hosted by Solsynth
|
||||
LLC.
|
||||
</p>
|
||||
<p>
|
||||
For simple, Solian is the app, and the Solar Network is the
|
||||
platform.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse collapse-arrow join-item">
|
||||
<input type="radio" name="my-accordion-4" checked="checked" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
What's the relationship between Solar Network and HyperNet?
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse collapse-arrow join-item">
|
||||
<input type="radio" name="my-accordion-4" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Which rules do I need to follow while using Solar Network?
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<p>
|
||||
Check out our{' '}
|
||||
<a href="/terms/user-agreements" class="link">
|
||||
User Agreements
|
||||
</a>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse collapse-arrow join-item">
|
||||
<input type="radio" name="my-accordion-4" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
If I have any question about Solar Network, where can I get help?
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<p>
|
||||
Feel free to email us at{' '}
|
||||
<a href="mailto:lily@solsynth.dev" class="link">
|
||||
<address>lily@solsynth.dev</address>
|
||||
</a>
|
||||
Our customer service team will try our best to help you solve your
|
||||
issue.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
</style>
|
230
src/pages/products/solar-network.tsx
Normal file
@ -0,0 +1,230 @@
|
||||
import {
|
||||
Link,
|
||||
Container,
|
||||
Box,
|
||||
Typography,
|
||||
Chip,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
} from '@mui/material'
|
||||
import { JSX } from 'react'
|
||||
import { Roboto_Serif } from 'next/font/google'
|
||||
import Image from 'next/image'
|
||||
import NextLink from 'next/link'
|
||||
|
||||
import ArrowDownward from '@mui/icons-material/ArrowDownward'
|
||||
import DownloadIcon from '@mui/icons-material/Download'
|
||||
import LaunchIcon from '@mui/icons-material/Launch'
|
||||
import AppleIcon from '@mui/icons-material/Apple'
|
||||
import AndroidIcon from '@mui/icons-material/Android'
|
||||
import WindowIcon from '@mui/icons-material/Window'
|
||||
import WebIcon from '@mui/icons-material/Public'
|
||||
import CodeIcon from '@mui/icons-material/Code'
|
||||
|
||||
import ImgSolarNetworkIcon from '@/assets/products/solar-network/icon.png'
|
||||
import ImgSolarNetworkAlpha from '@/assets/products/solar-network/alpha.webp'
|
||||
|
||||
import 'animate.css'
|
||||
|
||||
interface DownloadableAsset {
|
||||
icon: JSX.Element
|
||||
title: string
|
||||
href: string
|
||||
open?: boolean
|
||||
}
|
||||
|
||||
interface AskableQuestion {
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
const fontSerif = Roboto_Serif({
|
||||
subsets: ['latin'],
|
||||
weight: ['300'],
|
||||
display: 'swap',
|
||||
style: 'italic',
|
||||
})
|
||||
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
title: 'Solar Network',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function ProductSolarNetwork() {
|
||||
const downloadableAssets: DownloadableAsset[] = [
|
||||
{
|
||||
icon: <AppleIcon />,
|
||||
title: 'iOS / macOS (App Store)',
|
||||
href: 'https://apps.apple.com/us/app/solian/id6499032345?itscg=30200&itsct=apps_box_link&mttnsubad=6499032345',
|
||||
},
|
||||
{
|
||||
icon: <AppleIcon />,
|
||||
title: 'iOS / macOS (TestFlight)',
|
||||
href: 'https://testflight.apple.com/join/YJ0lmN6O',
|
||||
},
|
||||
{
|
||||
icon: <AndroidIcon />,
|
||||
title: 'Android',
|
||||
href: 'https://files.solsynth.dev/production01/solian/app-arm64-v8a-release.apk',
|
||||
},
|
||||
{
|
||||
icon: <WindowIcon />,
|
||||
title: 'Windows',
|
||||
href: 'https://files.solsynth.dev/production01/solian/windows-x86_64-release.zip',
|
||||
},
|
||||
{
|
||||
icon: <WebIcon />,
|
||||
title: 'Web',
|
||||
href: 'https://sn.solsynth.dev',
|
||||
open: true,
|
||||
},
|
||||
{
|
||||
icon: <CodeIcon />,
|
||||
title: 'Source Code',
|
||||
href: 'https://github.com/Solsynth/HyperNet.Surface',
|
||||
},
|
||||
]
|
||||
|
||||
const askableQuestions: AskableQuestion[] = [
|
||||
{
|
||||
question: "What's the relationship between Solar Network and Solian?",
|
||||
answer:
|
||||
'Solian is the official app made for Solar Network. And the Solar Network is the official HyperNet instance hosted by Solsynth LLC. For simple, Solian is the app, and the Solar Network is the platform.',
|
||||
},
|
||||
{
|
||||
question: "What's the relationship between Solar Network and HyperNet?",
|
||||
answer:
|
||||
'HyperNet is the entire project including frontend app (also knowns as Solian for public) and the backend server. And the Solar Network is the official HyperNet instance which hosted and managed by Solsynth LLC who developed the HyperNet Project.',
|
||||
},
|
||||
{
|
||||
question: 'Which rules do I need to follow while using Solar Network?',
|
||||
answer:
|
||||
'Check out our Terms & Conditions for a detailed explanation of what you can do and cannot do on Solar Network. If you violate any of these rules, we have the right to suspend or terminate your account., you can see them in the drawer.',
|
||||
},
|
||||
{
|
||||
question: 'If I have any question about Solar Network, where can I get help?',
|
||||
answer: 'Feel free to email as at lily@solsynth.dev',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 24, display: 'flex', flexDirection: 'column', gap: 32 }}>
|
||||
<Box sx={{ textAlign: 'center', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Image
|
||||
src={ImgSolarNetworkIcon}
|
||||
width={128}
|
||||
height={128}
|
||||
style={{ objectFit: 'cover' }}
|
||||
className="shadow-xl rounded-2xl mx-auto mb-8 border border-1 border-gray-200"
|
||||
alt="solar network icon"
|
||||
/>
|
||||
<Box position="relative" width="fit-content" className="animate__animated animate__fadeInUp">
|
||||
<Typography variant="h4" component="h1">
|
||||
Solar Network
|
||||
</Typography>
|
||||
<Box
|
||||
position="absolute"
|
||||
top={-14}
|
||||
right={-24}
|
||||
sx={{ rotate: '30deg' }}
|
||||
className="animate__animated animate__pulse animate__infinite"
|
||||
>
|
||||
<Chip
|
||||
label="2.0"
|
||||
variant="outlined"
|
||||
sx={{ fontFamily: 'monospace', backgroundColor: 'background.default', fontSize: 12 }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="subtitle1" component="h1" className="animate__animated animate__fadeInUp">
|
||||
The next generation Social Network platform.
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
fontSize={26}
|
||||
fontFamily={fontSerif.style.fontFamily}
|
||||
sx={{ mt: 2.5, width: 'fit-content', fontStyle: 'italic' }}
|
||||
className="textmarker-effect animate__animated animate__fadeInUp"
|
||||
>
|
||||
Social Network, Redefined.
|
||||
</Typography>
|
||||
|
||||
<Link href="#download" sx={{ my: 2.5 }}>
|
||||
Download <DownloadIcon sx={{ fontSize: 15, marginLeft: 0.5 }} />
|
||||
</Link>
|
||||
|
||||
<Box position="relative" width="100%" sx={{ aspectRatio: 16 / 10, mt: 5 }}>
|
||||
<Image src={ImgSolarNetworkAlpha} fill alt="solar network screenshot" style={{ objectFit: 'cover' }} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box id="download">
|
||||
<Typography variant="h5" component="h2" textAlign="center" sx={{ mb: 5 }}>
|
||||
Download
|
||||
</Typography>
|
||||
|
||||
<Table sx={{ maxWidth: '800px', marginX: 'auto' }} aria-label="download table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell />
|
||||
<TableCell>Platform</TableCell>
|
||||
<TableCell align="right">Distribution</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{downloadableAssets.map((a) => (
|
||||
<TableRow key={a.href}>
|
||||
<TableCell>{a.icon}</TableCell>
|
||||
<TableCell>{a.title}</TableCell>
|
||||
<TableCell align="right">
|
||||
<NextLink passHref href={a.href} target="_blank">
|
||||
{a.open ? (
|
||||
<Link component="span">
|
||||
Open now
|
||||
<LaunchIcon sx={{ fontSize: 15, marginLeft: 0.5 }} />
|
||||
</Link>
|
||||
) : (
|
||||
<Link component="span">
|
||||
Download now
|
||||
<DownloadIcon sx={{ fontSize: 15, marginLeft: 0.5 }} />
|
||||
</Link>
|
||||
)}
|
||||
</NextLink>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
<Box id="faq">
|
||||
<Typography variant="h5" component="h2" textAlign="center" sx={{ mb: 5 }}>
|
||||
Frequently Asked Questions
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ maxWidth: '800px', marginX: 'auto' }}>
|
||||
{askableQuestions.map((q) => (
|
||||
<Accordion key={q.question}>
|
||||
<AccordionSummary expandIcon={<ArrowDownward />} aria-controls="panel1-content" id="panel1-header">
|
||||
<Typography component="span">{q.question}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography variant="body1">{q.answer}</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
---
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
---
|
||||
|
||||
<Layout title="Terms & Conditions">
|
||||
<div
|
||||
class="container max-w-[85ch] mx-auto mt-[25vh] px-12 flex flex-col gap-4"
|
||||
>
|
||||
<h1 class="text-2xl font-bold">Terms & Conditions</h1>
|
||||
|
||||
<p>
|
||||
This place is the collections of all the terms and conditions that you
|
||||
will have to agree to in order to use our products.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We're trying to make it as simple as possible. And it's good for both of
|
||||
us. You do not need care about this in normal. Just makes our lawyers
|
||||
happy. <i>Do we really have a lawyer?</i>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The information here may changed from time to time. Please refresh to
|
||||
check this page for the latest version.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-4 mt-4 mx-[-16px]">
|
||||
<a href="/terms/user-agreements">
|
||||
<div class="card bg-base-100 w-full border-neutral border">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">User Agreements</h2>
|
||||
<p>
|
||||
The User Agreements for users who using the Solar Network and
|
||||
other products from us.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/terms/privacy-policy">
|
||||
<div class="card bg-base-100 w-full border-neutral border">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Privacy Policy</h2>
|
||||
<p>
|
||||
The Privacy Policy shows we how to process and handle the data
|
||||
provided by you.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
52
src/pages/terms/index.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { Box, Button, Card, CardActions, CardContent, Container, Typography } from '@mui/material'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function Terms() {
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Typography variant="h4" component="h1" gutterBottom sx={{ mt: 2 }}>
|
||||
Terms & Conditions
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1">
|
||||
Nothing too special here, just some legal files which make our lawyers happy.{' '}
|
||||
<del>Do we really have a lawyer?</del>
|
||||
</Typography>
|
||||
|
||||
<Box display="flex" flexDirection="column" gap={2} sx={{ mt: 2 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h5" component="h2" gutterBottom>
|
||||
Privacy Policy
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2">Learn about how do we protect your data and privacy.</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Link href="/terms/privacy-policy" passHref>
|
||||
<Button size="small">Read this policy</Button>
|
||||
</Link>
|
||||
</CardActions>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h5" component="h2" gutterBottom>
|
||||
User Agreements
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2">
|
||||
Learn about how do we dealing the user generated content on Solar Network, distrubution of our products
|
||||
and more.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Link href="/terms/user-agreements" passHref>
|
||||
<Button size="small">Read this agreements</Button>
|
||||
</Link>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
---
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
---
|
||||
|
||||
<Layout title="User Agreements">
|
||||
<div
|
||||
class="container max-w-[85ch] mx-auto mt-[25vh] px-12 flex flex-col gap-4"
|
||||
>
|
||||
<h1 class="text-4xl font-bold">Privacy Policy</h1>
|
||||
|
||||
<article class="prose prose-lg max-w-none mt-5">
|
||||
<h2 id="introduction">Introduction</h2>
|
||||
<p>
|
||||
We take your privacy seriously. This privacy policy outlines the types
|
||||
of personal information we collect, how we use it, and the measures we
|
||||
take to protect your data.
|
||||
</p>
|
||||
<h2 id="information-collection">Information Collection</h2>
|
||||
<p>
|
||||
We collect personal information only when necessary to provide our
|
||||
services. This may include your name, email address, and other relevant
|
||||
details.
|
||||
</p>
|
||||
<h2 id="use-of-information">Use of Information</h2>
|
||||
<p>We use your personal information to:</p>
|
||||
<ul>
|
||||
<li>Provide and improve our services</li>
|
||||
<li>Communicate with you about updates or important information</li>
|
||||
<li>Ensure compliance with legal obligations</li>
|
||||
</ul>
|
||||
<h2 id="data-sharing">Data Sharing</h2>
|
||||
<p>
|
||||
We do not sell, trade, or share your personal information with third
|
||||
parties except as required by law.
|
||||
</p>
|
||||
<h2 id="data-security">Data Security</h2>
|
||||
<p>
|
||||
We implement robust security measures to protect your personal
|
||||
information from unauthorized access, alteration, disclosure, or
|
||||
destruction.
|
||||
</p>
|
||||
<h2 id="your-rights">Your Rights</h2>
|
||||
<p>You have the right to:</p>
|
||||
<ul>
|
||||
<li>Access the personal information we hold about you</li>
|
||||
<li>Request corrections to your personal information</li>
|
||||
<li>Request the deletion of your personal information</li>
|
||||
</ul>
|
||||
<h2 id="contact-us">Contact Us</h2>
|
||||
<p>
|
||||
If you have any questions or concerns about this privacy policy or our
|
||||
data practices, please contact us at lily@solsynth.dev.
|
||||
</p>
|
||||
<h2 id="changes-to-this-policy">Changes to This Policy</h2>
|
||||
<p>
|
||||
We may update this privacy policy from time to time. Any changes will be
|
||||
posted on this page, and we will notify you of any significant changes.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</Layout>
|
65
src/pages/terms/privacy-policy.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { Box, Container, Divider, Typography } from '@mui/material'
|
||||
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
title: 'Privacy Policy',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function PrivacyPolicy() {
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Typography variant="h3" component="h1" sx={{ mt: 2, mb: 5 }}>
|
||||
Privacy Policy
|
||||
</Typography>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box component="article" sx={{ my: 5, maxWidth: 'unset' }} className="prose prose-lg">
|
||||
<h2 id="introduction">Introduction</h2>
|
||||
<p>
|
||||
We take your privacy seriously. This privacy policy outlines the types of personal information we collect, how
|
||||
we use it, and the measures we take to protect your data.
|
||||
</p>
|
||||
<h2 id="information-collection">Information Collection</h2>
|
||||
<p>
|
||||
We collect personal information only when necessary to provide our services. This may include your name, email
|
||||
address, and other relevant details.
|
||||
</p>
|
||||
<h2 id="use-of-information">Use of Information</h2>
|
||||
<p>We use your personal information to:</p>
|
||||
<ul>
|
||||
<li>Provide and improve our services</li>
|
||||
<li>Communicate with you about updates or important information</li>
|
||||
<li>Ensure compliance with legal obligations</li>
|
||||
</ul>
|
||||
<h2 id="data-sharing">Data Sharing</h2>
|
||||
<p>We do not sell, trade, or share your personal information with third parties except as required by law.</p>
|
||||
<h2 id="data-security">Data Security</h2>
|
||||
<p>
|
||||
We implement robust security measures to protect your personal information from unauthorized access,
|
||||
alteration, disclosure, or destruction.
|
||||
</p>
|
||||
<h2 id="your-rights">Your Rights</h2>
|
||||
<p>You have the right to:</p>
|
||||
<ul>
|
||||
<li>Access the personal information we hold about you</li>
|
||||
<li>Request corrections to your personal information</li>
|
||||
<li>Request the deletion of your personal information</li>
|
||||
</ul>
|
||||
<h2 id="contact-us">Contact Us</h2>
|
||||
<p>
|
||||
If you have any questions or concerns about this privacy policy or our data practices, please contact us at
|
||||
lily@solsynth.dev.
|
||||
</p>
|
||||
<h2 id="changes-to-this-policy">Changes to This Policy</h2>
|
||||
<p>
|
||||
We may update this privacy policy from time to time. Any changes will be posted on this page, and we will
|
||||
notify you of any significant changes.
|
||||
</p>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
---
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
---
|
||||
|
||||
<Layout title="User Agreements">
|
||||
<div
|
||||
class="container max-w-[85ch] mx-auto mt-[25vh] px-12 flex flex-col gap-4"
|
||||
>
|
||||
<h1 class="text-4xl font-bold">User Agreements</h1>
|
||||
|
||||
<article class="prose prose-lg max-w-none mt-5">
|
||||
<p>
|
||||
This Agreement applies to all Solsynth LLC products, including but not
|
||||
limited to Solar Network, Solian, DietaryGuard, AceField.
|
||||
</p>
|
||||
<h2 id="provision-and-discontinuance-of-service">
|
||||
Provision and Discontinuance of Service
|
||||
</h2>
|
||||
<p>
|
||||
Solsynth LLC will provide equal service to all living things in the
|
||||
world, including grasshoppers. We also reserve the right to stop service
|
||||
to any user. We do not require prior notice for discontinuing services
|
||||
to some users.
|
||||
</p>
|
||||
<h2 id="user-generated-content">User Generated Content</h2>
|
||||
<p>
|
||||
Any content posted on Solar Network (including but not limited to posts,
|
||||
articles, attachments) grants Solsynth LLC the right to display it by
|
||||
default. Unless otherwise stated by the user, all rights are reserved by
|
||||
the original poster, and reprints should be authorized by the original
|
||||
poster.
|
||||
</p>
|
||||
<h3 id="reproduction-recognition">Reproduction Recognition</h3>
|
||||
<p>
|
||||
Unless specifically stated by the poster, all content is subject to the
|
||||
definition of reprint in this section.
|
||||
</p>
|
||||
<p>
|
||||
Republishing means uploading the content of the original post to another
|
||||
platform or to the Solar Network, either unchanged or with minor
|
||||
modifications, provided that simultaneous reposting of the post,
|
||||
embedded components, and links to the presentation do not constitute
|
||||
republishing. Republishing also requires attribution when authorized by
|
||||
the original poster.
|
||||
</p>
|
||||
<h3 id="freedom-of-speech">Freedom of Speech</h3>
|
||||
<p>
|
||||
We do not remove user-generated content except in cases of misuse of
|
||||
resources. We will not ask any user to remove any content.
|
||||
</p>
|
||||
<p>
|
||||
However, Solsynth LLC reserves the right to restrict and stop the
|
||||
display of content to the public that violates community guidelines
|
||||
(e.g., obscenity, violence, gore, anti-social, terrorist organizations,
|
||||
etc.).
|
||||
</p>
|
||||
<p>
|
||||
Although you have 100% freedom of speech on Solar Network. However,
|
||||
please be aware that freedom of speech does not mean that you will not
|
||||
be held accountable for what you say.
|
||||
</p>
|
||||
<h4 id="restriction-and-discontinuation">
|
||||
Restriction and Discontinuation
|
||||
</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
Restriction of Display: Discontinuation of related tweets, while
|
||||
retaining the right to access them directly through resource
|
||||
identifiers and sharing links.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
Cease display: stop all access to the resource by anyone other than
|
||||
the author.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 id="resource-misuse-prevention-policy">
|
||||
Resource Misuse Prevention Policy
|
||||
</h2>
|
||||
<p>
|
||||
Although there are no capacity limitations for using Solar Network's
|
||||
data hosting services, resources determined to be abusive will be
|
||||
disenfranchised from some features. Solsynth LLC reserves the right to
|
||||
reclaim space on previously uploaded resources for deletion.
|
||||
</p>
|
||||
<h3 id="determination-of-misuse">Determination of Misuse</h3>
|
||||
<ul>
|
||||
<li>
|
||||
Uploading without using: e.g. uploading excessive attachments in Solar
|
||||
Network's Interactive Attachment Pool and not linking them to
|
||||
posts.
|
||||
</li>
|
||||
<li>
|
||||
Meaningless Posts: meaningless shuffling or wasting of Solar
|
||||
Network's storage resources
|
||||
</li>
|
||||
<li>
|
||||
Misuse: using Solar Network's public resources as if they were
|
||||
your own dedicated pool (see the Wiki's Dedicated Pools page for
|
||||
details).
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
The Solsynth Trust & Safety Team is ultimately responsible for
|
||||
determining misuse.
|
||||
</p>
|
||||
<h2 id="secondary-releases">Secondary Releases</h2>
|
||||
<p>
|
||||
A secondary release is when our assets are downloaded and re-hosted on
|
||||
another site.
|
||||
</p>
|
||||
<h3 id="product-secondary-release">Product Secondary Release</h3>
|
||||
<p>
|
||||
Unless otherwise stated, Solsynth LLC products are not available for
|
||||
secondary distribution, please do not download our product builds and
|
||||
upload them twice to another site. Please do not download our product
|
||||
builds and upload them to other sites. <strong
|
||||
>Secondary distribution for commercial use is not permitted.
|
||||
</strong>.
|
||||
</p>
|
||||
<p>
|
||||
What you should do is post a link to our product on another site. Or use
|
||||
the embedded component. And indicate Solsynth LLC All Rights Reserved.
|
||||
</p>
|
||||
<p>
|
||||
If you want to build a mirror site of our products, please contact us to
|
||||
waive this rule.
|
||||
</p>
|
||||
<h3 id="secondary-distribution-of-source-code">
|
||||
Secondary distribution of source code
|
||||
</h3>
|
||||
<p>
|
||||
We do not allow any form of redistribution of source code (except for
|
||||
Forks). This includes, but is not limited to, mirroring code
|
||||
repositories on GitHub or the Solsynth Code Repository to other Git
|
||||
providers such as GitLab, Gitee, and so on.
|
||||
<strong>Selling source code twice is not allowed. </strong>
|
||||
</p>
|
||||
<p>
|
||||
For more information on source code usage regulations, please follow the
|
||||
open source license used by the project.
|
||||
</p>
|
||||
<p>
|
||||
If you would like to set up a mirror of our source code, please contact
|
||||
us to waive this policy.
|
||||
</p>
|
||||
<hr />
|
||||
<p>
|
||||
Solsynth LLC reserves the right of final interpretation of this
|
||||
agreement.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</Layout>
|
121
src/pages/terms/user-agreements.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import { Box, Container, Divider, Typography } from '@mui/material'
|
||||
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
title: 'User Agreements',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function PrivacyPolicy() {
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Typography variant="h3" component="h1" sx={{ mt: 2, mb: 5 }}>
|
||||
User Agreements
|
||||
</Typography>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box component="article" sx={{ my: 5, maxWidth: 'unset' }} className="prose prose-lg">
|
||||
<p>
|
||||
This Agreement applies to all Solsynth LLC products, including but not limited to Solar Network, Solian,
|
||||
DietaryGuard, AceField.
|
||||
</p>
|
||||
<h2 id="provision-and-discontinuance-of-service">Provision and Discontinuance of Service</h2>
|
||||
<p>
|
||||
Solsynth LLC will provide equal service to all living things in the world, including grasshoppers. We also
|
||||
reserve the right to stop service to any user. We do not require prior notice for discontinuing services to
|
||||
some users.
|
||||
</p>
|
||||
<h2 id="user-generated-content">User Generated Content</h2>
|
||||
<p>
|
||||
Any content posted on Solar Network (including but not limited to posts, articles, attachments) grants
|
||||
Solsynth LLC the right to display it by default. Unless otherwise stated by the user, all rights are reserved
|
||||
by the original poster, and reprints should be authorized by the original poster.
|
||||
</p>
|
||||
<h3 id="reproduction-recognition">Reproduction Recognition</h3>
|
||||
<p>
|
||||
Unless specifically stated by the poster, all content is subject to the definition of reprint in this section.
|
||||
</p>
|
||||
<p>
|
||||
Republishing means uploading the content of the original post to another platform or to the Solar Network,
|
||||
either unchanged or with minor modifications, provided that simultaneous reposting of the post, embedded
|
||||
components, and links to the presentation do not constitute republishing. Republishing also requires
|
||||
attribution when authorized by the original poster.
|
||||
</p>
|
||||
<h3 id="freedom-of-speech">Freedom of Speech</h3>
|
||||
<p>
|
||||
We do not remove user-generated content except in cases of misuse of resources. We will not ask any user to
|
||||
remove any content.
|
||||
</p>
|
||||
<p>
|
||||
However, Solsynth LLC reserves the right to restrict and stop the display of content to the public that
|
||||
violates community guidelines (e.g., obscenity, violence, gore, anti-social, terrorist organizations, etc.).
|
||||
</p>
|
||||
<p>
|
||||
Although you have 100% freedom of speech on Solar Network. However, please be aware that freedom of speech
|
||||
does not mean that you will not be held accountable for what you say.
|
||||
</p>
|
||||
<h4 id="restriction-and-discontinuation">Restriction and Discontinuation</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
Restriction of Display: Discontinuation of related tweets, while retaining the right to access them
|
||||
directly through resource identifiers and sharing links.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Cease display: stop all access to the resource by anyone other than the author.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 id="resource-misuse-prevention-policy">Resource Misuse Prevention Policy</h2>
|
||||
<p>
|
||||
Although there are no capacity limitations for using Solar Network's data hosting services, resources
|
||||
determined to be abusive will be disenfranchised from some features. Solsynth LLC reserves the right to
|
||||
reclaim space on previously uploaded resources for deletion.
|
||||
</p>
|
||||
<h3 id="determination-of-misuse">Determination of Misuse</h3>
|
||||
<ul>
|
||||
<li>
|
||||
Uploading without using: e.g. uploading excessive attachments in Solar Network's Interactive Attachment
|
||||
Pool and not linking them to posts.
|
||||
</li>
|
||||
<li>Meaningless Posts: meaningless shuffling or wasting of Solar Network's storage resources</li>
|
||||
<li>
|
||||
Misuse: using Solar Network's public resources as if they were your own dedicated pool (see the
|
||||
Wiki's Dedicated Pools page for details).
|
||||
</li>
|
||||
</ul>
|
||||
<p>The Solsynth Trust & Safety Team is ultimately responsible for determining misuse.</p>
|
||||
<h2 id="secondary-releases">Secondary Releases</h2>
|
||||
<p>A secondary release is when our assets are downloaded and re-hosted on another site.</p>
|
||||
<h3 id="product-secondary-release">Product Secondary Release</h3>
|
||||
<p>
|
||||
Unless otherwise stated, Solsynth LLC products are not available for secondary distribution, please do not
|
||||
download our product builds and upload them twice to another site. Please do not download our product builds
|
||||
and upload them to other sites. <strong>Secondary distribution for commercial use is not permitted.</strong>.
|
||||
</p>
|
||||
<p>
|
||||
What you should do is post a link to our product on another site. Or use the embedded component. And indicate
|
||||
Solsynth LLC All Rights Reserved.
|
||||
</p>
|
||||
<p>If you want to build a mirror site of our products, please contact us to waive this rule.</p>
|
||||
<h3 id="secondary-distribution-of-source-code">Secondary distribution of source code</h3>
|
||||
<p>
|
||||
We do not allow any form of redistribution of source code (except for Forks). This includes, but is not
|
||||
limited to, mirroring code repositories on GitHub or the Solsynth Code Repository to other Git providers such
|
||||
as GitLab, Gitee, and so on.
|
||||
<strong>Selling source code twice is not allowed. </strong>
|
||||
</p>
|
||||
<p>
|
||||
For more information on source code usage regulations, please follow the open source license used by the
|
||||
project.
|
||||
</p>
|
||||
<p>If you would like to set up a mirror of our source code, please contact us to waive this policy.</p>
|
||||
<hr />
|
||||
<p>Solsynth LLC reserves the right of final interpretation of this agreement.</p>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
152
src/pages/users/[name].tsx
Normal file
@ -0,0 +1,152 @@
|
||||
import { SnCheckInRecord } from '@/services/checkIn'
|
||||
import { getAttachmentUrl, sni } from '@/services/network'
|
||||
import { SnAccount, SnAccountBadgeMapping } from '@/services/user'
|
||||
import { Avatar, Box, Card, CardContent, Container, Grid2 as Grid, Typography } from '@mui/material'
|
||||
import { LineChart } from '@mui/x-charts'
|
||||
import type { InferGetServerSidePropsType, GetServerSideProps } from 'next'
|
||||
import Image from 'next/image'
|
||||
|
||||
export const getServerSideProps = (async (context) => {
|
||||
const name = context.params!.name as string
|
||||
try {
|
||||
const { data: user } = await sni.get<SnAccount>('/cgi/id/users/' + name)
|
||||
const { data: checkIn } = await sni.get<{ data: SnCheckInRecord[] }>('/cgi/id/users/' + name + '/check-in', {
|
||||
params: { take: 14 },
|
||||
})
|
||||
return { props: { user, checkIn: checkIn.data } }
|
||||
} catch (err) {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}) satisfies GetServerSideProps<{ user: SnAccount; checkIn: SnCheckInRecord[] }>
|
||||
|
||||
export default function UserProfile({ user, checkIn }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
return (
|
||||
<>
|
||||
{user.banner && (
|
||||
<Box sx={{ aspectRatio: 16 / 5, position: 'relative' }}>
|
||||
<Image src={getAttachmentUrl(user.banner)} alt="account banner" style={{ objectFit: 'cover' }} fill />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Container sx={{ mt: 4, px: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
{user && <Avatar src={getAttachmentUrl(user.avatar)} />}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography fontWeight="bold">{user.nick}</Typography>
|
||||
<Typography fontFamily="monospace" fontSize={13} lineHeight={1.2}>
|
||||
@{user.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={2} sx={{ mt: 3 }}>
|
||||
<Grid size={{ xs: 12, sm: 12, md: 8 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Fortune History
|
||||
</Typography>
|
||||
<LineChart
|
||||
yAxis={[
|
||||
{
|
||||
data: [1, 2, 3, 4, 5],
|
||||
tickMinStep: 1,
|
||||
tickMaxStep: 1,
|
||||
valueFormatter(value, _) {
|
||||
const resultTierList = ['大凶', '凶', '中平', '吉', '大吉']
|
||||
return resultTierList[value]
|
||||
},
|
||||
},
|
||||
]}
|
||||
xAxis={[
|
||||
{
|
||||
scaleType: 'band',
|
||||
data: checkIn.map((c) => {
|
||||
const og = new Date(c.createdAt)
|
||||
og.setHours(0, 0, 0, 0)
|
||||
return og
|
||||
}),
|
||||
valueFormatter(value, _) {
|
||||
return new Date(value).toLocaleDateString('en-US', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
})
|
||||
},
|
||||
},
|
||||
]}
|
||||
series={[
|
||||
{
|
||||
data: checkIn.map((c) => c.resultTier),
|
||||
valueFormatter(value, _) {
|
||||
const resultTierList = ['大凶', '凶', '中平', '吉', '大吉']
|
||||
return resultTierList[value ?? 0]
|
||||
},
|
||||
},
|
||||
]}
|
||||
height={300}
|
||||
margin={{ top: 16, bottom: 24 }}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid
|
||||
size={{ xs: 12, sm: 12, md: 4 }}
|
||||
order={{ xs: -1, sm: -1, md: 1 }}
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
|
||||
>
|
||||
{user.badges && (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Badges
|
||||
</Typography>
|
||||
|
||||
<Box display="flex" flexDirection="column" gap={0.5}>
|
||||
{user.badges.map((b) => (
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'start' }}>
|
||||
{SnAccountBadgeMapping[b.type].icon}
|
||||
<Box>
|
||||
<Typography variant="body2">{SnAccountBadgeMapping[b.type].name}</Typography>
|
||||
{b.metadata.title && <Typography variant="subtitle2">{b.metadata.title}</Typography>}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Information
|
||||
</Typography>
|
||||
|
||||
{user.description && (
|
||||
<Typography variant="body1" gutterBottom>
|
||||
{user.description}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Typography variant="body2">
|
||||
Born on {new Date(user.profile!.birthday!).toLocaleDateString()}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Joined at {new Date(user.createdAt).toLocaleDateString()}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="overline" lineHeight={1} fontFamily="monospace">
|
||||
#{user.id.toString().padStart(8, '0')}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
64
src/pages/users/me.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { checkAuthenticatedClient, redirectToLogin } from '@/services/auth'
|
||||
import { useUserStore } from '@/services/user'
|
||||
import { Avatar, Box, Button, Container, Typography } from '@mui/material'
|
||||
import { getAttachmentUrl } from '@/services/network'
|
||||
import { useEffect } from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
import LogoutIcon from '@mui/icons-material/Logout'
|
||||
import LaunchIcon from '@mui/icons-material/Launch'
|
||||
import Link from 'next/link'
|
||||
import { deleteCookie } from 'cookies-next/client'
|
||||
|
||||
export default function UserItself() {
|
||||
useEffect(() => {
|
||||
if (!checkAuthenticatedClient()) redirectToLogin()
|
||||
}, [])
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
function logout() {
|
||||
deleteCookie('nex_user_atk')
|
||||
deleteCookie('nex_user_rtk')
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{userStore.account?.banner && (
|
||||
<Box sx={{ aspectRatio: 16 / 5, position: 'relative' }}>
|
||||
<Image src={getAttachmentUrl(userStore.account!.banner)} alt="account banner" objectFit="cover" fill />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Container sx={{ mt: 4, px: 2 }}>
|
||||
<Typography variant="h5" component="h1">
|
||||
Your Solarpass
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
{userStore.account && <Avatar src={getAttachmentUrl(userStore.account.avatar)} />}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography fontWeight="bold">{userStore.account?.nick}</Typography>
|
||||
<Typography fontFamily="monospace" fontSize={13} lineHeight={1.2}>
|
||||
@{userStore.account?.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
<Link passHref href="https://sn.solsynth.dev/account" target="_blank">
|
||||
<Button variant="contained" color="primary" startIcon={<LaunchIcon />}>
|
||||
Open in Solian
|
||||
</Button>
|
||||
</Link>
|
||||
<Button variant="contained" color="error" startIcon={<LogoutIcon />} onClick={logout}>
|
||||
Logout
|
||||
</Button>
|
||||
</Box>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
---
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
---
|
||||
|
||||
<Layout title="Terms & Conditions">
|
||||
<div
|
||||
class="container max-w-[85ch] mx-auto mt-[25vh] px-2 flex flex-col gap-4"
|
||||
>
|
||||
<h1 class="text-2xl font-bold">条款和条件</h1>
|
||||
|
||||
<p>这里罗列了您使用我们的产品将需要同意的所有条款和条件。</p>
|
||||
|
||||
<p>
|
||||
我们正努力让事情尽可能简单。这对我们双方都有好处。通常情况下你不需要关心这件事。只是为了让我们的律师高兴。
|
||||
<i>我们真的有律师吗?</i>
|
||||
</p>
|
||||
|
||||
<p>此处的信息可能会不时更改。请刷新 查看此页面以获取最新版本。</p>
|
||||
|
||||
<div class="flex flex-col gap-4 mt-4 mx-[-16px]">
|
||||
<a href="/terms/user-agreements">
|
||||
<div class="card bg-base-100 w-full border-neutral border">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">用户协议</h2>
|
||||
<p>使用 Solar Network 和 我们其他产品的用户须遵守的用户协议。</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/terms/privacy-policy">
|
||||
<div class="card bg-base-100 w-full border-neutral border">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">隐私政策</h2>
|
||||
<p>隐私政策向我们展示了如何处理和处理您提供的数据。</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
@ -1,51 +0,0 @@
|
||||
---
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
---
|
||||
|
||||
<Layout title="User Agreements">
|
||||
<div
|
||||
class="container max-w-[85ch] mx-auto mt-[25vh] px-12 flex flex-col gap-4"
|
||||
>
|
||||
<h1 class="text-4xl font-bold">隐私政策</h1>
|
||||
|
||||
<article class="prose prose-lg max-w-none mt-5">
|
||||
<h2 id="introduction">简介</h2>
|
||||
<p>
|
||||
我们非常重视您的隐私。本隐私政策概述了我们收集的个人信息类型、使用方式以及我们采取的保护措施。
|
||||
</p>
|
||||
<h2 id="information-collection">信息收集</h2>
|
||||
<p>
|
||||
我们仅在提供服务时收集必要的个人信息。这可能包括您的姓名、电子邮件地址以及其他相关信息。
|
||||
</p>
|
||||
<h2 id="use-of-information">信息使用</h2>
|
||||
<p>我们使用您的个人信息来:</p>
|
||||
<ul>
|
||||
<li>提供和改进我们的服务</li>
|
||||
<li>与您沟通更新或重要信息</li>
|
||||
<li>确保遵守法律义务</li>
|
||||
</ul>
|
||||
<h2 id="data-sharing">数据共享</h2>
|
||||
<p>我们不会出售、交易或与第三方分享您的个人信息,法律要求除外。</p>
|
||||
<h2 id="data-security">数据安全</h2>
|
||||
<p>
|
||||
我们实施了强有力的安全措施,以保护您的个人信息免受未经授权的访问、更改、披露或销毁。
|
||||
</p>
|
||||
<h2 id="your-rights">您的权利</h2>
|
||||
<p>您有权:</p>
|
||||
<ul>
|
||||
<li>访问我们持有的关于您的个人信息</li>
|
||||
<li>请求更正您的个人信息</li>
|
||||
<li>请求删除您的个人信息</li>
|
||||
</ul>
|
||||
<h2 id="contact-us">联系我们</h2>
|
||||
<p>
|
||||
如果您对本隐私政策或我们的数据处理方式有任何疑问或顾虑,请通过
|
||||
lily@solsynth.dev 与我们联系。
|
||||
</p>
|
||||
<h2 id="changes-to-this-policy">政策变更</h2>
|
||||
<p>
|
||||
我们可能会不时更新本隐私政策。任何更改将发布在此页面上,且我们会通知您任何重大更改。
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</Layout>
|
@ -1,101 +0,0 @@
|
||||
---
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
---
|
||||
|
||||
<Layout title="User Agreements">
|
||||
<div
|
||||
class="container max-w-[85ch] mx-auto mt-[25vh] px-12 flex flex-col gap-4"
|
||||
>
|
||||
<h1 class="text-4xl font-bold">用户协议</h1>
|
||||
|
||||
<article class="prose prose-lg max-w-none mt-5">
|
||||
<p>
|
||||
本协议适用于所有 Solsynth LLC 的产品,包括但不限于 Solar
|
||||
Network、Solian、DietaryGuard、AceField。
|
||||
</p>
|
||||
<h2 id="provision-and-discontinuance-of-service">服务的提供和停止</h2>
|
||||
<p>
|
||||
Solsynth LLC 将向世界上所有的生物提供同等的服务,包括草履虫。
|
||||
同时也保留向任意用户停止提供服务的权利。关于停止部分用户的服务,我们不需要提前通知。
|
||||
</p>
|
||||
<h2 id="user-generated-content">用户生成内容</h2>
|
||||
<p>
|
||||
任意发布在 Solar Network
|
||||
上的内容(包括但不限于帖子、文章、附件)都默认授权 Solsynth LLC
|
||||
予以展示的权利。
|
||||
除非用户特别声明,所有内容均为原帖主保留所有权利,转载请先向原帖主授权。
|
||||
</p>
|
||||
<h3 id="reproduction-recognition">转载的认定</h3>
|
||||
<p>无帖主特别声明,所有内容均适用本条转载的定义。</p>
|
||||
<p>
|
||||
转载指将原帖的内容原封不动或略作改动上传到别的平台或 Solar
|
||||
Network。但同时转帖、嵌入式组件与展示展开的链接不构成转载。
|
||||
转载即时在原帖主授权的情况下也需表明出处。
|
||||
</p>
|
||||
<h3 id="freedom-of-speech">言论的自由</h3>
|
||||
<p>
|
||||
除滥用资源的情况,我们不会将用户生成内容进行删除。也不会做出要求任何用户删除任何内容的要求。
|
||||
</p>
|
||||
<p>
|
||||
但 Solsynth LLC
|
||||
始终保留对于违反社区准则的内容(如淫秽、暴力、血腥、反社会、恐怖组织等)限制与停止向公众展示的权利。
|
||||
</p>
|
||||
<p>
|
||||
尽管在 Solar Network 上你拥有 100%
|
||||
的言论自由。但还请清楚,言论自由不代表不用对自己的言论负责。
|
||||
</p>
|
||||
<h4 id="restriction-and-discontinuation">限制展示与停止展示</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
限制展示:停止相关的推送,但是任保留直接通过资源标识符和分享连接访问的权利
|
||||
</p>
|
||||
</li>
|
||||
<li><p>停止展示:全面停止除作者之外任何人访问该资源的权利</p></li>
|
||||
</ul>
|
||||
<h2 id="resource-misuse-prevention-policy">防止资源滥用条例</h2>
|
||||
<p>
|
||||
尽管使用 Solar Network
|
||||
的数据托管服务并无任何的容量限制,但经过判定的滥用资源将会被取消使用部分功能的权利。
|
||||
并且之前上传的资源 Solsynth LLC 有权对其进行删除空间回收。
|
||||
</p>
|
||||
<h3 id="determination-of-misuse">滥用的认定</h3>
|
||||
<ul>
|
||||
<li>
|
||||
传而不用:例如在 Solar Network 的 Interactive
|
||||
附件池中过度上传附件并不将附件与帖子连接
|
||||
</li>
|
||||
<li>无意义帖:无意义洗版或浪费 Solar Network 的存储资源</li>
|
||||
<li>
|
||||
走错片场:将 Solar Network
|
||||
公有资源当作自己的专用资源池使用(详见维基《专用资源池》页面)
|
||||
</li>
|
||||
</ul>
|
||||
<p>滥用的认定最终解释权归属于 Solsynth Trust & Safety Team</p>
|
||||
<h2 id="secondary-releases">二次发布</h2>
|
||||
<p>二次发布指将我们的资产下载并重新托管到别站。</p>
|
||||
<h3 id="product-secondary-release">制品二次发布</h3>
|
||||
<p>
|
||||
除特殊声明,Solsynth LLC
|
||||
的产品均不允许二次发布,请勿将我们的产品构建下载并二次上传于其他站点。
|
||||
<strong>二次作为商用发布更是不允许的。</strong>
|
||||
</p>
|
||||
<p>
|
||||
你应该做的是将我们的产品链接贴上他站。或使用嵌入式组件。并且表明
|
||||
Solsynth LLC 版权所有。
|
||||
</p>
|
||||
<p>若您想搭建我们制品的镜像站,请与我们取得联系以豁免此条例。</p>
|
||||
<h3 id="secondary-distribution-of-source-code">源码二次发布</h3>
|
||||
<p>我们不允许任何形式的源码二次发布(Fork 除外)。</p>
|
||||
<p>
|
||||
包括但不限于,将 GitHub 或 Solsynth Code Repository 上的代码仓库镜像于
|
||||
GitLab、Gitee 等其他 Git 提供者。
|
||||
<strong>二次售卖源码更是不允许的。</strong>
|
||||
</p>
|
||||
<p>关于更多的源码使用条例,请遵循项目使用的开源许可证。</p>
|
||||
<p>若您想搭建我们源码的镜像站,请与我们取得联系以豁免此条例。</p>
|
||||
<hr />
|
||||
<p>Solsynth LLC 保留对此协议的最终解释权</p>
|
||||
</article>
|
||||
</div>
|
||||
</Layout>
|
@ -1,26 +0,0 @@
|
||||
export function getAttachmentUrl(identifier?: string): string {
|
||||
if (!identifier) return ''
|
||||
if (identifier.startsWith('http')) {
|
||||
return identifier
|
||||
}
|
||||
const baseUrl = import.meta.env.PUBLIC_SOLAR_NETWORK_URL
|
||||
return `${baseUrl}/cgi/uc/attachments/${identifier}`
|
||||
}
|
||||
|
||||
export async function fetchAttachmentMeta(
|
||||
identifiers: string[]
|
||||
): Promise<any[]> {
|
||||
if (!identifiers) return []
|
||||
|
||||
const baseUrl = import.meta.env.PUBLIC_SOLAR_NETWORK_URL
|
||||
const resp = await fetch(
|
||||
`${baseUrl}/cgi/uc/attachments?take=${
|
||||
identifiers.length
|
||||
}&id=${identifiers.join(',')}`
|
||||
)
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(`Failed to fetch attachment meta: ${await resp.text()}`)
|
||||
}
|
||||
const out = await resp.json()
|
||||
return out['data']
|
||||
}
|
48
src/services/attachment.ts
Normal file
@ -0,0 +1,48 @@
|
||||
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
|
||||
}
|
44
src/services/auth.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { hasCookie } from 'cookies-next/client'
|
||||
|
||||
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 checkAuthenticatedClient(): boolean {
|
||||
return !!hasCookie('nex_user_atk')
|
||||
}
|
||||
|
||||
export function redirectToLogin() {
|
||||
window.open('/auth/login?redirect_uri=' + encodeURIComponent(window.location.pathname), '_self')
|
||||
}
|
10
src/services/checkIn.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface SnCheckInRecord {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
resultTier: number
|
||||
resultExperience: number
|
||||
resultModifiers: number[]
|
||||
accountId: number
|
||||
}
|
77
src/services/network.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import applyCaseMiddleware from 'axios-case-converter'
|
||||
import { hasCookie, getCookie, setCookie } from 'cookies-next/client'
|
||||
|
||||
const baseURL = 'https://api.sn.solsynth.dev'
|
||||
|
||||
export let 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,
|
||||
)
|
||||
|
||||
applyCaseMiddleware(inst, {
|
||||
ignoreParams: true,
|
||||
ignoreHeaders: true,
|
||||
})
|
||||
return inst
|
||||
})()
|
||||
|
||||
async function refreshToken(): Promise<string | undefined> {
|
||||
if (!hasCookie('nex_user_atk') || !hasCookie('nex_user_rtk')) return
|
||||
|
||||
const ogTk: string = getCookie('nex_user_atk')!
|
||||
if (!isTokenExpired(ogTk)) return ogTk
|
||||
|
||||
const resp = await axios.post(
|
||||
'/cgi/id/auth/token',
|
||||
{
|
||||
refresh_token: getCookie('nex_user_rtk')!,
|
||||
grant_type: 'refresh_token',
|
||||
},
|
||||
{ baseURL },
|
||||
)
|
||||
const atk: string = resp.data['access_token']
|
||||
const rtk: string = resp.data['refresh_token']
|
||||
setCookie('nex_user_atk', atk, { path: '/', maxAge: 2592000 })
|
||||
setCookie('nex_user_rtk', rtk, { path: '/', maxAge: 2592000 })
|
||||
|
||||
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}`
|
||||
}
|
85
src/services/post.ts
Normal file
@ -0,0 +1,85 @@
|
||||
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
|
||||
}
|
97
src/services/user.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import { create } from 'zustand'
|
||||
import { sni } from './network'
|
||||
import { hasCookie } from 'cookies-next/client'
|
||||
import { JSX } from 'react'
|
||||
|
||||
import ConstructionIcon from '@mui/icons-material/Construction'
|
||||
import FlagIcon from '@mui/icons-material/Flag'
|
||||
|
||||
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 const SnAccountBadgeMapping: Record<string, { icon: JSX.Element; name: string }> = {
|
||||
'company.staff': {
|
||||
icon: <ConstructionIcon />,
|
||||
name: 'Solsynth Staff',
|
||||
},
|
||||
'site.migration': {
|
||||
icon: <FlagIcon />,
|
||||
name: 'Solar Network Natives',
|
||||
},
|
||||
}
|
||||
|
||||
export interface UserStore {
|
||||
account: SnAccount | undefined
|
||||
fetchUser: () => Promise<SnAccount | undefined>
|
||||
}
|
||||
|
||||
export const useUserStore = create<UserStore>((set) => ({
|
||||
account: undefined,
|
||||
fetchUser: async (): Promise<SnAccount | undefined> => {
|
||||
if (!hasCookie('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
|
||||
}
|
||||
},
|
||||
}))
|
38
src/styles/globals.css
Normal file
@ -0,0 +1,38 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.textmarker-effect {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.textmarker-effect:before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
content: '';
|
||||
background: var(--mui-palette-primary-main);
|
||||
height: 20px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 0%;
|
||||
opacity: 0.5;
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.textmarker-effect:hover:before {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.textmarker-effect.active:before {
|
||||
animation: textmarker-effect-animation 0.5s ease-in-out;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes textmarker-effect-animation {
|
||||
0% {
|
||||
width: 0;
|
||||
}
|
||||
100% {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require('@tailwindcss/typography'), require('daisyui')],
|
||||
daisyui: {
|
||||
themes: [
|
||||
{
|
||||
light: {
|
||||
primary: '#3f51b5',
|
||||
secondary: '#4ba6ee',
|
||||
accent: '#03a9f4',
|
||||
neutral: '#4b5563',
|
||||
'base-100': '#ffffff',
|
||||
info: '#4994ec',
|
||||
success: '#67ad5b',
|
||||
warning: '#f5c344',
|
||||
error: '#e15241',
|
||||
},
|
||||
},
|
||||
{
|
||||
dark: {
|
||||
primary: '#3f51b5',
|
||||
secondary: '#4ba6ee',
|
||||
accent: '#03a9f4',
|
||||
neutral: '#1f2937',
|
||||
'base-100': '#000011',
|
||||
info: '#4994ec',
|
||||
success: '#67ad5b',
|
||||
warning: '#f5c344',
|
||||
error: '#e15241',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
18
tailwind.config.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
export default {
|
||||
content: [
|
||||
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: 'var(--background)',
|
||||
foreground: 'var(--foreground)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('@tailwindcss/typography')],
|
||||
} satisfies Config
|
@ -1,17 +1,22 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [
|
||||
".astro/types.d.ts",
|
||||
"**/*",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.astro"
|
||||
],
|
||||
"exclude": ["dist"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|