Compare commits
16 Commits
4682aa000f
...
archive/ne
Author | SHA1 | Date | |
---|---|---|---|
|
654d5fb191 | ||
|
eafb99c1cb | ||
|
5718faea5d | ||
|
bf5fb6f259 | ||
|
b65a87e38b | ||
|
05de2e1799 | ||
|
f26d29e2f0 | ||
|
931c917ba7 | ||
|
5973c7f25e | ||
|
35cb92b322 | ||
|
8bfd19f7e3 | ||
|
80d3dac2b0 | ||
|
7600e3f93d | ||
|
dad48b7a60 | ||
|
5f07990ff2 | ||
|
102e14f643 |
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
12
.idea/Capital.iml
generated
Normal file
12
.idea/Capital.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
61
.idea/codeStyles/Project.xml
generated
Normal file
61
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,61 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<HTMLCodeStyleSettings>
|
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
</HTMLCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<VueCodeStyleSettings>
|
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||
</VueCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Vue">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
10
.idea/material_theme_project_new.xml
generated
Normal file
10
.idea/material_theme_project_new.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MaterialThemeProjectNewConfig">
|
||||
<option name="metadata">
|
||||
<MTProjectMetadataState>
|
||||
<option name="userId" value="14beee28:194cb09ea37:-7ffd" />
|
||||
</MTProjectMetadataState>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/Capital.iml" filepath="$PROJECT_DIR$/.idea/Capital.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"Testflight"
|
||||
]
|
||||
}
|
7
next-i18next.config.js
Normal file
7
next-i18next.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/** @type {import('next-i18next').UserConfig} */
|
||||
module.exports = {
|
||||
i18n: {
|
||||
defaultLocale: 'en-US',
|
||||
locales: ['en-US', 'zh-CN'],
|
||||
},
|
||||
}
|
@@ -1,7 +1,10 @@
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
import { i18n } from './next-i18next.config'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
i18n,
|
||||
reactStrictMode: true,
|
||||
output: 'standalone',
|
||||
generateBuildId: async () => {
|
||||
|
17
package.json
17
package.json
@@ -19,6 +19,7 @@
|
||||
"@mui/material": "^6.3.1",
|
||||
"@mui/material-nextjs": "^6.3.1",
|
||||
"@mui/x-charts": "^7.23.6",
|
||||
"@next/third-parties": "^15.1.6",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@toolpad/core": "^0.11.0",
|
||||
"animate.css": "^4.1.1",
|
||||
@@ -26,11 +27,15 @@
|
||||
"axios-case-converter": "^1.1.1",
|
||||
"cookies-next": "^5.0.2",
|
||||
"feed": "^4.2.2",
|
||||
"next": "^15.1.4",
|
||||
"i18next": "^24.2.2",
|
||||
"next": "^15.1.5",
|
||||
"next-i18next": "^15.4.2",
|
||||
"next-nprogress-bar": "^2.4.3",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-i18next": "^15.4.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
@@ -43,18 +48,20 @@
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.3",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@types/node": "^20.17.12",
|
||||
"@types/react": "^19.0.4",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"daisyui": "^4.12.23",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-next": "15.1.3",
|
||||
"@eslint/eslintrc": "^3.2.0"
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@vercel/speed-insights",
|
||||
"core-js",
|
||||
"esbuild",
|
||||
"sharp"
|
||||
]
|
||||
|
16
public/locales/en-US/common.json
Normal file
16
public/locales/en-US/common.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"actionDownload": "Download",
|
||||
"actionLearnMore": "Learn more",
|
||||
"downloadPlatform": "Platform",
|
||||
"downloadDistribution": "Distribution",
|
||||
"downloadAppleStore": "iOS / macOS (App Store)",
|
||||
"downloadAppleTestflight": "iOS / macOS (TestFlight)",
|
||||
"downloadAndroid": "Android",
|
||||
"downloadWindows": "Windows",
|
||||
"downloadWeb": "Web",
|
||||
"downloadSourceCode": "Source Code",
|
||||
"downloadLinux": "Linux Unpacked",
|
||||
"downloadLinuxDebian": "deb (Debian/Ubuntu)",
|
||||
"actionOpen": "Open",
|
||||
"faq": "Frequently Asked Questions"
|
||||
}
|
35
public/locales/en-US/product-solar-network.json
Normal file
35
public/locales/en-US/product-solar-network.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"appName": "Solar Network",
|
||||
"appDescription": "The next generation Social Network platform.",
|
||||
"appSlogan": "Social Network, Redefined.",
|
||||
"faq1": "What's the relationship between Solar Network and Solian?",
|
||||
"faq1a": "Solian is the official app made for Solar Network. And the Solar Network is the official HyperNet instance hosted by Solsynth LLC. For simple, Solian is the app, and the Solar Network is the platform.",
|
||||
"faq2": "What's the relationship between Solar Network and HyperNet?",
|
||||
"faq2a": "HyperNet is the entire project including frontend app (also knowns as Solian for public) and the backend server. And the Solar Network is the official HyperNet instance which hosted and managed by Solsynth LLC who developed the HyperNet Project.",
|
||||
"faq3": "Which rules do I need to follow while using Solar Network?",
|
||||
"faq3a": "Check out our Terms & Conditions for a detailed explanation of what you can do and cannot do on Solar Network. If you violate any of these rules, we have the right to suspend or terminate your account., you can see them in the drawer.",
|
||||
"faq4": "If I have any question about Solar Network, where can I get help?",
|
||||
"faq4a": "Feel free to email as at lily@solsynth.dev",
|
||||
"ftDashboard": "Dashboard",
|
||||
"ftDashboardDescription": "Get what happened recently, all in one place.",
|
||||
"ftExplore": "Exploring",
|
||||
"ftExploreDescription": "Content you love without the ads or algorithms.",
|
||||
"ftChat": "Chat",
|
||||
"ftChatDescription": "Keep in touch with your friends and communities, across the world.",
|
||||
"ftNews": "News",
|
||||
"ftNewsDescription": "Stay up to date with the latest news and events.",
|
||||
"ftStickers": "Stickers",
|
||||
"ftStickersDescription": "Express your feelings better with the various stickers.",
|
||||
"ftPosting": "Posting",
|
||||
"ftPostingDescription": "Share your thoughts and ideas with the world. Without limits and censorship.",
|
||||
"ftPostingDescriptionAddition": "The Solar Network team will not impose any restrictions on the content you post, but according to our User Agreement, we may reduce or limit the public display of content that violates its rules.",
|
||||
"whatsMore": "What's more",
|
||||
"ftOpenSource": "Free, Transparent, Open-source",
|
||||
"ftOpenSourceDescription": "The code powered Solar Network is open-sourced under GPLv3 license, you can check the source code down below in the download section.",
|
||||
"ftSecurity": "Security",
|
||||
"ftSecurityDescription": "Solar Network has done a lot in terms of security. We use multi-factor authentication to protect your account, while being safe and convenient.",
|
||||
"ftNoCollecting": "No data collection",
|
||||
"ftNoCollectingDescription": "Solar Network does not collect any personal information for marketing or other purposes, nor does it sell it to third parties.",
|
||||
"noWaiting": "What are you waiting for?",
|
||||
"noWaitingDescription": "Join Solar Network today, by downloading the app or open it in your browser and create an account."
|
||||
}
|
16
public/locales/zh-CN/common.json
Normal file
16
public/locales/zh-CN/common.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"actionDownload": "下载",
|
||||
"actionLearnMore": "了解更多",
|
||||
"downloadPlatform": "平台",
|
||||
"downloadDistribution": "分发",
|
||||
"downloadAppleStore": "iOS / macOS (App Store)",
|
||||
"downloadAppleTestflight": "iOS / macOS (TestFlight)",
|
||||
"downloadAndroid": "安卓",
|
||||
"downloadWindows": "Windows",
|
||||
"downloadWeb": "网页版",
|
||||
"downloadSourceCode": "源代码",
|
||||
"downloadLinux": "Linux 未打包",
|
||||
"downloadLinuxDebian": "deb (Debian/Ubuntu)",
|
||||
"actionOpen": "打开",
|
||||
"faq": "常见问题"
|
||||
}
|
35
public/locales/zh-CN/product-solar-network.json
Normal file
35
public/locales/zh-CN/product-solar-network.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"appName": "Solar Network",
|
||||
"appDescription": "下一代社交网络平台",
|
||||
"appSlogan": "重新定义社交网络",
|
||||
"faq1": "Solar Network 和 Solian 之间有什么关系?",
|
||||
"faq1a": "Solian 是为 Solar Network 制作的官方应用程序。而 Solar Network 是由 Solsynth LLC 托管的官方 HyperNet 实例。简单来说,Solian 是应用程序,而 Solar Network 是平台。",
|
||||
"faq2": "Solar Network 和 HyperNet 之间有什么关系?",
|
||||
"faq2a": "HyperNet 是整个项目,包括前端应用程序(公众也称 Solian)和后端服务器。而 Solar Network 是由开发 HyperNet 项目的 Solsynth LLC 托管和管理的官方 HyperNet 实例。",
|
||||
"faq3": "使用 Solar Network 时我需要遵守哪些规则?",
|
||||
"faq3a": "查看我们的条款和条件,详细了解您在 Solar Network 上可以做什么和不能做什么。如果您违反任何这些规则,我们有权暂停或终止您的帐户。您可以在抽屉中看到它们。",
|
||||
"faq4": "如果我对 Solar Network 有任何疑问,我可以在哪里获得帮助?",
|
||||
"faq4a": "你可以发邮件给我们的客户服务获取支持:lily@solsynth.dev",
|
||||
"ftDashboard": "冲浪板",
|
||||
"ftDashboardDescription": "在同一个地方,方便地了解最近发生了什么。",
|
||||
"ftExplore": "探索",
|
||||
"ftExploreDescription": "在没有广告或算法的干扰下欣赏你喜欢的内容。",
|
||||
"ftChat": "聊天",
|
||||
"ftChatDescription": "跨过地区的间隔,与你的朋友和社区进行保持联系。",
|
||||
"ftNews": "新闻",
|
||||
"ftNewsDescription": "不行千里,也能知晓天下事。",
|
||||
"ftStickers": "贴图 / 表情",
|
||||
"ftStickersDescription": "使用各种贴纸更好地表达您的感受。",
|
||||
"ftPosting": "撰写",
|
||||
"ftPostingDescription": "在没有限制和审查的环境,与世界分享你的想法和想法",
|
||||
"ftPostingDescriptionAddition": "Solar Network 团队不会对您发表的内容做任何限制,但是根据我们的《用户协议》,我们可能会对违反其规则的内容进行减少或限制公开展示。",
|
||||
"whatsMore": "还有更多",
|
||||
"ftOpenSource": "自由、透明、开源",
|
||||
"ftOpenSourceDescription": "驱动 Solar Network 的代码在 GPLv3 许可下开源,您可以在下面的下载区域查看源代码。",
|
||||
"ftSecurity": "安全",
|
||||
"ftSecurityDescription": "Solar Network 在安全性方面做了很多,我们采用多因子验证方式来保护你的帐号,同时具备安全和方便。",
|
||||
"ftNoCollecting": "不采集数据",
|
||||
"ftNoCollectingDescription": "Solar Network 不会收集任何个人信息用于营销或者其他目的,更不会出售给第三方。",
|
||||
"noWaiting": "你还在等待什么?",
|
||||
"noWaitingDescription": "通过下载 / 在浏览器中打开 Solian 并创建一个帐号,现在就加入 Solar Network 吧!"
|
||||
}
|
BIN
src/assets/products/solar-network/ft-chat.png
Normal file
BIN
src/assets/products/solar-network/ft-chat.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 441 KiB |
BIN
src/assets/products/solar-network/ft-dashboard.png
Normal file
BIN
src/assets/products/solar-network/ft-dashboard.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 787 KiB |
BIN
src/assets/products/solar-network/ft-explore.png
Normal file
BIN
src/assets/products/solar-network/ft-explore.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
BIN
src/assets/products/solar-network/ft-news.png
Normal file
BIN
src/assets/products/solar-network/ft-news.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 770 KiB |
BIN
src/assets/products/solar-network/ft-posting.png
Normal file
BIN
src/assets/products/solar-network/ft-posting.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 749 KiB |
BIN
src/assets/products/solar-network/ft-stickers.png
Normal file
BIN
src/assets/products/solar-network/ft-stickers.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 461 KiB |
@@ -8,6 +8,8 @@ import { useState } from 'react'
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import PasswordIcon from '@mui/icons-material/Password'
|
||||
import EmailIcon from '@mui/icons-material/Email'
|
||||
import PinIcon from '@mui/icons-material/Pin'
|
||||
import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive'
|
||||
|
||||
export function SnLoginRouter({
|
||||
ticket,
|
||||
@@ -18,8 +20,13 @@ export function SnLoginRouter({
|
||||
factorList: SnAuthFactor[]
|
||||
onNext: (val: SnAuthFactor) => void
|
||||
}) {
|
||||
const factorTypeIcons = [<PasswordIcon key="password-icon" />, <EmailIcon key="email-icon" />]
|
||||
const factorTypeLabels = ['Password', 'Email verification code']
|
||||
const factorTypeIcons = [
|
||||
<PasswordIcon key="password-icon" />,
|
||||
<EmailIcon key="email-icon" />,
|
||||
<PinIcon key="pin-icon" />,
|
||||
<NotificationsActiveIcon key="notification-icon" />,
|
||||
]
|
||||
const factorTypeLabels = ['Password', 'Email verification code', 'Time-based OTP', 'In-app verification code']
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
|
77
src/components/events/CountdownTimer.tsx
Normal file
77
src/components/events/CountdownTimer.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Noto_Serif_TC } from 'next/font/google'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export interface TimeDiff {
|
||||
days: number
|
||||
hours: number
|
||||
minutes: number
|
||||
seconds: number
|
||||
isCountdown: boolean
|
||||
}
|
||||
|
||||
const serifFont = Noto_Serif_TC({
|
||||
weight: ['400', '500', '700'],
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
export function CountdownTimer({ targetDate, onUpdate }: { targetDate: Date; onUpdate: (diff: TimeDiff) => void }) {
|
||||
const [timeDiff, setTimeDiff] = useState({
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isCountdown: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const updateTimeDiff = () => {
|
||||
const now = new Date()
|
||||
const diff = targetDate.getTime() - now.getTime()
|
||||
|
||||
const absDiff = Math.abs(diff)
|
||||
const isCountdown = diff > 0
|
||||
|
||||
const days = Math.floor(absDiff / (1000 * 60 * 60 * 24))
|
||||
const hours = Math.floor((absDiff / (1000 * 60 * 60)) % 24)
|
||||
const minutes = Math.floor((absDiff / (1000 * 60)) % 60)
|
||||
const seconds = Math.floor((absDiff / 1000) % 60)
|
||||
|
||||
setTimeDiff({ days, hours, minutes, seconds, isCountdown })
|
||||
onUpdate({ days, hours, minutes, seconds, isCountdown })
|
||||
}
|
||||
|
||||
const intervalId = setInterval(updateTimeDiff, 1000)
|
||||
|
||||
return () => clearInterval(intervalId)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex gap-5">
|
||||
<div>
|
||||
<span className="countdown font-mono text-4xl">
|
||||
<span style={{ '--value': timeDiff.days } as any}></span>
|
||||
</span>
|
||||
<span className={serifFont.className}>天</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="countdown font-mono text-4xl">
|
||||
<span style={{ '--value': timeDiff.hours } as any}></span>
|
||||
</span>
|
||||
<span className={serifFont.className}>小时</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="countdown font-mono text-4xl">
|
||||
<span style={{ '--value': timeDiff.minutes } as any}></span>
|
||||
</span>
|
||||
<span className={serifFont.className}>分钟</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="countdown font-mono text-4xl">
|
||||
<span style={{ '--value': timeDiff.seconds } as any}></span>
|
||||
</span>
|
||||
<span className={serifFont.className}>秒</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
import '@/styles/globals.css'
|
||||
|
||||
import type { AppProps } from 'next/app'
|
||||
import { Box, createTheme, CssBaseline, ThemeProvider } from '@mui/material'
|
||||
import { Roboto } from 'next/font/google'
|
||||
@@ -7,6 +8,7 @@ import { PagesProgressBar as ProgressBar } from 'next-nprogress-bar'
|
||||
import { AppProvider } from '@toolpad/core/nextjs'
|
||||
import { useUserStore } from 'solar-js-sdk'
|
||||
import { useEffect } from 'react'
|
||||
import { appWithTranslation } from 'next-i18next'
|
||||
import Head from 'next/head'
|
||||
|
||||
const fontRoboto = Roboto({
|
||||
@@ -31,7 +33,7 @@ const siteTheme = createTheme({
|
||||
},
|
||||
})
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
function App({ Component, pageProps }: AppProps) {
|
||||
const userStore = useUserStore()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -80,3 +82,5 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default appWithTranslation(App)
|
@@ -11,6 +11,11 @@ export default function Document(props: DocumentProps & DocumentHeadTagsProps) {
|
||||
<AppCacheProvider {...props}>
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<script
|
||||
defer
|
||||
src="https://cloud.umami.is/script.js"
|
||||
data-website-id="eef151fb-07e2-461b-8b7f-2547aab735d4"
|
||||
></script>
|
||||
<DocumentHeadTags {...props} />
|
||||
</Head>
|
||||
<body className="antialiased">
|
||||
|
@@ -6,6 +6,7 @@ import { useUserStore } from 'solar-js-sdk'
|
||||
import { Box, Container, Typography } from '@mui/material'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
|
||||
export default function Login() {
|
||||
const [period, setPeriod] = useState<number>(0)
|
||||
@@ -14,17 +15,12 @@ export default function Login() {
|
||||
const [factor, setFactor] = useState<SnAuthFactor | null>(null)
|
||||
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
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()
|
||||
}
|
||||
|
||||
const redirectUrl = searchParams.get('redirect_uri')
|
||||
if (redirectUrl) {
|
||||
if (redirectUrl.startsWith('/')) {
|
||||
router.push(redirectUrl)
|
||||
} else {
|
||||
|
34
src/pages/events/2025-lunar-countdown.tsx
Normal file
34
src/pages/events/2025-lunar-countdown.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { CountdownTimer } from '@/components/events/CountdownTimer'
|
||||
import { Box, Container, Typography } from '@mui/material'
|
||||
import { Noto_Serif_TC } from 'next/font/google'
|
||||
import { useState } from 'react'
|
||||
|
||||
const serifFont = Noto_Serif_TC({
|
||||
weight: ['400', '500', '700'],
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
export default function LunarCountdownFor2025() {
|
||||
const [isCountdown, setIsCountdown] = useState(true)
|
||||
|
||||
return (
|
||||
<Container sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: 'calc(100vh - 64px)' }}>
|
||||
<Box>
|
||||
<Typography style={serifFont.style} sx={{ textAlign: 'center' }}>
|
||||
距离
|
||||
</Typography>
|
||||
<Typography variant="h5" style={serifFont.style} sx={{ textAlign: 'center', fontWeight: 'bold' }}>
|
||||
二〇二五乙巳年
|
||||
</Typography>
|
||||
<Typography style={serifFont.style} sx={{ textAlign: 'center', mb: 3 }}>
|
||||
{isCountdown ? '还有' : '已经'}
|
||||
</Typography>
|
||||
<CountdownTimer
|
||||
targetDate={new Date('2025-01-29T00:00:00')}
|
||||
onUpdate={({ isCountdown }) => setIsCountdown(isCountdown)}
|
||||
/>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { Container, Box, Typography, CircularProgress, Alert, Collapse } from '@mui/material'
|
||||
import { useRouter } from 'next/router'
|
||||
@@ -6,16 +8,18 @@ import { useEffect, useState } from 'react'
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
|
||||
import 'animate.css'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
|
||||
export default function AccountConfirm() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
async function confirm() {
|
||||
try {
|
||||
await sni.post('/cgi/id/users/me/confirm', {
|
||||
code: router.query['code'] as string,
|
||||
code: searchParams.get('code'),
|
||||
})
|
||||
router.push('/')
|
||||
} catch (err: any) {
|
||||
@@ -25,7 +29,7 @@ export default function AccountConfirm() {
|
||||
|
||||
useEffect(() => {
|
||||
confirm()
|
||||
}, [])
|
||||
}, [searchParams])
|
||||
|
||||
return (
|
||||
<Container
|
||||
|
@@ -1,12 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { Container, Box, Typography, Alert, Collapse, Button } from '@mui/material'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
|
||||
export default function AccountDeletion() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
@@ -15,7 +19,7 @@ export default function AccountDeletion() {
|
||||
try {
|
||||
setBusy(true)
|
||||
await sni.patch('/cgi/id/users/me/deletion', {
|
||||
code: router.query['code'] as string,
|
||||
code: searchParams.get('code'),
|
||||
})
|
||||
router.push('/')
|
||||
} catch (err: any) {
|
||||
|
@@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { Container, Box, Typography, Alert, Collapse, Button, TextField } from '@mui/material'
|
||||
import { useRouter } from 'next/router'
|
||||
@@ -5,6 +7,7 @@ import { useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
|
||||
export type SnResetPasswordForm = {
|
||||
password: string
|
||||
@@ -12,6 +15,7 @@ export type SnResetPasswordForm = {
|
||||
|
||||
export default function AccountPasswordReset() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const { handleSubmit, register } = useForm<SnResetPasswordForm>()
|
||||
|
||||
@@ -22,7 +26,7 @@ export default function AccountPasswordReset() {
|
||||
try {
|
||||
setBusy(true)
|
||||
await sni.patch('/cgi/id/users/me/password-reset', {
|
||||
code: router.query['code'] as string,
|
||||
code: searchParams.get('code'),
|
||||
new_password: data.password,
|
||||
})
|
||||
router.push('/')
|
||||
|
118
src/pages/orders/[id].tsx
Normal file
118
src/pages/orders/[id].tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Box, Typography, Container, Button, TextField, Collapse, Alert } from '@mui/material'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
import { EventHandler, FormEvent, FormEventHandler, useEffect, useState } from 'react'
|
||||
import { checkAuthenticatedClient, redirectToLogin, sni } from 'solar-js-sdk'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import PriceCheckIcon from '@mui/icons-material/PriceCheck'
|
||||
|
||||
type SnOrder = any
|
||||
|
||||
export const getServerSideProps = (async (context) => {
|
||||
const id = context.params!.id
|
||||
try {
|
||||
const { data: order } = await sni.get<SnOrder>('/cgi/wa/orders/' + id)
|
||||
return { props: { order, title: `Order #${order.id}` } }
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}) satisfies GetServerSideProps<{ order: SnOrder }>
|
||||
|
||||
export default function Post({ order }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
useEffect(() => {
|
||||
if (!checkAuthenticatedClient()) redirectToLogin()
|
||||
}, [])
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [password, setPassword] = useState<string>('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [paid, setPaid] = useState(false)
|
||||
const [canceled, setCanceled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (order?.status === 1) {
|
||||
setPaid(true)
|
||||
} else if (order?.status === 2) {
|
||||
setCanceled(true)
|
||||
}
|
||||
}, [order])
|
||||
|
||||
async function confirmPayment() {
|
||||
try {
|
||||
setBusy(true)
|
||||
await sni.post('/cgi/wa/orders/' + order.id + '/pay', {
|
||||
wallet_password: password,
|
||||
})
|
||||
setPaid(true)
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
height: 'calc(100vh - 64px)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<Box
|
||||
component="form"
|
||||
sx={{ width: '100%' }}
|
||||
onSubmit={(evt) => {
|
||||
evt.preventDefault()
|
||||
confirmPayment()
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" component="h1" gutterBottom>
|
||||
Order <code>#{order.id.toString().padStart(8, '0')}</code>
|
||||
</Typography>
|
||||
<Typography variant="body1" component="h2" gutterBottom>
|
||||
{order.remark}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" fontSize={32} pt={2} fontFamily={'monospace'} gutterBottom>
|
||||
{order.amount} SRC
|
||||
</Typography>
|
||||
|
||||
<Collapse in={!!error}>
|
||||
<Alert sx={{ mt: 3, width: '100%' }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<Box sx={{ my: 3, flexDirection: 'column', display: 'flex', gap: 2 }}>
|
||||
{paid || canceled ? (
|
||||
canceled ? (
|
||||
<Typography textAlign="center">Canceled, you are not able to pay this order any more</Typography>
|
||||
) : (
|
||||
<Typography textAlign="center">Paid, you can return to the seller now</Typography>
|
||||
)
|
||||
) : (
|
||||
<TextField
|
||||
label="Wallet Password"
|
||||
variant="outlined"
|
||||
type="password"
|
||||
onInput={(evt) => setPassword((evt.target as HTMLInputElement).value)}
|
||||
/>
|
||||
)}
|
||||
<Button type="submit" variant="contained" startIcon={<PriceCheckIcon />} disabled={busy || paid}>
|
||||
Pay
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="caption" sx={{ opacity: 0.75 }}>
|
||||
Powered by HyperNet.Wallet
|
||||
</Typography>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@@ -12,9 +12,13 @@ import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Grid2 as Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
} from '@mui/material'
|
||||
import { JSX } from 'react'
|
||||
import { Roboto_Serif } from 'next/font/google'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import Image from 'next/image'
|
||||
import NextLink from 'next/link'
|
||||
|
||||
@@ -26,11 +30,24 @@ 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 SearchIcon from '@mui/icons-material/Search'
|
||||
import GitHubIcon from '@mui/icons-material/GitHub'
|
||||
import SecurityIcon from '@mui/icons-material/Security'
|
||||
import CookieIcon from '@mui/icons-material/Cookie'
|
||||
import ComputerIcon from '@mui/icons-material/Computer';
|
||||
|
||||
import ImgSolarNetworkIcon from '@/assets/products/solar-network/icon.png'
|
||||
import ImgSolarNetworkAlpha from '@/assets/products/solar-network/alpha.webp'
|
||||
|
||||
import ImgFtDashboard from '@/assets/products/solar-network/ft-dashboard.png'
|
||||
import ImgFtExplore from '@/assets/products/solar-network/ft-explore.png'
|
||||
import ImgFtChat from '@/assets/products/solar-network/ft-chat.png'
|
||||
import ImgFtNews from '@/assets/products/solar-network/ft-news.png'
|
||||
import ImgFtStickers from '@/assets/products/solar-network/ft-stickers.png'
|
||||
import ImgFtPosting from '@/assets/products/solar-network/ft-posting.png'
|
||||
|
||||
import 'animate.css'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
interface DownloadableAsset {
|
||||
icon: JSX.Element
|
||||
@@ -51,68 +68,79 @@ const fontSerif = Roboto_Serif({
|
||||
style: 'italic',
|
||||
})
|
||||
|
||||
export async function getStaticProps() {
|
||||
export async function getStaticProps({ locale }: { locale: string }) {
|
||||
return {
|
||||
props: {
|
||||
title: 'Solar Network',
|
||||
...(await serverSideTranslations(locale, ['common', 'product-solar-network'])),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function ProductSolarNetwork() {
|
||||
const { t: ct } = useTranslation('common')
|
||||
const { t } = useTranslation('product-solar-network')
|
||||
|
||||
const downloadableAssets: DownloadableAsset[] = [
|
||||
{
|
||||
icon: <AppleIcon />,
|
||||
title: 'iOS / macOS (App Store)',
|
||||
title: ct('downloadAppleStore'),
|
||||
href: 'https://apps.apple.com/us/app/solian/id6499032345?itscg=30200&itsct=apps_box_link&mttnsubad=6499032345',
|
||||
},
|
||||
{
|
||||
icon: <AppleIcon />,
|
||||
title: 'iOS / macOS (TestFlight)',
|
||||
title: ct('downloadAppleTestflight'),
|
||||
href: 'https://testflight.apple.com/join/YJ0lmN6O',
|
||||
},
|
||||
{
|
||||
icon: <AndroidIcon />,
|
||||
title: 'Android',
|
||||
href: 'https://files.solsynth.dev/production01/solian/app-arm64-v8a-release.apk',
|
||||
title: ct('downloadAndroid'),
|
||||
href: 'https://github.com/Solsynth/HyperNet.Surface/releases/latest',
|
||||
},
|
||||
{
|
||||
icon: <WindowIcon />,
|
||||
title: 'Windows',
|
||||
href: 'https://files.solsynth.dev/production01/solian/windows-x86_64-release.zip',
|
||||
title: ct('downloadWindows'),
|
||||
href: 'https://github.com/Solsynth/HyperNet.Surface/releases/latest',
|
||||
},
|
||||
{
|
||||
icon: <ComputerIcon />,
|
||||
title: ct('downloadLinux'),
|
||||
href: 'https://github.com/Solsynth/HyperNet.Surface/releases/latest',
|
||||
},
|
||||
{
|
||||
icon: <ComputerIcon />,
|
||||
title: ct('downloadLinuxDebian'),
|
||||
href: 'https://github.com/Solsynth/HyperNet.Surface/releases/latest',
|
||||
},
|
||||
{
|
||||
icon: <WebIcon />,
|
||||
title: 'Web',
|
||||
title: ct('downloadWeb'),
|
||||
href: 'https://sn.solsynth.dev',
|
||||
open: true,
|
||||
},
|
||||
{
|
||||
icon: <CodeIcon />,
|
||||
title: 'Source Code',
|
||||
title: ct('downloadSourceCode'),
|
||||
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: t('faq1'),
|
||||
answer: t('faq1a'),
|
||||
},
|
||||
{
|
||||
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: t('faq2'),
|
||||
answer: t('faq2a'),
|
||||
},
|
||||
{
|
||||
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: t('faq3'),
|
||||
answer: t('faq3a'),
|
||||
},
|
||||
{
|
||||
question: 'If I have any question about Solar Network, where can I get help?',
|
||||
answer: 'Feel free to email as at lily@solsynth.dev',
|
||||
question: t('faq4'),
|
||||
answer: t('faq4a'),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -129,7 +157,7 @@ export default function ProductSolarNetwork() {
|
||||
/>
|
||||
<Box position="relative" width="fit-content" className="animate__animated animate__fadeInUp">
|
||||
<Typography variant="h4" component="h1">
|
||||
Solar Network
|
||||
{t('appName')}
|
||||
</Typography>
|
||||
<Box
|
||||
position="absolute"
|
||||
@@ -139,14 +167,14 @@ export default function ProductSolarNetwork() {
|
||||
className="animate__animated animate__pulse animate__infinite"
|
||||
>
|
||||
<Chip
|
||||
label="2.0"
|
||||
label="2.2"
|
||||
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.
|
||||
{t('appDescription')}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
@@ -156,29 +184,218 @@ export default function ProductSolarNetwork() {
|
||||
sx={{ mt: 2.5, width: 'fit-content', fontStyle: 'italic' }}
|
||||
className="textmarker-effect animate__animated animate__fadeInUp"
|
||||
>
|
||||
Social Network, Redefined.
|
||||
{t('appSlogan')}
|
||||
</Typography>
|
||||
|
||||
<Link href="#download" sx={{ my: 2.5 }}>
|
||||
Download <DownloadIcon sx={{ fontSize: 15, marginLeft: 0.5 }} />
|
||||
</Link>
|
||||
<Box display="flex" gap={2}>
|
||||
<Link href="#features" sx={{ my: 2.5 }}>
|
||||
{ct('actionLearnMore')} <SearchIcon sx={{ fontSize: 15, marginLeft: 0.5 }} />
|
||||
</Link>
|
||||
<Link href="#download" sx={{ my: 2.5 }}>
|
||||
{ct('actionDownload')} <DownloadIcon sx={{ fontSize: 15, marginLeft: 0.5 }} />
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
<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="features" display="flex" flexDirection="column" gap={12}>
|
||||
<Grid container columns={{ xs: 1, md: 2 }} spacing={4} alignItems="center">
|
||||
<Grid size={1}>
|
||||
<Typography variant="h5" component="h3" fontWeight="bold" gutterBottom>
|
||||
{t('ftDashboard')}
|
||||
</Typography>
|
||||
<Typography variant="body1" fontSize={18}>
|
||||
{t('ftDashboardDescription')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={1}>
|
||||
<Box position="relative" borderRadius="4px" width="100%" sx={{ aspectRatio: 16 / 9 }} className="shadow-xl">
|
||||
<Image
|
||||
src={ImgFtDashboard}
|
||||
alt="solar network dashboard"
|
||||
fill
|
||||
style={{ objectFit: 'cover', borderRadius: '8px' }}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container columns={{ xs: 1, md: 2 }} spacing={4} alignItems="center">
|
||||
<Grid size={1}>
|
||||
<Box position="relative" borderRadius="4px" width="100%" sx={{ aspectRatio: 16 / 9 }} className="shadow-xl">
|
||||
<Image
|
||||
src={ImgFtExplore}
|
||||
alt="solar network explore"
|
||||
fill
|
||||
style={{ objectFit: 'cover', borderRadius: '8px' }}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={1} textAlign={{ xs: 'left', md: 'right' }} order={{ xs: -1, md: 1 }}>
|
||||
<Typography variant="h5" component="h3" fontWeight="bold" gutterBottom>
|
||||
{t('ftExplore')}
|
||||
</Typography>
|
||||
<Typography variant="body1" fontSize={18}>
|
||||
{t('ftExploreDescription')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container columns={{ xs: 1, md: 2 }} spacing={4} alignItems="center">
|
||||
<Grid size={1}>
|
||||
<Typography variant="h5" component="h3" fontWeight="bold" gutterBottom>
|
||||
{t('ftChat')}
|
||||
</Typography>
|
||||
<Typography variant="body1" fontSize={18}>
|
||||
{t('ftChatDescription')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={1}>
|
||||
<Box position="relative" borderRadius="4px" width="100%" sx={{ aspectRatio: 16 / 9 }} className="shadow-xl">
|
||||
<Image
|
||||
src={ImgFtChat}
|
||||
alt="solar network chat"
|
||||
fill
|
||||
style={{ objectFit: 'cover', borderRadius: '8px' }}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container columns={{ xs: 1, md: 2 }} spacing={4} alignItems="center">
|
||||
<Grid size={1}>
|
||||
<Box position="relative" borderRadius="4px" width="100%" sx={{ aspectRatio: 16 / 9 }} className="shadow-xl">
|
||||
<Image
|
||||
src={ImgFtNews}
|
||||
alt="solar network news"
|
||||
fill
|
||||
style={{ objectFit: 'cover', borderRadius: '8px' }}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={1} textAlign={{ xs: 'left', md: 'right' }} order={{ xs: -1, md: 1 }}>
|
||||
<Typography variant="h5" component="h3" fontWeight="bold" gutterBottom>
|
||||
{t('ftNews')}
|
||||
</Typography>
|
||||
<Typography variant="body1" fontSize={18}>
|
||||
{t('ftNewsDescription')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container columns={{ xs: 1, md: 2 }} spacing={4} alignItems="center">
|
||||
<Grid size={1}>
|
||||
<Typography variant="h5" component="h3" fontWeight="bold" gutterBottom>
|
||||
{t('ftStickers')}
|
||||
</Typography>
|
||||
<Typography variant="body1" fontSize={18}>
|
||||
{t('ftStickersDescription')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={1}>
|
||||
<Box position="relative" borderRadius="4px" width="100%" sx={{ aspectRatio: 16 / 9 }} className="shadow-xl">
|
||||
<Image
|
||||
src={ImgFtStickers}
|
||||
alt="solar network stickers"
|
||||
fill
|
||||
style={{ objectFit: 'cover', borderRadius: '8px' }}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container columns={{ xs: 1, md: 2 }} spacing={4} alignItems="center">
|
||||
<Grid size={1}>
|
||||
<Box position="relative" borderRadius="4px" width="100%" sx={{ aspectRatio: 16 / 9 }} className="shadow-xl">
|
||||
<Image
|
||||
src={ImgFtPosting}
|
||||
alt="solar network posting"
|
||||
fill
|
||||
style={{ objectFit: 'cover', borderRadius: '8px' }}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={1} textAlign={{ xs: 'left', md: 'right' }} order={{ xs: -1, md: 1 }}>
|
||||
<Typography variant="h5" component="h3" fontWeight="bold" gutterBottom>
|
||||
{t('ftPosting')}
|
||||
</Typography>
|
||||
<Typography variant="body1" fontSize={18} gutterBottom>
|
||||
{t('ftPostingDescription')}
|
||||
</Typography>
|
||||
<Typography variant="caption">*{t('ftPostingDescriptionAddition')}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Box>
|
||||
<Typography variant="h5" component="h2" textAlign="center" sx={{ my: 5 }}>
|
||||
{t('whatsMore')}
|
||||
</Typography>
|
||||
|
||||
<Grid container columns={{ xs: 1, sm: 2, md: 3 }} spacing={4}>
|
||||
<Grid size={1}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<GitHubIcon sx={{ fontSize: 40, mb: 1 }} />
|
||||
<Typography variant="h6" component="h5" gutterBottom>
|
||||
{t('ftOpenSource')}
|
||||
</Typography>
|
||||
<Typography variant="body2">{t('ftOpenSourceDescription')}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={1}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<SecurityIcon sx={{ fontSize: 40, mb: 1 }} />
|
||||
<Typography variant="h6" component="h5" gutterBottom>
|
||||
{t('ftSecurity')}
|
||||
</Typography>
|
||||
<Typography variant="body2">{t('ftSecurityDescription')}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={1}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<CookieIcon sx={{ fontSize: 40, mb: 1 }} />
|
||||
<Typography variant="h6" component="h5" gutterBottom>
|
||||
{t('ftNoCollecting')}
|
||||
</Typography>
|
||||
<Typography variant="body2">{t('ftNoCollectingDescription')}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Box textAlign="center">
|
||||
<Typography variant="h5" component="h2" fontWeight="bold" sx={{ mt: 5 }} gutterBottom>
|
||||
{t('noWaiting')}
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||
{t('noWaitingDescription')}
|
||||
</Typography>
|
||||
|
||||
<Link href="#download" sx={{ my: 2.5 }}>
|
||||
{ct('actionDownload')} <DownloadIcon sx={{ fontSize: 15, marginLeft: 0.5 }} />
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box id="download">
|
||||
<Typography variant="h5" component="h2" textAlign="center" sx={{ mb: 5 }}>
|
||||
Download
|
||||
{ct('actionDownload')}
|
||||
</Typography>
|
||||
|
||||
<Table sx={{ maxWidth: '800px', marginX: 'auto' }} aria-label="download table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell />
|
||||
<TableCell>Platform</TableCell>
|
||||
<TableCell align="right">Distribution</TableCell>
|
||||
<TableCell>{ct('downloadPlatform')}</TableCell>
|
||||
<TableCell align="right">{ct('downloadDistribution')}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -190,12 +407,12 @@ export default function ProductSolarNetwork() {
|
||||
<NextLink passHref href={a.href} target="_blank">
|
||||
{a.open ? (
|
||||
<Link component="span">
|
||||
Open now
|
||||
{ct('actionOpen')}
|
||||
<LaunchIcon sx={{ fontSize: 15, marginLeft: 0.5 }} />
|
||||
</Link>
|
||||
) : (
|
||||
<Link component="span">
|
||||
Download now
|
||||
{ct('actionDownload')}
|
||||
<DownloadIcon sx={{ fontSize: 15, marginLeft: 0.5 }} />
|
||||
</Link>
|
||||
)}
|
||||
@@ -209,7 +426,7 @@ export default function ProductSolarNetwork() {
|
||||
|
||||
<Box id="faq">
|
||||
<Typography variant="h5" component="h2" textAlign="center" sx={{ mb: 5 }}>
|
||||
Frequently Asked Questions
|
||||
{ct('faq')}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ maxWidth: '800px', marginX: 'auto' }}>
|
||||
|
228
src/pages/realms/[id].tsx
Normal file
228
src/pages/realms/[id].tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Checkbox,
|
||||
Collapse,
|
||||
Container,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { checkAuthenticatedClient, getAttachmentUrl, redirectToLogin, sni, useUserStore } from 'solar-js-sdk'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
|
||||
import PublicIcon from '@mui/icons-material/Public'
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
|
||||
export const getServerSideProps = (async (context) => {
|
||||
const id = context.params!.id as string[]
|
||||
try {
|
||||
const { data: realm } = await sni.get<any>('/cgi/id/realms/' + id)
|
||||
return { props: { realm, title: `Realm ${realm.name} / Solar Network` } }
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
}) satisfies GetServerSideProps<{ realm: any }>
|
||||
|
||||
export default function Realm({ realm }: any) {
|
||||
useEffect(() => {
|
||||
if (!checkAuthenticatedClient()) redirectToLogin()
|
||||
}, [])
|
||||
|
||||
const user = useUserStore()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [joined, setJoined] = useState(false)
|
||||
|
||||
const [publicChannels, setPublicChannels] = useState<any[]>([])
|
||||
const [checkedChannels, setCheckedChannels] = useState<string[]>([])
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const isShare = useMemo(() => searchParams.has('share'), [searchParams])
|
||||
|
||||
function handleCheckChannel(value: string) {
|
||||
const currentIndex = checkedChannels.indexOf(value)
|
||||
const newChecked = [...checkedChannels]
|
||||
|
||||
if (currentIndex === -1) {
|
||||
newChecked.push(value)
|
||||
} else {
|
||||
newChecked.splice(currentIndex, 1)
|
||||
}
|
||||
|
||||
setCheckedChannels(newChecked)
|
||||
}
|
||||
|
||||
async function fetchPublicChannels() {
|
||||
try {
|
||||
const { data: channels } = await sni.get<any>('/cgi/im/channels/' + realm.alias)
|
||||
setPublicChannels(channels)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchPublicChannels()
|
||||
}, [realm])
|
||||
|
||||
async function joinRealm() {
|
||||
setLoading(true)
|
||||
try {
|
||||
await sni.post('/cgi/id/realms/' + realm.id + '/members', {
|
||||
related: user.account!.name,
|
||||
})
|
||||
setLoading(false)
|
||||
await joinChannels()
|
||||
setJoined(true)
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function joinChannels() {
|
||||
for (const chan of checkedChannels) {
|
||||
try {
|
||||
await sni.post('/cgi/im/channels/' + realm.alias + '/' + chan + '/members', {
|
||||
related: user.account!.name,
|
||||
})
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
height: '100vh',
|
||||
paddingTop: '64px',
|
||||
marginTop: '-64px',
|
||||
backgroundImage: `url(${getAttachmentUrl(realm.banner ?? '')})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="xs">
|
||||
<Card
|
||||
variant="outlined"
|
||||
className="backdrop-blur-lg"
|
||||
sx={{ backgroundColor: 'rgba(255, 255, 255, .2)', borderRadius: '16px' }}
|
||||
>
|
||||
<CardContent sx={{ px: 5, pt: 5, pb: 12 }}>
|
||||
<Avatar className="shadow-md" sx={{ width: 64, height: 64 }} src={getAttachmentUrl(realm.avatar ?? '')} />
|
||||
<Typography sx={{ mt: 3 }} variant="h5">
|
||||
{realm.name}
|
||||
</Typography>
|
||||
<Typography fontSize={13} fontFamily="monospace">
|
||||
@{realm.alias}
|
||||
</Typography>
|
||||
<Typography sx={{ mt: 3 }}>{realm.description}</Typography>
|
||||
|
||||
{isShare ? (
|
||||
<Box mt={3} mx="auto" width={256}>
|
||||
<QRCodeSVG
|
||||
title="Realm QR Code"
|
||||
value={'https://solsynth.dev/realms/' + realm.alias}
|
||||
level="H"
|
||||
bgColor="#00000000"
|
||||
fgColor={theme.palette.text.primary}
|
||||
size={256}
|
||||
/>
|
||||
|
||||
<Typography textAlign="center" mt={2}>
|
||||
Scan the QR Code above to join
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
{publicChannels.length > 0 && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography fontSize={14} mx={1} sx={{ opacity: 0.75, mb: 0.5, textAlign: 'center' }}>
|
||||
Public channels in this realm you can join
|
||||
</Typography>
|
||||
<List sx={{ width: '100%', p: 0, borderRadius: '8px', bgcolor: 'rgba(255, 255, 255, .2)' }}>
|
||||
{publicChannels.map((value) => {
|
||||
const labelId = `checkbox-list-label-${value}`
|
||||
|
||||
return (
|
||||
<ListItem key={value.id} disablePadding>
|
||||
<ListItemButton
|
||||
sx={{ borderRadius: '8px' }}
|
||||
onClick={() => handleCheckChannel(value.alias)}
|
||||
dense
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
edge="start"
|
||||
checked={checkedChannels.includes(value.alias)}
|
||||
tabIndex={-1}
|
||||
disableRipple
|
||||
inputProps={{ 'aria-labelledby': labelId }}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText id={labelId} primary={value.name} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{realm.isCommunity && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Box display="flex" sx={{ opacity: 0.75 }}>
|
||||
<PublicIcon />
|
||||
<Typography sx={{ ml: 1 }} variant="body2">
|
||||
A community realm, you can join it as you wish.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert sx={{ mt: 3 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
{joined ? (
|
||||
<Alert severity="info" sx={{ mt: 2.5 }}>
|
||||
Joined, check it out in the app
|
||||
</Alert>
|
||||
) : (
|
||||
<Button fullWidth variant="contained" disabled={loading} sx={{ mt: 3 }} onClick={joinRealm}>
|
||||
Join
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
}
|
@@ -57,6 +57,11 @@ export default function PrivacyPolicy() {
|
||||
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="the-branding-issue">The Impersonating Issue</h4>
|
||||
<p>
|
||||
You cannot impersonating us (the Solsynth LLC) in anyways. For example like using our logos, our product name,
|
||||
or our name. Otherwise we have the right to remove / suspend your account.
|
||||
</p>
|
||||
<h4 id="restriction-and-discontinuation">Restriction and Discontinuation</h4>
|
||||
<ul>
|
||||
<li>
|
||||
|
@@ -14,5 +14,5 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('@tailwindcss/typography')],
|
||||
plugins: [require('@tailwindcss/typography'), require('daisyui')],
|
||||
} satisfies Config
|
||||
|
@@ -17,6 +17,6 @@
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next-i18next.config.js"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
Reference in New Issue
Block a user