Compare commits
No commits in common. "master" and "archive/nextjs" have entirely different histories.
master
...
archive/ne
3
.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
18
.gitignore
vendored
@ -3,12 +3,8 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
@ -28,10 +24,9 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@ -39,3 +34,8 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
bun.lockb
|
||||
*.lock
|
||||
|
||||
.env
|
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
@ -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>
|
57
.idea/codeStyles/Project.xml
generated
Normal file
@ -0,0 +1,57 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<HTMLCodeStyleSettings>
|
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
</HTMLCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<VueCodeStyleSettings>
|
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||
</VueCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</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
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/Capital.iml" filepath="$PROJECT_DIR$/.idea/Capital.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
@ -1,7 +1,5 @@
|
||||
{
|
||||
"semi": false,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true
|
||||
"singleQuote": false,
|
||||
"printWidth": 120
|
||||
}
|
||||
|
13
.roadsignrc
@ -1,13 +0,0 @@
|
||||
{
|
||||
"sync": {
|
||||
"region": "capital",
|
||||
"configPath": "roadsign.toml"
|
||||
},
|
||||
"deployments": [
|
||||
{
|
||||
"region": "capital",
|
||||
"site": "capital-app",
|
||||
"path": ".next"
|
||||
}
|
||||
]
|
||||
}
|
36
README.md
Normal file
@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
17
app/console/[[...index]]/page.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* This route is responsible for the built-in authoring environment using Sanity Studio.
|
||||
* All routes under your studio path is handled by this file using Next.js' catch-all routes:
|
||||
* https://nextjs.org/docs/routing/dynamic-routes#catch-all-routes
|
||||
*
|
||||
* You can learn more about the next-sanity package here:
|
||||
* https://github.com/sanity-io/next-sanity
|
||||
*/
|
||||
|
||||
import { NextStudio } from 'next-sanity/studio'
|
||||
import config from '../../../sanity.config'
|
||||
|
||||
export default function StudioPage() {
|
||||
return <NextStudio config={config} />
|
||||
}
|
31
app/consts.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { ReactNode } from "react";
|
||||
import GitHubIcon from "@mui/icons-material/GitHub";
|
||||
import TwitterIcon from "@mui/icons-material/Twitter";
|
||||
import CoffeeIcon from "@mui/icons-material/Coffee";
|
||||
|
||||
export interface RelatedAccount {
|
||||
icon: ReactNode;
|
||||
platform: string;
|
||||
accountName: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export const SITE_NAME = "Goatshed";
|
||||
export const SITE_DESCRIPTION = "山羊寒舍,在这里最终智羊工作室的最新动态。";
|
||||
export const SITE_URL = "https://smartsheep.studio";
|
||||
|
||||
export const RELATED_ACCOUNTS: RelatedAccount[] = [
|
||||
{ icon: <GitHubIcon />, platform: "GitHub", accountName: "@smartsheep-hq", link: "https://github.com/smartsheep-hq" },
|
||||
{
|
||||
icon: <TwitterIcon />,
|
||||
platform: "Twitter",
|
||||
accountName: "@littlesheepovo",
|
||||
link: "https://twitter.com/littlesheepovo",
|
||||
},
|
||||
{
|
||||
icon: <CoffeeIcon />,
|
||||
platform: "Ko-fi",
|
||||
accountName: "@littlesheep2code",
|
||||
link: "https://ko-fi.com/littlesheep2code",
|
||||
},
|
||||
];
|
36
app/feed/route.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { Feed } from "feed";
|
||||
import { SITE_DESCRIPTION, SITE_NAME, SITE_URL } from "@/app/consts";
|
||||
import { client } from "@/sanity/lib/client";
|
||||
|
||||
export async function GET() {
|
||||
const feed = new Feed({
|
||||
title: SITE_NAME,
|
||||
description: SITE_DESCRIPTION,
|
||||
id: SITE_URL,
|
||||
link: SITE_URL,
|
||||
favicon: `${SITE_URL}/favicon.png`,
|
||||
feedLinks: { atom: `${SITE_URL}/feed` },
|
||||
language: "zh-CN",
|
||||
copyright: `Copyright © ${new Date().getFullYear()} SmartSheep Studio`,
|
||||
});
|
||||
|
||||
const posts = await client.fetch<any[]>(`*[_type == "post"] {
|
||||
title, description, slug, publishedAt,
|
||||
}`);
|
||||
|
||||
posts.forEach((item) => {
|
||||
feed.addItem({
|
||||
id: `${SITE_URL}/p/${item.slug.current}`,
|
||||
link: `${SITE_URL}/p/${item.slug.current}`,
|
||||
title: item.title,
|
||||
description: item.description ?? "No description yet.",
|
||||
date: new Date(item.publishedAt)
|
||||
});
|
||||
});
|
||||
|
||||
return new Response(feed.rss2(), {
|
||||
headers: {
|
||||
"content-type": "application/xml"
|
||||
}
|
||||
});
|
||||
}
|
7
app/globals.css
Normal file
@ -0,0 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.zoomist-wrapper {
|
||||
background: transparent !important;
|
||||
}
|
21
app/icon.svg
Executable file
@ -0,0 +1,21 @@
|
||||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
|
||||
<title>SmartSheep Logo</title>
|
||||
<defs>
|
||||
<image width="124" height="198" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHwAAADGCAMAAAAnkRSfAAAAAXNSR0IB2cksfwAAAq9QTFRFAAAA////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////J9QBRAAAAOV0Uk5TAA0WEiFfjMLMr40/BRdmtuz//vbQYAQmpfLdQAGovyURj/iOD0r07loDGJfbPDTf/bhd94QLdPrrVQLWcPmtHU31egknzehRg9Ev1xnzbgYHhfzmTMorSOSbFJloKsniReXDKG/7kQ6m8WMw3ue7I4cKNdTZOFTqsR5/Hzra0zZY7al4Ir5SPWHvnpbG4MRrlKJs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"/>
|
||||
<image width="122" height="142" id="img2" href="data:image/png;base64,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"/>
|
||||
</defs>
|
||||
<style>
|
||||
.s0 { fill: #ffffff;stroke: #000000;stroke-miterlimit:100;stroke-width: 56 }
|
||||
.s1 { fill: #4750a3;stroke: #000000;stroke-miterlimit:100;stroke-width: 56 }
|
||||
</style>
|
||||
<path id="Wool" fill-rule="evenodd" class="s0" d="m128 608.4c0 95.9 77.4 173.6 172.8 173.6h441.6c84.8 0 153.6-69.1 153.6-154.3 0-74.6-52.8-136.9-122.9-151.1 4.9-12.9 7.7-27 7.7-41.7 0-63.9-51.6-115.8-115.2-115.8-23.6 0-45.7 7.3-64 19.6-33.2-57.9-95.2-96.7-166.4-96.7-106.1 0-192 86.3-192 192.9 0 3.2 0.1 6.5 0.2 9.7-67.2 23.8-115.4 88.1-115.4 163.8z"/>
|
||||
<g id="Crystal">
|
||||
<path id="Crystal" class="s1" d="m699 224l138.6 80v160l-138.6 80-138.6-80v-160z"/>
|
||||
<use id="Highlight" href="#img1" x="688" y="255"/>
|
||||
</g>
|
||||
<g id="Horn">
|
||||
</g>
|
||||
<g id="Face">
|
||||
<use id="Slime" href="#img2" x="233" y="538"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.1 KiB |
49
app/information/[id]/community-guidelines.mdx
Normal file
@ -0,0 +1,49 @@
|
||||
export const metadata = {
|
||||
title: "社区准则"
|
||||
}
|
||||
|
||||
Welcome to our community! We are committed to fostering an open, friendly, and respectful environment to facilitate meaningful communication and shared experiences. Please adhere to the following community guidelines to ensure that every member can enjoy a positive and enriching community experience.
|
||||
|
||||
## 1. Respect Others
|
||||
|
||||
Ensure that your words and actions respect fellow community members. Avoid the use of insulting, discriminatory, or offensive language. Respect others' viewpoints, even if you disagree. Engage in discussions constructively to foster understanding and knowledge sharing.
|
||||
|
||||
## 2. Maintain Friendliness and Inclusivity
|
||||
|
||||
We welcome members from diverse cultures, backgrounds, and perspectives. Ensure that your interactions are friendly and inclusive. Refrain from posting offensive or discriminatory content. Help us create an environment that accommodates a variety of opinions and ideas.
|
||||
|
||||
## 3. Respect Privacy
|
||||
|
||||
Respect the privacy of other members. Avoid sharing others' personal information without their consent. If you need to share information related to others, ensure you have obtained their permission.
|
||||
|
||||
## 4. Inappropriate Content and Behavior
|
||||
|
||||
Refrain from posting any illegal, obscene, threatening, or otherwise inappropriate content. This includes but is not limited to explicit material, hate speech, false information, or any behavior that violates laws and ethical standards.
|
||||
|
||||
## 5. Provide Valuable Contributions
|
||||
|
||||
Share meaningful and valuable content within the community. Avoid posting unrelated or repetitive information. Maintain the quality of discussions and provide helpful insights and experiences for other members.
|
||||
|
||||
## 6. Respect Administrators and Moderators
|
||||
|
||||
Follow the guidance and rules provided by administrators and moderators. Their goal is to maintain community order and ensure a pleasant experience for every member. If you have any questions or concerns, feel free to contact them privately.
|
||||
|
||||
## 7. Respect Diverse Political Views
|
||||
|
||||
We encourage the respectful sharing of diverse political views. However, express your opinions in a way that respects others, avoiding offensive, provocative, or insulting language. Political discussions should be conducted constructively to promote understanding and information sharing.
|
||||
|
||||
## 8. Adhere to International Political Standards
|
||||
|
||||
Our community consists of members from around the world with different political beliefs. Understand and respect international political viewpoints, avoiding conflicts arising from political stances. Keep an open mind and be willing to listen and learn from diverse cultural and national perspectives.
|
||||
|
||||
## 9. Comply with Legal Regulations
|
||||
|
||||
Any form of illegal behavior is unacceptable in our community. Ensure that your words and actions comply with local and international laws, especially in matters related to politics. Prohibitions include incitement, advocating violence, or any other unlawful activities.
|
||||
|
||||
## 10. Avoid Improper Political Promotion
|
||||
|
||||
Avoid engaging in inappropriate political promotion within the community. The community is intended for constructive exchange, not as a platform for political propaganda. Respect the preferences of other members and refrain from forcefully advocating personal or group political stances.
|
||||
|
||||
We believe that by adhering to these guidelines, we can create an inclusive community rich in diverse perspectives. Violations may result in appropriate sanctions, and the specific consequences will depend on the severity and frequency of the violations.
|
||||
|
||||
Thank you for your cooperation in maintaining an open and friendly atmosphere in our community, providing an enjoyable communication experience for all members!
|
17
app/information/[id]/layout.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { Box, Card, CardContent, Container } from "@mui/material";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
|
||||
export default function LegalLayout({ children }: Readonly<{
|
||||
children: ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<Container sx={{ display: "flex", justifyContent: "center", gap: 4, py: 2 }}>
|
||||
<Card sx={{ flexGrow: 1, maxWidth: 720 }}>
|
||||
{children}
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
}
|
63
app/information/[id]/page.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { ReactNode } from "react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Box, CardContent, Divider, Typography } from "@mui/material";
|
||||
import UserAgreement from "@/app/information/[id]/user-agreement.mdx";
|
||||
import PrivacyPolicy from "@/app/information/[id]/privacy-policy.mdx";
|
||||
import CommunityGuidelines from "@/app/information/[id]/community-guidelines.mdx";
|
||||
|
||||
interface InfoMeta {
|
||||
title: string;
|
||||
content: ReactNode;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const INFO_DIRECTORY: { [id: string]: InfoMeta } = {
|
||||
"user-agreement": {
|
||||
title: "User Agreement",
|
||||
content: <UserAgreement />,
|
||||
updatedAt: new Date("2019-01-28 01:28")
|
||||
},
|
||||
"privacy-policy": {
|
||||
title: "Privacy Policy",
|
||||
content: <PrivacyPolicy />,
|
||||
updatedAt: new Date("2019-01-28 01:28")
|
||||
},
|
||||
"community-guidelines": {
|
||||
title: "Goatworks Community Guidelines",
|
||||
content: <CommunityGuidelines />,
|
||||
updatedAt: new Date("2019-01-28 01:28")
|
||||
}
|
||||
};
|
||||
|
||||
export async function generateMetadata({ params }: { params: { id: string } }) {
|
||||
const info = INFO_DIRECTORY[params.id];
|
||||
|
||||
return {
|
||||
title: info?.title,
|
||||
}
|
||||
}
|
||||
|
||||
export default function InfoPage({ params }: { params: { id: string } }) {
|
||||
const info = INFO_DIRECTORY[params.id];
|
||||
|
||||
if (!info) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<CardContent sx={{ paddingX: 5, paddingY: 3 }}>
|
||||
<Typography variant="h2">
|
||||
{info.title}
|
||||
</Typography>
|
||||
<Typography color="text.secondary" variant="body2" sx={{ mt: 0.25 }}>
|
||||
Last Updated At: {info.updatedAt.toLocaleString()}
|
||||
</Typography>
|
||||
|
||||
|
||||
<Divider sx={{ my: 2.5, mx: -5 }} />
|
||||
<Box className="prose max-w-none">
|
||||
{info.content}
|
||||
</Box>
|
||||
</CardContent>
|
||||
);
|
||||
}
|
52
app/information/[id]/privacy-policy.mdx
Normal file
@ -0,0 +1,52 @@
|
||||
export const metadata = {
|
||||
title: "隐私协议"
|
||||
}
|
||||
|
||||
SmartSheep Studio (referred to as "we," "our," or "the Studio") respects and protects your personal privacy. This Privacy Policy is intended to explain our practices regarding the collection, use, sharing, and protection of your personal information. Please read this Privacy Policy carefully to understand how we handle your personal information.
|
||||
|
||||
## 1. Information Collection
|
||||
|
||||
We may collect various types of information, including but not limited to:
|
||||
|
||||
* Personal identification information (e.g., name, address, email address, phone number, etc.)
|
||||
* Device information (e.g., IP address, operating system, browser type, etc.)
|
||||
* Usage data (e.g., access times, browsing history, clickstream data, etc.)
|
||||
|
||||
We may collect information through various methods, including:
|
||||
|
||||
* Information you provide (e.g., account registration, form submissions, etc.)
|
||||
* Automatically collected information (e.g., through Cookies, Web Beacons, etc.)
|
||||
|
||||
## 2. Information Use
|
||||
|
||||
We may use your personal information for various purposes, including:
|
||||
|
||||
* Providing requested products or services
|
||||
* Processing transactions and payments
|
||||
* Sending relevant notifications
|
||||
* Providing customer support and services
|
||||
* Improving our products and services
|
||||
|
||||
## 3. Information Sharing
|
||||
|
||||
We may share your personal information with third parties, including but not limited to:
|
||||
|
||||
* Business partners
|
||||
* Third-party service providers
|
||||
* When required by law or government agencies
|
||||
|
||||
## 4. Information Security
|
||||
|
||||
We will take reasonable security measures to protect your personal information from unauthorized access, use, or disclosure. However, please note that no method of transmission over the internet is 100% secure.
|
||||
|
||||
## 5. Changes to Privacy Policy
|
||||
|
||||
We reserve the right to modify this Privacy Policy at any time. The updated policy will be posted on our website and will become effective upon posting. Please check our Privacy Policy regularly for updates.
|
||||
|
||||
## 6. Contact Us
|
||||
|
||||
If you have any questions or concerns about our Privacy Policy, please contact us at:
|
||||
|
||||
alphabot@smartsheep.studio
|
||||
|
||||
Thank you for reading our Privacy Policy.
|
47
app/information/[id]/user-agreement.mdx
Normal file
@ -0,0 +1,47 @@
|
||||
export const metadata = {
|
||||
title: "用户协议"
|
||||
}
|
||||
|
||||
Please read this user agreement carefully before using our product/service. By using our product/service, you agree to comply with the following terms and conditions:
|
||||
|
||||
## 1. User Content
|
||||
|
||||
1.1 You are solely responsible for any text, images, audio, video, or other materials (collectively referred to as "User Content") you upload, publish, or share through our product/service.
|
||||
|
||||
1.2 You warrant that you are the lawful owner of all User Content you upload, or you have obtained all necessary authorizations and licenses to use and share such User Content on our product/service.
|
||||
|
||||
1.3 You agree not to upload, publish, or share any User Content that is illegal, infringing, obscene, threatening, defamatory, or otherwise violates applicable laws.
|
||||
|
||||
## 2. Community Guidelines and Service Termination
|
||||
|
||||
2.1 You must adhere to our community guidelines, which include but are not limited to [Goatworks Community Guidelines](/legal/community-guidelines).
|
||||
|
||||
2.2 Violation of the community guidelines may result in our sole discretion to delete, modify, or refuse acceptance of any User Content and may lead to termination or suspension of your access to our product/service.
|
||||
|
||||
2.3 We reserve the right to modify the community guidelines at any time, and you agree to comply with the latest community guidelines.
|
||||
|
||||
## 3. Legal Responsibility
|
||||
|
||||
3.1 We do not assume any responsibility for the legality, accuracy, completeness, or quality of User Content.
|
||||
|
||||
3.2 You agree to bear all legal responsibilities arising from your User Content, including but not limited to claims, lawsuits, losses, and expenses incurred by third parties.
|
||||
|
||||
3.3 We are not liable for any losses or damages caused by User Content, including but not limited to direct or indirect special, incidental, consequential, or punitive damages.
|
||||
|
||||
## 4. User Responsibilities
|
||||
|
||||
4.1 You agree to maintain the confidentiality of your account and password and assume full responsibility for all activities conducted through your account.
|
||||
|
||||
4.2 You agree to promptly notify us of any unauthorized use of your account or other security vulnerabilities.
|
||||
|
||||
## 5. Termination
|
||||
|
||||
We reserve the right to terminate or suspend your access to our product/service at any time without prior notice if we believe you have violated any provisions of this user agreement or community guidelines.
|
||||
|
||||
## 6. Contact Us
|
||||
|
||||
If you have any questions about this user agreement, please contact us at:
|
||||
|
||||
alphabot@smartsheep.studio
|
||||
|
||||
Thank you for reading and complying with our user agreement.
|
54
app/layout.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ReactNode } from "react";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import { AppRouterCacheProvider } from "@mui/material-nextjs/v13-appRouter";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
import { SITE_DESCRIPTION, SITE_NAME } from "@/app/consts";
|
||||
import { theme } from "@/app/theme";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
|
||||
import "@fontsource/roboto/300.css";
|
||||
import "@fontsource/roboto/400.css";
|
||||
import "@fontsource/roboto/500.css";
|
||||
import "@fontsource/roboto/700.css";
|
||||
|
||||
import "./globals.css";
|
||||
|
||||
import AppShell from "@/components/AppShell";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: SITE_NAME,
|
||||
template: `${SITE_NAME} | %s`
|
||||
},
|
||||
description: SITE_DESCRIPTION
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: Readonly<{
|
||||
children: ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body>
|
||||
<AppRouterCacheProvider>
|
||||
<CssBaseline />
|
||||
<SpeedInsights />
|
||||
<NextTopLoader color="#ffffff" showSpinner={false} />
|
||||
<ThemeProvider theme={theme}>
|
||||
<AppShell>{children}</AppShell>
|
||||
</ThemeProvider>
|
||||
</AppRouterCacheProvider>
|
||||
|
||||
<script
|
||||
async
|
||||
src="https://analytics.smartsheep.studio/script.js"
|
||||
data-website-id="bbe87bab-bd5b-416b-8767-b29088c75ab2"
|
||||
/>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
76
app/page.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
colors,
|
||||
Container,
|
||||
Grid,
|
||||
List,
|
||||
ListItemAvatar,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { RELATED_ACCOUNTS } from "@/app/consts";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<Container sx={{ scrollBehavior: "smooth", px: 5 }}>
|
||||
<Grid container id="introduce" alignItems="center" sx={{ height: "calc(100vh - 64px)" }}>
|
||||
<Grid item xs={12} sm={6} sx={{ textAlign: { xs: "center", sm: "initial" } }}>
|
||||
<Typography variant="h1" gutterBottom>
|
||||
你好呀 👋
|
||||
</Typography>
|
||||
<Typography paragraph>
|
||||
欢迎来到 SmartSheep Studio 的官方网站!在这里了解,订阅,跟踪我们的最新消息。
|
||||
接触我们最大的官方社区,并且尝试最新产品,参与各种活动,提供反馈,让我们更好的服务您。
|
||||
</Typography>
|
||||
<Button variant="contained" href="#about-us" size="large">
|
||||
探索更多
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
sm={6}
|
||||
sx={{ display: "flex", justifyContent: { xs: "center", sm: "end" }, order: { xs: -100, sm: 0 } }}
|
||||
>
|
||||
<Box>
|
||||
<Image src="/smartsheep.svg" alt="Logo" width={256} height={256} />
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container id="about-us" alignItems="center" sx={{ height: "calc(100vh - 64px)" }}>
|
||||
<Grid item xs={12} sm={6} sx={{ display: "flex", justifyContent: { xs: "center", sm: "end" } }}>
|
||||
<Card sx={{ flexGrow: 1, mr: { xs: 0, sm: 4, md: 8 } }}>
|
||||
<List sx={{ width: "100%", bgcolor: "background.paper" }}>
|
||||
{RELATED_ACCOUNTS.map((item, idx) => (
|
||||
<Link key={idx} href={item.link} target="_blank" passHref>
|
||||
<ListItemButton>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: colors.blueGrey[700] }}>{item.icon}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={item.platform} secondary={item.accountName} />
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
))}
|
||||
</List>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} sx={{ textAlign: { xs: "center", sm: "initial" } }}>
|
||||
<Typography variant="h1" gutterBottom>
|
||||
关于我们
|
||||
</Typography>
|
||||
<Typography paragraph>
|
||||
我们是一群充满活力、对开源充满热情的开发者。成立于 2019 年。自那年起我们一直在开发让人喜欢的开源软件。
|
||||
在我们这里,“取之于开源,用之于开源” 不仅是原则,更是我们信仰的座右铭。
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
67
app/posts/[id]/page.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { Box, Card, CardContent, CardMedia, Chip, Divider, Stack, Typography } from "@mui/material";
|
||||
import { client } from "@/sanity/lib/client";
|
||||
import PostContent from "@/components/posts/PostContent";
|
||||
import Image from "next/image";
|
||||
|
||||
export async function generateMetadata({ params }: { params: { id: string } }) {
|
||||
const post = await client.fetch<any>(`*[_type == "post" && slug.current == $slug][0] {
|
||||
title, description
|
||||
}`, { slug: params.id });
|
||||
|
||||
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.description
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PostDetailPage({ params }: { params: { id: string } }) {
|
||||
const post = await client.fetch<any>(`*[_type == "post" && slug.current == $slug][0] {
|
||||
title, description, slug, body, author, publishedAt,
|
||||
mainImage {
|
||||
asset -> {
|
||||
_id,
|
||||
url
|
||||
},
|
||||
alt
|
||||
},
|
||||
"categories": categories[]->title,
|
||||
"author_name": author->name,
|
||||
"author_image": author->image
|
||||
}`, { slug: params.id });
|
||||
|
||||
return (
|
||||
<Card>
|
||||
{
|
||||
post.mainImage &&
|
||||
<CardMedia sx={{ height: 360, position: "relative" }} title={post.mainImage.alt}>
|
||||
<Image
|
||||
fill
|
||||
src={post.mainImage.asset.url}
|
||||
alt={post.mainImage.alt}
|
||||
style={{ objectFit: "cover" }}
|
||||
/>
|
||||
</CardMedia>
|
||||
}
|
||||
|
||||
<CardContent sx={{ paddingX: 5, paddingY: 3 }}>
|
||||
<Box>
|
||||
<Typography variant="h2">
|
||||
{post.title}
|
||||
</Typography>
|
||||
|
||||
<Stack direction="row" sx={{ mx: -0.5, mt: 1, mb: 1.2 }}>
|
||||
{post.categories.map((category: string, idx: number) => <Chip size="small" label={category} key={idx} />)}
|
||||
</Stack>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{post.description ?? "No description yet."}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider sx={{ my: 2.5, mx: -5 }} />
|
||||
<Box component="article" className="prose max-w-none" sx={{ minWidth: 0 }}>
|
||||
<PostContent content={post.body} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
14
app/posts/layout.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { Box, Container } from "@mui/material";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export default function PostLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<Container sx={{ display: "flex", justifyContent: "center", gap: 4, py: 2 }}>
|
||||
<Box sx={{ flexGrow: 1, maxWidth: 720 }}>{children}</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
60
app/posts/page.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { Button, Card, CardActions, CardContent, CardMedia, Chip, Stack, Typography } from "@mui/material";
|
||||
import { client } from "@/sanity/lib/client";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export const metadata = {
|
||||
title: "博客"
|
||||
}
|
||||
|
||||
export default async function PostList() {
|
||||
const posts = await client.fetch<any[]>(`*[_type == "post"] {
|
||||
title, description, slug, author, publishedAt,
|
||||
mainImage {
|
||||
asset -> {
|
||||
_id,
|
||||
url
|
||||
},
|
||||
alt
|
||||
},
|
||||
"categories": categories[]->title,
|
||||
"author_name": author->name,
|
||||
"author_image": author->image
|
||||
}`);
|
||||
|
||||
return (
|
||||
posts.map((post) => (
|
||||
<Card key={post.slug.current} sx={{ width: "100%" }}>
|
||||
{
|
||||
post.mainImage &&
|
||||
<CardMedia sx={{ height: 160, position: "relative" }} title={post.mainImage.alt}>
|
||||
<Image
|
||||
fill
|
||||
src={post.mainImage.asset.url}
|
||||
alt={post.mainImage.alt}
|
||||
style={{ objectFit: "cover" }}
|
||||
/>
|
||||
</CardMedia>
|
||||
}
|
||||
|
||||
<CardContent sx={{ paddingX: 5, paddingY: 3 }}>
|
||||
<Typography variant="h3">
|
||||
{post.title}
|
||||
</Typography>
|
||||
|
||||
<Stack direction="row" sx={{ mx: -0.5, mt: 1, mb: 1.2 }}>
|
||||
{post.categories.map((category: string, idx: number) => <Chip size="small" label={category} key={idx} />)}
|
||||
</Stack>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{post.description ? post.description : "No description yet."}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions sx={{ paddingX: 4, paddingBottom: 2 }}>
|
||||
<Link href={`/p/${post.slug.current}`} passHref>
|
||||
<Button>Read more</Button>
|
||||
</Link>
|
||||
</CardActions>
|
||||
</Card>
|
||||
))
|
||||
);
|
||||
}
|
31
app/sitemap.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { MetadataRoute } from "next";
|
||||
import { SITE_URL } from "@/app/consts";
|
||||
import { client } from "@/sanity/lib/client";
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const posts = await client.fetch<any[]>(`*[_type == "post"] {
|
||||
slug, publishedAt,
|
||||
}`);
|
||||
|
||||
return [
|
||||
{
|
||||
url: `${SITE_URL}/`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly",
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: `${SITE_URL}/posts`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
|
||||
...posts.map((item: any) => ({
|
||||
url: `${SITE_URL}/posts/${item.slug.current}`,
|
||||
lastModified: new Date(item.publishedAt),
|
||||
changeFrequency: "daily" as any,
|
||||
priority: 0.75,
|
||||
})),
|
||||
];
|
||||
}
|
22
app/theme.ts
Normal file
@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { createTheme } from "@mui/material/styles";
|
||||
|
||||
export const theme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: "#49509e",
|
||||
},
|
||||
secondary: {
|
||||
main: "#d43630",
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
h1: { fontSize: "2.5rem" },
|
||||
h2: { fontSize: "2rem" },
|
||||
h3: { fontSize: "1.75rem" },
|
||||
h4: { fontSize: "1.5rem" },
|
||||
h5: { fontSize: "1.25rem" },
|
||||
h6: { fontSize: "1.15rem" },
|
||||
},
|
||||
});
|
139
components/AppShell.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Slide,
|
||||
Toolbar,
|
||||
Typography,
|
||||
AppBar as MuiAppBar,
|
||||
AppBarProps as MuiAppBarProps,
|
||||
useScrollTrigger,
|
||||
IconButton,
|
||||
styled,
|
||||
Box,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
import { ReactElement, ReactNode, useEffect, useState } from "react";
|
||||
import { SITE_NAME } from "@/app/consts";
|
||||
import NavigationDrawer, { DRAWER_WIDTH, AppNavigationHeader, isMobileQuery } from "@/components/NavigationDrawer";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
function HideOnScroll(props: { window?: () => Window; children: ReactElement }) {
|
||||
const { children, window } = props;
|
||||
const trigger = useScrollTrigger({
|
||||
target: window ? window() : undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
<Slide appear={false} direction="down" in={!trigger}>
|
||||
{children}
|
||||
</Slide>
|
||||
);
|
||||
}
|
||||
|
||||
interface AppBarProps extends MuiAppBarProps {
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
const ShellAppBar = styled(MuiAppBar, {
|
||||
shouldForwardProp: (prop) => prop !== "open",
|
||||
})<AppBarProps>(({ theme, open }) => {
|
||||
const isMobile = useMediaQuery(isMobileQuery);
|
||||
|
||||
return {
|
||||
transition: theme.transitions.create(["margin", "width"], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
...(!isMobile &&
|
||||
open && {
|
||||
width: `calc(100% - ${DRAWER_WIDTH}px)`,
|
||||
transition: theme.transitions.create(["margin", "width"], {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
marginRight: DRAWER_WIDTH,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const AppMain = styled("main", { shouldForwardProp: (prop) => prop !== "open" })<{
|
||||
open?: boolean;
|
||||
}>(({ theme, open }) => {
|
||||
const isMobile = useMediaQuery(isMobileQuery);
|
||||
|
||||
return {
|
||||
flexGrow: 1,
|
||||
transition: theme.transitions.create("margin", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
marginRight: -DRAWER_WIDTH,
|
||||
...(!isMobile &&
|
||||
open && {
|
||||
transition: theme.transitions.create("margin", {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
marginRight: 0,
|
||||
}),
|
||||
position: "relative",
|
||||
};
|
||||
});
|
||||
|
||||
export default function AppShell({ children }: { children: ReactNode }) {
|
||||
let documentWindow: Window;
|
||||
|
||||
const isMobile = useMediaQuery(isMobileQuery);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
documentWindow = window;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<HideOnScroll window={() => documentWindow}>
|
||||
<ShellAppBar open={open} position="fixed">
|
||||
<Toolbar sx={{ height: 64 }}>
|
||||
<IconButton
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
sx={{ ml: isMobile ? 0.5 : 0, mr: 2 }}
|
||||
>
|
||||
<Image src="/smartsheep.svg" alt="Logo" width={32} height={32} />
|
||||
</IconButton>
|
||||
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1, fontSize: "1.2rem" }}>
|
||||
<Link href="/">{SITE_NAME}</Link>
|
||||
</Typography>
|
||||
|
||||
<IconButton
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
onClick={() => setOpen(true)}
|
||||
sx={{ mr: 1, display: !isMobile && open ? "none" : "block" }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</ShellAppBar>
|
||||
</HideOnScroll>
|
||||
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<AppMain open={open}>
|
||||
<AppNavigationHeader />
|
||||
|
||||
{children}
|
||||
</AppMain>
|
||||
|
||||
<NavigationDrawer open={open} onClose={() => setOpen(false)} />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
154
components/NavigationDrawer.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
|
||||
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
import {
|
||||
Box, Collapse,
|
||||
Divider,
|
||||
Drawer,
|
||||
IconButton,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
styled,
|
||||
useMediaQuery
|
||||
} from "@mui/material";
|
||||
import { theme } from "@/app/theme";
|
||||
import { Fragment, ReactNode, useState } from "react";
|
||||
import HomeIcon from "@mui/icons-material/Home";
|
||||
import ArticleIcon from "@mui/icons-material/Article";
|
||||
import FeedIcon from "@mui/icons-material/RssFeed";
|
||||
import InfoIcon from "@mui/icons-material/Info";
|
||||
import GavelIcon from "@mui/icons-material/Gavel";
|
||||
import PolicyIcon from "@mui/icons-material/Policy";
|
||||
import SupervisedUserCircleIcon from "@mui/icons-material/SupervisedUserCircle";
|
||||
import ExpandLess from "@mui/icons-material/ExpandLess";
|
||||
import ExpandMore from "@mui/icons-material/ExpandMore";
|
||||
import Link from "next/link";
|
||||
|
||||
export interface NavigationItem {
|
||||
icon?: ReactNode;
|
||||
title?: string;
|
||||
link?: string;
|
||||
divider?: boolean;
|
||||
children?: NavigationItem[];
|
||||
}
|
||||
|
||||
export const DRAWER_WIDTH = 320;
|
||||
export const NAVIGATION_ITEMS: NavigationItem[] = [
|
||||
{ icon: <HomeIcon />, title: "首页", link: "/" },
|
||||
{ icon: <ArticleIcon />, title: "博客", link: "/posts" },
|
||||
{
|
||||
icon: <InfoIcon />, title: "信息中心", children: [
|
||||
{ icon: <GavelIcon />, title: "用户协议", link: "/i/user-agreement" },
|
||||
{ icon: <PolicyIcon />, title: "隐私协议", link: "/i/privacy-policy" },
|
||||
{ icon: <SupervisedUserCircleIcon />, title: "社区准则", link: "/i/community-guidelines" }
|
||||
]
|
||||
},
|
||||
{ divider: true },
|
||||
{ icon: <FeedIcon />, title: "订阅源", link: "/feed" }
|
||||
];
|
||||
|
||||
export const AppNavigationHeader = styled("div")(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: theme.spacing(0, 1),
|
||||
justifyContent: "flex-start",
|
||||
height: 64,
|
||||
...theme.mixins.toolbar
|
||||
}));
|
||||
|
||||
export function AppNavigationSection({ items, depth }: { items: NavigationItem[], depth?: number }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return items.map((item, idx) => {
|
||||
if (item.divider) {
|
||||
return <Divider key={idx} sx={{ my: 1 }} />;
|
||||
} else if (item.children) {
|
||||
return (
|
||||
<Fragment key={idx}>
|
||||
<ListItemButton onClick={() => setOpen(!open)} sx={{ pl: 2 + (depth ?? 0) * 2 }}>
|
||||
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||
<ListItemText primary={item.title} />
|
||||
{open ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemButton>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<List component="div" disablePadding>
|
||||
<AppNavigationSection items={item.children} depth={(depth ?? 0) + 1} />
|
||||
</List>
|
||||
</Collapse>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link key={idx} href={item.link ?? "/"} passHref>
|
||||
<ListItemButton sx={{ pl: 2 + (depth ?? 0) * 2 }}>
|
||||
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||
<ListItemText primary={item.title} />
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function AppNavigation({ showClose, onClose }: { showClose?: boolean; onClose: () => void }) {
|
||||
return (
|
||||
<>
|
||||
<AppNavigationHeader>
|
||||
{showClose && (
|
||||
<IconButton onClick={onClose}>
|
||||
{theme.direction === "rtl" ? <ChevronLeftIcon /> : <ChevronRightIcon />}
|
||||
</IconButton>
|
||||
)}
|
||||
</AppNavigationHeader>
|
||||
<Divider />
|
||||
<List>
|
||||
<AppNavigationSection items={NAVIGATION_ITEMS} />
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const isMobileQuery = theme.breakpoints.down("md");
|
||||
|
||||
export default function NavigationDrawer({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
const isMobile = useMediaQuery(isMobileQuery);
|
||||
|
||||
return isMobile ? (
|
||||
<>
|
||||
<Box sx={{ flexShrink: 0, width: DRAWER_WIDTH }} />
|
||||
<Drawer
|
||||
keepMounted
|
||||
anchor="right"
|
||||
variant="temporary"
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
sx={{
|
||||
"& .MuiDrawer-paper": {
|
||||
boxSizing: "border-box",
|
||||
width: DRAWER_WIDTH
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AppNavigation onClose={onClose} />
|
||||
</Drawer>
|
||||
</>
|
||||
) : (
|
||||
<Drawer
|
||||
variant="persistent"
|
||||
anchor="right"
|
||||
open={open}
|
||||
sx={{
|
||||
width: DRAWER_WIDTH,
|
||||
flexShrink: 0,
|
||||
"& .MuiDrawer-paper": {
|
||||
width: DRAWER_WIDTH
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AppNavigation showClose onClose={onClose} />
|
||||
</Drawer>
|
||||
);
|
||||
}
|
38
components/articles/ImageViewer.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import Zoomist from "zoomist";
|
||||
|
||||
import "zoomist/css";
|
||||
|
||||
export default function ImageViewer({ src, alt }: { src: string, alt: string }) {
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (container.current) {
|
||||
new Zoomist(container.current, {
|
||||
maxScale: 5,
|
||||
bounds: true,
|
||||
pinchable: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={container} className="zoomist-container h-fit">
|
||||
<div className="zoomist-wrapper">
|
||||
<div className="zoomist-image h-fit">
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={0}
|
||||
height={0}
|
||||
sizes="100vw"
|
||||
style={{ width: "100%", height: "auto" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
30
components/posts/PostContent.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { PortableText } from "@portabletext/react";
|
||||
import { client } from "@/sanity/lib/client";
|
||||
import imageUrlBuilder from "@sanity/image-url";
|
||||
import Link from "next/link";
|
||||
import ImageViewer from "@/components/articles/ImageViewer";
|
||||
|
||||
export default function PostContent({ content }: { content: any }) {
|
||||
const imageBuilder = imageUrlBuilder(client);
|
||||
|
||||
const componentSet = {
|
||||
types: {
|
||||
image: ({ value }: any) => {
|
||||
const image = imageBuilder.image(value);
|
||||
return <ImageViewer src={image.url()} alt={value.alt} />;
|
||||
}
|
||||
},
|
||||
marks: {
|
||||
link: ({ children, value }: any) => {
|
||||
const rel = !value.href.startsWith("/") ? "noreferrer noopener" : undefined;
|
||||
return (
|
||||
<Link href={value.href} rel={rel}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return <PortableText value={content} components={componentSet} />;
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import { dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { FlatCompat } from '@eslint/eslintrc'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
})
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends('next/core-web-vitals', 'next/typescript'),
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@next/next/no-img-element': 'off',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default eslintConfig
|
7
mdx-components.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import type { MDXComponents } from "mdx/types";
|
||||
|
||||
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
return {
|
||||
...components
|
||||
};
|
||||
}
|
26
next.config.mjs
Normal file
@ -0,0 +1,26 @@
|
||||
import mdx from "@next/mdx";
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "**"
|
||||
}
|
||||
]
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{ source: "/rss", destination: "/feed" },
|
||||
{ source: "/rss.xml", destination: "/feed.xml" },
|
||||
{ source: "/feed.xml", destination: "/feed" },
|
||||
|
||||
{ source: "/p/:id", destination: "/posts/:id" },
|
||||
{ source: "/i/:id", destination: "/information/:id" }
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
export default mdx()(nextConfig);
|
@ -1,32 +0,0 @@
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
reactStrictMode: true,
|
||||
output: 'standalone',
|
||||
generateBuildId: async () => {
|
||||
return process.env.GIT_HASH ?? 'development'
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'raw.sn.solsynth.dev',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'api.sn.solsynth.dev',
|
||||
},
|
||||
],
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/.well-known/:path*',
|
||||
destination: '/api/well-known/:path*',
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
94
package.json
@ -3,61 +3,59 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"build-standalone": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/cache": "^11.14.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@mui/icons-material": "^6.3.1",
|
||||
"@mui/material": "^6.3.1",
|
||||
"@mui/material-nextjs": "^6.3.1",
|
||||
"@mui/x-charts": "^7.23.6",
|
||||
"@next/third-parties": "^15.1.6",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@toolpad/core": "^0.11.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.7.9",
|
||||
"axios-case-converter": "^1.1.1",
|
||||
"cookies-next": "^5.0.2",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"@mdx-js/loader": "^3.0.1",
|
||||
"@mdx-js/react": "^3.0.1",
|
||||
"@mui/icons-material": "^5.15.10",
|
||||
"@mui/material": "^5.15.10",
|
||||
"@mui/material-nextjs": "^5.15.11",
|
||||
"@next/mdx": "^14.1.0",
|
||||
"@portabletext/react": "^3.0.11",
|
||||
"@sanity/client": "^6.12.4",
|
||||
"@sanity/icons": "^2.8",
|
||||
"@sanity/image-url": "1",
|
||||
"@sanity/types": "^3.25",
|
||||
"@sanity/ui": "^2.0",
|
||||
"@sanity/vision": "3",
|
||||
"@types/mdx": "^2.0.11",
|
||||
"@vercel/speed-insights": "^1.0.10",
|
||||
"feed": "^4.2.2",
|
||||
"next": "^15.1.5",
|
||||
"next-nprogress-bar": "^2.4.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"sitemap": "^8.0.0",
|
||||
"solar-js-sdk": "^0.1.3",
|
||||
"unified": "^11.0.5",
|
||||
"zustand": "^5.0.3"
|
||||
"gray-matter": "^4.0.3",
|
||||
"next": "^14.1",
|
||||
"next-sanity": "7.1.4",
|
||||
"nextjs-toploader": "^1.6.6",
|
||||
"react": "^18.2",
|
||||
"react-dom": "^18",
|
||||
"react-markdown": "^9.0.1",
|
||||
"sanity": "^3.25",
|
||||
"styled-components": "^6.0",
|
||||
"zoomist": "^2.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@types/node": "^20.17.12",
|
||||
"@types/react": "^19.0.4",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"daisyui": "^4.12.23",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-next": "15.1.3",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.3"
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/node": "^20",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/rss": "^0.0.32",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.1.0",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@vercel/speed-insights",
|
||||
"esbuild",
|
||||
"sharp"
|
||||
]
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0"
|
||||
}
|
||||
}
|
||||
|
175
packages/sn/.gitignore
vendored
@ -1,175 +0,0 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Caches
|
||||
|
||||
.cache
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "solar-js-sdk",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"entrypoint": "dist/index.js",
|
||||
"type": "module",
|
||||
"author": {
|
||||
"name": "LittleSheep",
|
||||
"email": "littlesheep.code@hotmail.com"
|
||||
},
|
||||
"version": "0.1.3",
|
||||
"tsup": {
|
||||
"entry": [
|
||||
"src/index.ts"
|
||||
],
|
||||
"splitting": true,
|
||||
"sourcemap": true,
|
||||
"clean": true,
|
||||
"dts": true,
|
||||
"format": "esm"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"tsup": "^8.3.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"universal-cookie": "^7.2.2",
|
||||
"zustand": "^5.0.3"
|
||||
}
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
import { sni } from './network'
|
||||
|
||||
export interface SnAttachment {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
rid: string
|
||||
uuid: string
|
||||
size: number
|
||||
name: string
|
||||
alt: string
|
||||
mimetype: string
|
||||
hash: string
|
||||
destination: number
|
||||
refCount: number
|
||||
contentRating: number
|
||||
qualityRating: number
|
||||
cleanedAt?: Date | null
|
||||
isAnalyzed: boolean
|
||||
isSelfRef: boolean
|
||||
isIndexable: boolean
|
||||
ref?: SnAttachment | null
|
||||
refId?: number | null
|
||||
poolId?: number | null
|
||||
accountId: number
|
||||
thumbnailId?: number | null
|
||||
thumbnail?: SnAttachment | null
|
||||
compressedId?: number | null
|
||||
compressed?: SnAttachment | null
|
||||
usermeta: Record<string, any>
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
|
||||
export async function getAttachment(id: string | number): Promise<SnAttachment> {
|
||||
const resp = await sni.get<SnAttachment>('/cgi/uc/attachments/' + id + '/meta')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
export async function listAttachment(id: string[]): Promise<SnAttachment[]> {
|
||||
const resp = await sni.get<{ data: SnAttachment[] }>('/cgi/uc/attachments', {
|
||||
params: {
|
||||
id: id.join(','),
|
||||
take: id.length,
|
||||
},
|
||||
})
|
||||
return resp.data.data
|
||||
}
|
||||
|
||||
export type MultipartProgress = {
|
||||
value: number | null
|
||||
current: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type MultipartInfo = {
|
||||
rid: string
|
||||
fileChunks: Record<string, number>
|
||||
}
|
||||
|
||||
export class UploadAttachmentTask {
|
||||
private content: File
|
||||
private pool: string
|
||||
private multipartSize: number = 0
|
||||
private multipartInfo: MultipartInfo | null = null
|
||||
private multipartProgress: MultipartProgress = { value: null, current: 0, total: 0 }
|
||||
|
||||
onProgress?: (progress: MultipartProgress) => void
|
||||
onSuccess?: (success: boolean) => void
|
||||
onError?: (error: string) => void
|
||||
|
||||
constructor(content: File, pool: string) {
|
||||
if (!content || !pool) {
|
||||
throw new Error('Content and pool are required.')
|
||||
}
|
||||
this.content = content
|
||||
this.pool = pool
|
||||
}
|
||||
|
||||
public async submit(): Promise<SnAttachment> {
|
||||
const limit = 3
|
||||
|
||||
try {
|
||||
await this.createFragment()
|
||||
console.log(`[Paperclip] Multipart placeholder has been created with rid ${this.multipartInfo?.rid}`)
|
||||
|
||||
this.multipartProgress.value = 0
|
||||
this.multipartProgress.current = 0
|
||||
|
||||
const chunks = Object.keys(this.multipartInfo?.fileChunks || {})
|
||||
this.multipartProgress.total = chunks.length
|
||||
|
||||
let result: SnAttachment | null = null
|
||||
|
||||
const uploadChunks = async (chunk: string): Promise<void> => {
|
||||
try {
|
||||
const resp = await this.uploadOneChunk(chunk)
|
||||
this.multipartProgress.current++
|
||||
console.log(
|
||||
`[Paperclip] Uploaded multipart ${this.multipartProgress.current}/${this.multipartProgress.total}`,
|
||||
)
|
||||
this.multipartProgress.value = this.multipartProgress.current / this.multipartProgress.total
|
||||
|
||||
if (this.onProgress) this.onProgress(this.multipartProgress)
|
||||
|
||||
result = resp
|
||||
} catch (err) {
|
||||
console.log(`[Paperclip] Upload multipart ${chunk} failed, retrying in 3 seconds...`)
|
||||
await this.delay(3000)
|
||||
await uploadChunks(chunk)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < chunks.length; i += limit) {
|
||||
const chunkSlice = chunks.slice(i, i + limit)
|
||||
await Promise.all(chunkSlice.map(uploadChunks))
|
||||
}
|
||||
|
||||
console.log(`[Paperclip] Entire file has been uploaded in ${this.multipartProgress.total} chunk(s)`)
|
||||
if (this.onSuccess) this.onSuccess(true)
|
||||
|
||||
return result!
|
||||
} catch (err: any) {
|
||||
if (this.onError) this.onError(err.toString())
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
private async createFragment(): Promise<void> {
|
||||
const mimetypeMap: Record<string, string> = {
|
||||
mp4: 'video/mp4',
|
||||
mov: 'video/quicktime',
|
||||
mp3: 'audio/mp3',
|
||||
wav: 'audio/wav',
|
||||
m4a: 'audio/m4a',
|
||||
}
|
||||
|
||||
const fileExtension = this.content.name.split('.').pop() || ''
|
||||
const mimetype = mimetypeMap[fileExtension]
|
||||
|
||||
const nameArray = this.content.name.split('.')
|
||||
nameArray.pop()
|
||||
|
||||
const resp = await sni.post('/cgi/uc/fragments', {
|
||||
pool: this.pool,
|
||||
size: this.content.size,
|
||||
name: this.content.name,
|
||||
alt: nameArray.join('.'),
|
||||
mimetype,
|
||||
})
|
||||
|
||||
const data = await resp.data
|
||||
|
||||
this.multipartSize = data.chunkSize
|
||||
this.multipartInfo = data.meta
|
||||
}
|
||||
|
||||
private async uploadOneChunk(chunkId: string): Promise<SnAttachment | null> {
|
||||
if (!this.multipartInfo) return null
|
||||
|
||||
const chunkIdx = this.multipartInfo.fileChunks[chunkId]
|
||||
const chunk = this.content.slice(chunkIdx * this.multipartSize, (chunkIdx + 1) * this.multipartSize)
|
||||
|
||||
const resp = await sni.post(`/cgi/uc/fragments/${this.multipartInfo.rid}/${chunkId}`, chunk, {
|
||||
headers: { 'Content-Type': 'application/octet-stream' },
|
||||
timeout: 3 * 60 * 1000,
|
||||
})
|
||||
|
||||
if (resp.data['attachment']) {
|
||||
return resp.data['attachment'] as SnAttachment
|
||||
}
|
||||
this.multipartInfo = resp.data['fragment']
|
||||
return null
|
||||
}
|
||||
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
public static formatBytes(bytes: number, decimals = 2): string {
|
||||
if (!+bytes) return '0 Bytes'
|
||||
|
||||
const k = 1024
|
||||
const dm = decimals < 0 ? 0 : decimals
|
||||
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import Cookies from 'universal-cookie'
|
||||
|
||||
export interface SnAuthResult {
|
||||
isFinished: boolean
|
||||
ticket: SnAuthTicket
|
||||
}
|
||||
|
||||
export interface SnAuthTicket {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
stepRemain: number
|
||||
grantToken?: string | null
|
||||
accessToken?: string | null
|
||||
refreshToken?: string | null
|
||||
ipAddress: string
|
||||
location: string
|
||||
userAgent: string
|
||||
expiredAt?: Date | null
|
||||
lastGrantAt?: Date | null
|
||||
availableAt?: Date | null
|
||||
nonce?: string | null
|
||||
accountId?: number | null
|
||||
factorTrail: number[]
|
||||
}
|
||||
|
||||
export interface SnAuthFactor {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
type: number
|
||||
config?: Record<string, any> | null
|
||||
accountId?: number | null
|
||||
}
|
||||
|
||||
export function setTokenCookies(atk: string, rtk: string) {
|
||||
const cookies = new Cookies()
|
||||
cookies.set('nex_user_atk', atk, { path: '/', maxAge: 2592000 })
|
||||
cookies.set('nex_user_rtk', rtk, { path: '/', maxAge: 2592000 })
|
||||
}
|
||||
|
||||
export function removeTokenCookies() {
|
||||
const cookies = new Cookies()
|
||||
cookies.remove('nex_user_atk')
|
||||
cookies.remove('nex_user_rtk')
|
||||
}
|
||||
|
||||
export function checkAuthenticatedClient(): boolean {
|
||||
const cookies = new Cookies()
|
||||
return !!cookies.get('nex_user_atk')
|
||||
}
|
||||
|
||||
export function redirectToLogin() {
|
||||
window.open('/auth/login?redirect_uri=' + encodeURIComponent(window.location.pathname), '_self')
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
export interface SnCheckInRecord {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
resultTier: number
|
||||
resultExperience: number
|
||||
resultModifiers: number[]
|
||||
accountId: number
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
export * from './matrix/product'
|
||||
export * from './matrix/release'
|
||||
export * from './attachment'
|
||||
export * from './auth'
|
||||
export * from './checkIn'
|
||||
export * from './network'
|
||||
export * from './post'
|
||||
export * from './user'
|
@ -1,25 +0,0 @@
|
||||
export interface MaProduct {
|
||||
id: number
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
deleted_at?: Date
|
||||
icon: string
|
||||
name: string
|
||||
alias: string
|
||||
description: string
|
||||
previews: string[]
|
||||
tags: string[]
|
||||
meta: MaProductMeta
|
||||
releases: null
|
||||
account_id: number
|
||||
}
|
||||
|
||||
export interface MaProductMeta {
|
||||
id: number
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
deleted_at?: Date
|
||||
introduction: string
|
||||
attachments: string[]
|
||||
product_id: number
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
export interface MaRelease {
|
||||
id: number
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
deleted_at?: Date
|
||||
version: string
|
||||
type: number
|
||||
channel: string
|
||||
assets: Record<string, MaReleaseAsset>
|
||||
installers: Record<string, MaReleaseInstaller>
|
||||
runners: Record<string, MaReleaseRunner>
|
||||
product_id: number
|
||||
meta: MaReleaseMeta
|
||||
}
|
||||
|
||||
export interface MaReleaseMeta {
|
||||
id: number
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
deleted_at?: Date
|
||||
title: string
|
||||
description: string
|
||||
content: string
|
||||
attachments: string[]
|
||||
release_id: number
|
||||
}
|
||||
|
||||
export interface MaReleaseAsset {
|
||||
uri: string
|
||||
contentType: string
|
||||
}
|
||||
|
||||
export interface MaReleaseInstallerPatch {
|
||||
action: string
|
||||
glob: string
|
||||
}
|
||||
|
||||
export interface MaReleaseInstaller {
|
||||
workdir?: string
|
||||
script?: string
|
||||
patches: MaReleaseInstallerPatch[]
|
||||
}
|
||||
|
||||
export interface MaReleaseRunner {
|
||||
workdir?: string
|
||||
script: string
|
||||
label: string
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
import axios, { type AxiosInstance } from 'axios'
|
||||
import Cookies from 'universal-cookie'
|
||||
import { setTokenCookies } from './auth'
|
||||
|
||||
function toCamelCase(obj: any): any {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(toCamelCase)
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
return Object.keys(obj).reduce((result: any, key) => {
|
||||
const camelKey = key.replace(/_([a-z])/g, (_, char) => char.toUpperCase())
|
||||
result[camelKey] = toCamelCase(obj[key])
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
function toSnakeCase(obj: any): any {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(toSnakeCase)
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
return Object.keys(obj).reduce((result: any, key) => {
|
||||
const snakeKey = key.replace(/[A-Z]/g, (char) => `_${char.toLowerCase()}`)
|
||||
result[snakeKey] = toSnakeCase(obj[key])
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
const baseURL = 'https://api.sn.solsynth.dev'
|
||||
|
||||
export const sni: AxiosInstance = (() => {
|
||||
const inst = axios.create({
|
||||
baseURL,
|
||||
})
|
||||
|
||||
inst.interceptors.request.use(
|
||||
async (config) => {
|
||||
const tk = await refreshToken()
|
||||
if (tk) config.headers['Authorization'] = `Bearer ${tk}`
|
||||
return config
|
||||
},
|
||||
(error) => error,
|
||||
)
|
||||
|
||||
/// Case convertor
|
||||
|
||||
inst.interceptors.request.use(
|
||||
(config) => {
|
||||
if (config.data) {
|
||||
config.data = toSnakeCase(config.data)
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
)
|
||||
|
||||
inst.interceptors.response.use(
|
||||
(response) => {
|
||||
if (response.data) {
|
||||
response.data = toCamelCase(response.data)
|
||||
}
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
if (error.response && error.response.data) {
|
||||
error.response.data = toCamelCase(error.response.data)
|
||||
}
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
return inst
|
||||
})()
|
||||
|
||||
async function refreshToken(): Promise<string | undefined> {
|
||||
const cookies = new Cookies()
|
||||
if (!cookies.get('nex_user_atk') || !cookies.get('nex_user_rtk')) return
|
||||
|
||||
const ogTk: string = cookies.get('nex_user_atk')!
|
||||
if (!isTokenExpired(ogTk)) return ogTk
|
||||
|
||||
const resp = await axios.post(
|
||||
'/cgi/id/auth/token',
|
||||
{
|
||||
refresh_token: cookies.get('nex_user_rtk')!,
|
||||
grant_type: 'refresh_token',
|
||||
},
|
||||
{ baseURL },
|
||||
)
|
||||
const atk: string = resp.data['access_token']
|
||||
const rtk: string = resp.data['refresh_token']
|
||||
setTokenCookies(atk, rtk)
|
||||
|
||||
console.log('[Authenticator] Refreshed token...')
|
||||
|
||||
return atk
|
||||
}
|
||||
|
||||
function isTokenExpired(token: string): boolean {
|
||||
try {
|
||||
const parts = token.split('.')
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('Invalid JWT format')
|
||||
}
|
||||
|
||||
const payload = JSON.parse(atob(parts[1]))
|
||||
|
||||
if (!payload.exp) {
|
||||
throw new Error("'exp' claim is missing in the JWT payload")
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
|
||||
return now >= payload.exp
|
||||
} catch (error) {
|
||||
console.error('[Authenticator] Something went wrong with token: ', error)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export function getAttachmentUrl(identifer: string): string {
|
||||
if (identifer.startsWith('http')) return identifer
|
||||
return `${baseURL}/cgi/uc/attachments/${identifer}`
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
export interface SnPost {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
type: string
|
||||
body: SnPostBody & Record<string, any>
|
||||
language: string
|
||||
alias?: string | null
|
||||
aliasPrefix?: string | null
|
||||
tags: SnPostTag[]
|
||||
categories: SnPostCategory[]
|
||||
replies?: SnPost[] | null
|
||||
replyId?: number | null
|
||||
repostId?: number | null
|
||||
replyTo?: SnPost | null
|
||||
repostTo?: SnPost | null
|
||||
visibleUsersList?: number[] | null
|
||||
invisibleUsersList?: number[] | null
|
||||
visibility: number
|
||||
editedAt?: Date | null
|
||||
pinnedAt?: Date | null
|
||||
lockedAt?: Date | null
|
||||
isDraft: boolean
|
||||
publishedAt?: Date | null
|
||||
publishedUntil?: Date | null
|
||||
totalUpvote: number
|
||||
totalDownvote: number
|
||||
publisherId: number
|
||||
publisher: SnPublisher
|
||||
metric: SnMetric
|
||||
}
|
||||
|
||||
export interface SnPostTag {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date
|
||||
alias: string
|
||||
name: string
|
||||
description: string
|
||||
posts?: SnPost[]
|
||||
}
|
||||
|
||||
export interface SnPostCategory {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date
|
||||
alias: string
|
||||
name: string
|
||||
description: string
|
||||
posts?: SnPost[]
|
||||
}
|
||||
|
||||
export interface SnPostBody {
|
||||
attachments: string[]
|
||||
content: string
|
||||
location?: string
|
||||
thumbnail?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface SnMetric {
|
||||
replyCount: number
|
||||
reactionCount: number
|
||||
reactionList: Record<string, number>
|
||||
}
|
||||
|
||||
export interface SnPublisher {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
type: number
|
||||
name: string
|
||||
nick: string
|
||||
description: string
|
||||
avatar: string
|
||||
banner: string
|
||||
totalUpvote: number
|
||||
totalDownvote: number
|
||||
realmId?: number | null
|
||||
accountId: number
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import { create } from 'zustand'
|
||||
import { sni } from './network'
|
||||
import Cookies from 'universal-cookie'
|
||||
|
||||
export interface SnAccount {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
confirmedAt?: Date | null
|
||||
contacts?: SnAccountContact[] | null
|
||||
avatar: string
|
||||
banner: string
|
||||
description: string
|
||||
name: string
|
||||
nick: string
|
||||
permNodes: Record<string, any>
|
||||
profile?: SnAccountProfile | null
|
||||
badges: SnAccountBadge[]
|
||||
suspendedAt?: Date | null
|
||||
affiliatedId?: number | null
|
||||
affiliatedTo?: number | null
|
||||
automatedBy?: number | null
|
||||
automatedId?: number | null
|
||||
}
|
||||
|
||||
export interface SnAccountContact {
|
||||
accountId: number
|
||||
content: string
|
||||
createdAt: Date
|
||||
deletedAt?: Date | null
|
||||
id: number
|
||||
isPrimary: boolean
|
||||
isPublic: boolean
|
||||
type: number
|
||||
updatedAt: Date
|
||||
verifiedAt?: Date | null
|
||||
}
|
||||
|
||||
export interface SnAccountProfile {
|
||||
id: number
|
||||
accountId: number
|
||||
birthday?: Date | null
|
||||
createdAt: Date
|
||||
deletedAt?: Date | null
|
||||
experience: number
|
||||
firstName: string
|
||||
lastName: string
|
||||
lastSeenAt?: Date | null
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export interface SnAccountBadge {
|
||||
id: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
deletedAt?: Date | null
|
||||
type: string
|
||||
accountId: number
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
|
||||
export interface UserStore {
|
||||
account: SnAccount | undefined
|
||||
fetchUser: () => Promise<SnAccount | undefined>
|
||||
}
|
||||
|
||||
export const useUserStore = create<UserStore>((set) => ({
|
||||
account: undefined,
|
||||
fetchUser: async (): Promise<SnAccount | undefined> => {
|
||||
const cookies = new Cookies()
|
||||
if (!cookies.get('nex_user_atk')) return
|
||||
try {
|
||||
const resp = await sni.get<SnAccount>('/cgi/id/users/me')
|
||||
set({ account: resp.data })
|
||||
console.log('[Authenticator] Logged in as @' + resp.data.name)
|
||||
return resp.data
|
||||
} catch (err) {
|
||||
console.error('[Authenticator] Unable to get user profile: ', err)
|
||||
return
|
||||
}
|
||||
},
|
||||
}))
|
@ -1,27 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
@ -1,8 +0,0 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
Before Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 74 KiB |
BIN
public/logo.png
Before Width: | Height: | Size: 86 KiB |
1
public/next.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 28 KiB |
21
public/smartsheep.svg
Executable file
@ -0,0 +1,21 @@
|
||||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
|
||||
<title>SmartSheep Logo</title>
|
||||
<defs>
|
||||
<image width="124" height="198" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHwAAADGCAMAAAAnkRSfAAAAAXNSR0IB2cksfwAAAq9QTFRFAAAA////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////J9QBRAAAAOV0Uk5TAA0WEiFfjMLMr40/BRdmtuz//vbQYAQmpfLdQAGovyURj/iOD0r07loDGJfbPDTf/bhd94QLdPrrVQLWcPmtHU31egknzehRg9Ev1xnzbgYHhfzmTMorSOSbFJloKsniReXDKG/7kQ6m8WMw3ue7I4cKNdTZOFTqsR5/Hzra0zZY7al4Ir5SPWHvnpbG4MRrlKJs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"/>
|
||||
<image width="122" height="142" id="img2" href="data:image/png;base64,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"/>
|
||||
</defs>
|
||||
<style>
|
||||
.s0 { fill: #ffffff;stroke: #000000;stroke-miterlimit:100;stroke-width: 56 }
|
||||
.s1 { fill: #4750a3;stroke: #000000;stroke-miterlimit:100;stroke-width: 56 }
|
||||
</style>
|
||||
<path id="Wool" fill-rule="evenodd" class="s0" d="m128 608.4c0 95.9 77.4 173.6 172.8 173.6h441.6c84.8 0 153.6-69.1 153.6-154.3 0-74.6-52.8-136.9-122.9-151.1 4.9-12.9 7.7-27 7.7-41.7 0-63.9-51.6-115.8-115.2-115.8-23.6 0-45.7 7.3-64 19.6-33.2-57.9-95.2-96.7-166.4-96.7-106.1 0-192 86.3-192 192.9 0 3.2 0.1 6.5 0.2 9.7-67.2 23.8-115.4 88.1-115.4 163.8z"/>
|
||||
<g id="Crystal">
|
||||
<path id="Crystal" class="s1" d="m699 224l138.6 80v160l-138.6 80-138.6-80v-160z"/>
|
||||
<use id="Highlight" href="#img1" x="688" y="255"/>
|
||||
</g>
|
||||
<g id="Horn">
|
||||
</g>
|
||||
<g id="Face">
|
||||
<use id="Slime" href="#img2" x="233" y="538"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.1 KiB |
1
public/vercel.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
After Width: | Height: | Size: 629 B |
@ -1,15 +0,0 @@
|
||||
id = "capital"
|
||||
|
||||
[[locations]]
|
||||
id = "capital"
|
||||
hosts = ["solsynth.dev", "www.solsynth.dev"]
|
||||
paths = ["/"]
|
||||
[[locations.destinations]]
|
||||
id = "capital-destination"
|
||||
uri = "http://localhost:3000"
|
||||
|
||||
[[applications]]
|
||||
id = "capital-app"
|
||||
workdir = "/workdir/capital"
|
||||
command = ["node", "standalone/server.js"]
|
||||
environment = ["HOSTNAME=0.0.0.0"]
|
10
sanity.cli.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* This configuration file lets you run `$ sanity [command]` in this folder
|
||||
* Go to https://www.sanity.io/docs/cli to learn more.
|
||||
**/
|
||||
import { defineCliConfig } from "sanity/cli";
|
||||
|
||||
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID;
|
||||
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET;
|
||||
|
||||
export default defineCliConfig({ api: { projectId, dataset } });
|
25
sanity.config.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* This configuration is used to for the Sanity Studio that’s mounted on the `/pages/studio/page.tsx` route
|
||||
*/
|
||||
|
||||
import { visionTool } from "@sanity/vision";
|
||||
import { defineConfig } from "sanity";
|
||||
import { structureTool } from "sanity/structure";
|
||||
|
||||
// Go to https://www.sanity.io/docs/api-versioning to learn how API versioning works
|
||||
import { apiVersion, dataset, projectId } from "./sanity/env";
|
||||
import { schema } from "./sanity/schema";
|
||||
|
||||
export default defineConfig({
|
||||
basePath: "/console",
|
||||
projectId,
|
||||
dataset,
|
||||
// Add and edit the content schema in the './sanity/schema' folder
|
||||
schema,
|
||||
plugins: [
|
||||
structureTool(),
|
||||
// Vision is a tool that lets you query your content with GROQ in the studio
|
||||
// https://www.sanity.io/docs/the-vision-plugin
|
||||
visionTool({ defaultApiVersion: apiVersion }),
|
||||
],
|
||||
});
|
21
sanity/env.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2024-02-24";
|
||||
|
||||
export const dataset = assertValue(
|
||||
process.env.NEXT_PUBLIC_SANITY_DATASET,
|
||||
"Missing environment variable: NEXT_PUBLIC_SANITY_DATASET",
|
||||
);
|
||||
|
||||
export const projectId = assertValue(
|
||||
process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
|
||||
"Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_ID",
|
||||
);
|
||||
|
||||
export const useCdn = false;
|
||||
|
||||
function assertValue<T>(v: T | undefined, errorMessage: string): T {
|
||||
if (v === undefined) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
10
sanity/lib/client.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { createClient } from "next-sanity";
|
||||
|
||||
import { apiVersion, dataset, projectId, useCdn } from "../env";
|
||||
|
||||
export const client = createClient({
|
||||
apiVersion,
|
||||
dataset,
|
||||
projectId,
|
||||
useCdn,
|
||||
});
|
13
sanity/lib/image.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import createImageUrlBuilder from "@sanity/image-url";
|
||||
import type { Image } from "sanity";
|
||||
|
||||
import { dataset, projectId } from "../env";
|
||||
|
||||
const imageBuilder = createImageUrlBuilder({
|
||||
projectId: projectId || "",
|
||||
dataset: dataset || "",
|
||||
});
|
||||
|
||||
export const urlForImage = (source: Image) => {
|
||||
return imageBuilder?.image(source).auto("format").fit("max").url();
|
||||
};
|
10
sanity/schema.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { type SchemaTypeDefinition } from "sanity";
|
||||
|
||||
import blockContent from "./schemaTypes/blockContent";
|
||||
import category from "./schemaTypes/category";
|
||||
import post from "./schemaTypes/post";
|
||||
import author from "./schemaTypes/author";
|
||||
|
||||
export const schema: { types: SchemaTypeDefinition[] } = {
|
||||
types: [post, author, category, blockContent],
|
||||
};
|
57
sanity/schemaTypes/author.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { defineField, defineType } from "sanity";
|
||||
|
||||
export default defineType({
|
||||
name: "author",
|
||||
title: "Author",
|
||||
type: "document",
|
||||
fields: [
|
||||
defineField({
|
||||
name: "name",
|
||||
title: "Name",
|
||||
type: "string",
|
||||
}),
|
||||
defineField({
|
||||
name: "slug",
|
||||
title: "Slug",
|
||||
type: "slug",
|
||||
options: {
|
||||
source: "name",
|
||||
maxLength: 96,
|
||||
},
|
||||
}),
|
||||
defineField({
|
||||
name: "image",
|
||||
title: "Image",
|
||||
type: "image",
|
||||
options: {
|
||||
hotspot: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "alt",
|
||||
type: "string",
|
||||
title: "Alternative Text",
|
||||
},
|
||||
],
|
||||
}),
|
||||
defineField({
|
||||
name: "bio",
|
||||
title: "Bio",
|
||||
type: "array",
|
||||
of: [
|
||||
{
|
||||
title: "Block",
|
||||
type: "block",
|
||||
styles: [{ title: "Normal", value: "normal" }],
|
||||
lists: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
preview: {
|
||||
select: {
|
||||
title: "name",
|
||||
media: "image",
|
||||
},
|
||||
},
|
||||
});
|
75
sanity/schemaTypes/blockContent.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { defineType, defineArrayMember } from "sanity";
|
||||
|
||||
/**
|
||||
* This is the schema type for block content used in the post document type
|
||||
* Importing this type into the studio configuration's `schema` property
|
||||
* lets you reuse it in other document types with:
|
||||
* {
|
||||
* name: 'someName',
|
||||
* title: 'Some title',
|
||||
* type: 'blockContent'
|
||||
* }
|
||||
*/
|
||||
|
||||
export default defineType({
|
||||
title: "Block Content",
|
||||
name: "blockContent",
|
||||
type: "array",
|
||||
of: [
|
||||
defineArrayMember({
|
||||
title: "Block",
|
||||
type: "block",
|
||||
// Styles let you define what blocks can be marked up as. The default
|
||||
// set corresponds with HTML tags, but you can set any title or value
|
||||
// you want, and decide how you want to deal with it where you want to
|
||||
// use your content.
|
||||
styles: [
|
||||
{ title: "Normal", value: "normal" },
|
||||
{ title: "H1", value: "h1" },
|
||||
{ title: "H2", value: "h2" },
|
||||
{ title: "H3", value: "h3" },
|
||||
{ title: "H4", value: "h4" },
|
||||
{ title: "Quote", value: "blockquote" },
|
||||
],
|
||||
lists: [{ title: "Bullet", value: "bullet" }],
|
||||
// Marks let you mark up inline text in the Portable Text Editor
|
||||
marks: {
|
||||
// Decorators usually describe a single property – e.g. a typographic
|
||||
// preference or highlighting
|
||||
decorators: [
|
||||
{ title: "Strong", value: "strong" },
|
||||
{ title: "Emphasis", value: "em" },
|
||||
],
|
||||
// Annotations can be any object structure – e.g. a link or a footnote.
|
||||
annotations: [
|
||||
{
|
||||
title: "URL",
|
||||
name: "link",
|
||||
type: "object",
|
||||
fields: [
|
||||
{
|
||||
title: "URL",
|
||||
name: "href",
|
||||
type: "url",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
// You can add additional types here. Note that you can't use
|
||||
// primitive types such as 'string' and 'number' in the same array
|
||||
// as a block type.
|
||||
defineArrayMember({
|
||||
type: "image",
|
||||
options: { hotspot: true },
|
||||
fields: [
|
||||
{
|
||||
name: "alt",
|
||||
type: "string",
|
||||
title: "Alternative Text",
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
19
sanity/schemaTypes/category.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { defineField, defineType } from "sanity";
|
||||
|
||||
export default defineType({
|
||||
name: "category",
|
||||
title: "Category",
|
||||
type: "document",
|
||||
fields: [
|
||||
defineField({
|
||||
name: "title",
|
||||
title: "Title",
|
||||
type: "string",
|
||||
}),
|
||||
defineField({
|
||||
name: "description",
|
||||
title: "Description",
|
||||
type: "text",
|
||||
}),
|
||||
],
|
||||
});
|
77
sanity/schemaTypes/post.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { defineField, defineType } from "sanity";
|
||||
|
||||
export default defineType({
|
||||
name: "post",
|
||||
title: "Post",
|
||||
type: "document",
|
||||
fields: [
|
||||
defineField({
|
||||
name: "title",
|
||||
title: "Title",
|
||||
type: "string",
|
||||
}),
|
||||
defineField({
|
||||
name: "description",
|
||||
title: "Description",
|
||||
type: "string",
|
||||
}),
|
||||
defineField({
|
||||
name: "slug",
|
||||
title: "Slug",
|
||||
type: "slug",
|
||||
options: {
|
||||
source: "title",
|
||||
maxLength: 96,
|
||||
},
|
||||
}),
|
||||
defineField({
|
||||
name: "author",
|
||||
title: "Author",
|
||||
type: "reference",
|
||||
to: { type: "author" },
|
||||
}),
|
||||
defineField({
|
||||
name: "mainImage",
|
||||
title: "Main image",
|
||||
type: "image",
|
||||
options: {
|
||||
hotspot: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "alt",
|
||||
type: "string",
|
||||
title: "Alternative Text",
|
||||
},
|
||||
],
|
||||
}),
|
||||
defineField({
|
||||
name: "categories",
|
||||
title: "Categories",
|
||||
type: "array",
|
||||
of: [{ type: "reference", to: { type: "category" } }],
|
||||
}),
|
||||
defineField({
|
||||
name: "publishedAt",
|
||||
title: "Published at",
|
||||
type: "datetime",
|
||||
}),
|
||||
defineField({
|
||||
name: "body",
|
||||
title: "Body",
|
||||
type: "blockContent",
|
||||
}),
|
||||
],
|
||||
|
||||
preview: {
|
||||
select: {
|
||||
title: "title",
|
||||
author: "author.name",
|
||||
media: "mainImage",
|
||||
},
|
||||
prepare(selection) {
|
||||
const { author } = selection;
|
||||
return { ...selection, subtitle: author && `by ${author}` };
|
||||
},
|
||||
},
|
||||
});
|
Before Width: | Height: | Size: 696 KiB |
Before Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 2.0 MiB |
@ -1,90 +0,0 @@
|
||||
import { useUserStore } from 'solar-js-sdk'
|
||||
import { AppBar, AppBarProps, Avatar, IconButton, Toolbar, Typography, useScrollTrigger, useTheme } from '@mui/material'
|
||||
import { getAttachmentUrl } from 'solar-js-sdk'
|
||||
import MenuIcon from '@mui/icons-material/Menu'
|
||||
import AccountCircle from '@mui/icons-material/AccountCircle'
|
||||
import Link from 'next/link'
|
||||
import React, { useState } from 'react'
|
||||
import { CapDrawer } from './CapDrawer'
|
||||
|
||||
interface AppBarScrollProps {
|
||||
elevation?: number
|
||||
children?: React.ReactElement<{ elevation?: number } & AppBarProps>
|
||||
}
|
||||
|
||||
function AppBarScroll(props: AppBarScrollProps) {
|
||||
if (typeof window === 'undefined') return props.children
|
||||
|
||||
const trigger = useScrollTrigger({
|
||||
disableHysteresis: true,
|
||||
threshold: 0,
|
||||
target: window,
|
||||
})
|
||||
|
||||
const commonStyle = {
|
||||
transition: 'all',
|
||||
transitionDuration: '300ms',
|
||||
}
|
||||
|
||||
return props.children
|
||||
? React.cloneElement(props.children, {
|
||||
elevation: trigger ? props.elevation : 0,
|
||||
sx: trigger ? { borderBottom: 1, borderColor: 'divider', ...commonStyle } : { ...commonStyle },
|
||||
})
|
||||
: null
|
||||
}
|
||||
|
||||
export function CapAppBar() {
|
||||
const userStore = useUserStore()
|
||||
const theme = useTheme()
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const drawerWidth = 280
|
||||
|
||||
return (
|
||||
<>
|
||||
<CapDrawer width={drawerWidth} open={open} onClose={() => setOpen(false)} />
|
||||
|
||||
<AppBarScroll elevation={0}>
|
||||
<AppBar position="sticky" elevation={0} color="transparent" className="backdrop-blur-md z-10">
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
sx={{ mr: 2 }}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Link href="/" passHref style={{ flexGrow: 1 }}>
|
||||
<Typography variant="h6" component="div">
|
||||
Capital
|
||||
</Typography>
|
||||
</Link>
|
||||
|
||||
<Link href={userStore.account ? '/users/me' : '/auth/login'} passHref>
|
||||
<IconButton
|
||||
size="large"
|
||||
aria-label="account of current user"
|
||||
aria-controls="primary-search-account-menu"
|
||||
aria-haspopup="true"
|
||||
color="inherit"
|
||||
>
|
||||
{userStore.account ? (
|
||||
<Avatar sx={{ backgroundColor: 'transparent' }} src={getAttachmentUrl(userStore.account.avatar)} />
|
||||
) : (
|
||||
<Avatar sx={{ backgroundColor: 'transparent', color: theme.palette.text.primary }}>
|
||||
<AccountCircle />
|
||||
</Avatar>
|
||||
)}
|
||||
</IconButton>
|
||||
</Link>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</AppBarScroll>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Divider,
|
||||
Drawer,
|
||||
Toolbar,
|
||||
Typography,
|
||||
Link,
|
||||
} from '@mui/material'
|
||||
import { JSX } from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
import ExploreIcon from '@mui/icons-material/Explore'
|
||||
import PhotoLibraryIcon from '@mui/icons-material/PhotoLibrary'
|
||||
import AppsIcon from '@mui/icons-material/Apps'
|
||||
import NextLink from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export interface NavLink {
|
||||
title: string
|
||||
icon?: JSX.Element
|
||||
href: string
|
||||
}
|
||||
|
||||
export function CapDrawer({ width, open, onClose }: { width: number; open: boolean; onClose: () => void }) {
|
||||
const router = useRouter()
|
||||
|
||||
const functionLinks: NavLink[] = [
|
||||
{
|
||||
title: 'Explore',
|
||||
icon: <ExploreIcon />,
|
||||
href: '/posts',
|
||||
},
|
||||
{
|
||||
title: 'Gallery',
|
||||
icon: <PhotoLibraryIcon />,
|
||||
href: '/attachments',
|
||||
},
|
||||
{
|
||||
title: 'Matrix',
|
||||
icon: <AppsIcon />,
|
||||
href: '/matrix',
|
||||
},
|
||||
]
|
||||
|
||||
const additionalLinks: NavLink[] = [
|
||||
{
|
||||
title: 'Terms & Conditions',
|
||||
href: '/terms',
|
||||
},
|
||||
{
|
||||
title: 'Solar Console',
|
||||
href: '/console',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Drawer open={open} onClose={onClose}>
|
||||
<Box sx={{ width: width }} role="presentation" onClick={onClose}>
|
||||
<Toolbar style={{ padding: 0 }}>
|
||||
<Box display="flex" gap={2} sx={{ mx: 2 }}>
|
||||
<Image
|
||||
src="/logo.png"
|
||||
width={28}
|
||||
height={28}
|
||||
alt="company logo"
|
||||
style={{ objectFit: 'contain' }}
|
||||
className="dark:invert"
|
||||
/>
|
||||
|
||||
<Box display="flex" flexDirection="column" justifyContent="center">
|
||||
<Typography variant="body2" component="h2" fontWeight="bold" lineHeight={1.4}>
|
||||
Solsynth LLC
|
||||
</Typography>
|
||||
<Typography variant="caption" component="h3" lineHeight={1} fontFamily="monospace">
|
||||
Capital
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
|
||||
<List>
|
||||
{functionLinks.map((l) => (
|
||||
<NextLink passHref href={l.href} key={l.href}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton selected={router.pathname == l.href}>
|
||||
<ListItemIcon>{l.icon}</ListItemIcon>
|
||||
<ListItemText primary={l.title} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</NextLink>
|
||||
))}
|
||||
</List>
|
||||
<Divider />
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', px: 2, py: 1.5, gap: 1 }}>
|
||||
{additionalLinks.map((l) => (
|
||||
<NextLink passHref href={l.href} key={l.href}>
|
||||
<Link variant="body2" color={'textSecondary'} fontSize={13}>
|
||||
{l.title}
|
||||
</Link>
|
||||
</NextLink>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import { SnAttachment } from 'solar-js-sdk'
|
||||
import { getAttachmentUrl } from 'solar-js-sdk'
|
||||
import { QuestionMark } from '@mui/icons-material'
|
||||
import { Link, Paper, Typography } from '@mui/material'
|
||||
import { ComponentProps } from 'react'
|
||||
|
||||
export function AttachmentItem({
|
||||
item,
|
||||
borderRadius,
|
||||
...rest
|
||||
}: { item: SnAttachment; borderRadius?: string } & ComponentProps<'div'>) {
|
||||
switch (item.mimetype.split('/')[0]) {
|
||||
case 'image':
|
||||
return (
|
||||
<Paper {...rest}>
|
||||
<img
|
||||
src={getAttachmentUrl(item.rid)}
|
||||
alt={item.alt}
|
||||
style={{ objectFit: 'cover', borderRadius: borderRadius ?? '8px' }}
|
||||
/>
|
||||
</Paper>
|
||||
)
|
||||
case 'video':
|
||||
return (
|
||||
<Paper {...rest}>
|
||||
<video src={getAttachmentUrl(item.rid)} controls style={{ borderRadius: borderRadius ?? '8px' }} />
|
||||
</Paper>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<Paper sx={{ width: '100%', height: '100%', p: 5, textAlign: 'center' }} {...rest}>
|
||||
<QuestionMark sx={{ mb: 2 }} />
|
||||
<Typography>Unknown</Typography>
|
||||
<Typography gutterBottom>{item.name}</Typography>
|
||||
<Typography fontFamily="monospace" gutterBottom>
|
||||
{item.mimetype}
|
||||
</Typography>
|
||||
<Link href={getAttachmentUrl(item.rid)} target="_blank" rel="noreferrer" fontSize={13}>
|
||||
Open in browser
|
||||
</Link>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { setTokenCookies, SnAuthFactor, SnAuthResult, SnAuthTicket } from 'solar-js-sdk'
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { ArrowForward } from '@mui/icons-material'
|
||||
import { Collapse, Alert, Box, TextField, Button } from '@mui/material'
|
||||
import { useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
|
||||
export interface SnLoginCheckpointForm {
|
||||
password: string
|
||||
}
|
||||
|
||||
export function SnLoginCheckpoint({
|
||||
ticket,
|
||||
factor,
|
||||
onNext,
|
||||
}: {
|
||||
ticket: SnAuthTicket
|
||||
factor: SnAuthFactor
|
||||
onNext: (val: SnAuthTicket, done: boolean) => void
|
||||
}) {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
|
||||
const { handleSubmit, register } = useForm<SnLoginCheckpointForm>()
|
||||
|
||||
async function onSubmit(data: any) {
|
||||
try {
|
||||
setLoading(true)
|
||||
const resp = await sni.patch<SnAuthResult>('/cgi/id/auth', {
|
||||
ticket_id: ticket.id,
|
||||
factor_id: factor.id,
|
||||
code: data.password,
|
||||
})
|
||||
|
||||
if (resp.data.isFinished) {
|
||||
const tokenResp = await sni.post('/cgi/id/auth/token', {
|
||||
grant_type: 'grant_token',
|
||||
code: resp.data.ticket.grantToken!,
|
||||
})
|
||||
const atk: string = tokenResp.data['accessToken']
|
||||
const rtk: string = tokenResp.data['refreshToken']
|
||||
setTokenCookies(atk, rtk)
|
||||
console.log('[Authenticator] User has been logged in. Result atk: ', atk)
|
||||
}
|
||||
|
||||
onNext(resp.data.ticket, resp.data.isFinished)
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert sx={{ mb: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', width: '100%', gap: 2, textAlign: 'center' }}>
|
||||
<TextField
|
||||
label={factor.type == 0 ? 'Password' : 'Verification code'}
|
||||
type="password"
|
||||
{...register('password', { required: true })}
|
||||
/>
|
||||
|
||||
<Button variant="contained" endIcon={<ArrowForward />} disabled={loading} type="submit">
|
||||
Next
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { SnAuthFactor, SnAuthTicket } from 'solar-js-sdk'
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { Collapse, Alert, Box, Button, Typography, ButtonGroup } from '@mui/material'
|
||||
import { useState } from 'react'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import PasswordIcon from '@mui/icons-material/Password'
|
||||
import EmailIcon from '@mui/icons-material/Email'
|
||||
|
||||
export function SnLoginRouter({
|
||||
ticket,
|
||||
factorList,
|
||||
onNext,
|
||||
}: {
|
||||
ticket: SnAuthTicket
|
||||
factorList: SnAuthFactor[]
|
||||
onNext: (val: SnAuthFactor) => void
|
||||
}) {
|
||||
const factorTypeIcons = [<PasswordIcon key="password-icon" />, <EmailIcon key="email-icon" />]
|
||||
const factorTypeLabels = ['Password', 'Email verification code']
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
|
||||
async function onSubmit(factor: SnAuthFactor) {
|
||||
try {
|
||||
setLoading(true)
|
||||
await sni.post('/cgi/id/auth/factors/' + factor.id)
|
||||
onNext(factor)
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse in={!!error} sx={{ width: 320 }}>
|
||||
<Alert sx={{ mb: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', width: '100%', gap: 2, textAlign: 'center' }}>
|
||||
<ButtonGroup orientation="vertical" aria-label="Vertical button group">
|
||||
{factorList.map((factor) => (
|
||||
<Button
|
||||
sx={{ py: 1 }}
|
||||
key={factor.id}
|
||||
onClick={() => onSubmit(factor)}
|
||||
disabled={loading || ticket.factorTrail?.includes(factor.id)}
|
||||
startIcon={factorTypeIcons[factor.type]}
|
||||
>
|
||||
{factorTypeLabels[factor.type]}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
|
||||
<Typography variant="caption" sx={{ opacity: 0.75, mx: 2 }}>
|
||||
{ticket.stepRemain} step(s) left
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { ArrowForward } from '@mui/icons-material'
|
||||
import { Alert, Box, Button, Collapse, Link, TextField, Typography } from '@mui/material'
|
||||
import { SnAuthFactor, SnAuthResult, SnAuthTicket } from 'solar-js-sdk'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import NextLink from 'next/link'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
|
||||
export type SnLoginStartForm = {
|
||||
username: string
|
||||
}
|
||||
|
||||
export function SnLoginStart({ onNext }: { onNext: (val: SnAuthTicket, fcs: SnAuthFactor[]) => void }) {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
|
||||
const { handleSubmit, register } = useForm<SnLoginStartForm>()
|
||||
|
||||
async function onSubmit(data: any) {
|
||||
try {
|
||||
setLoading(true)
|
||||
const resp = await sni.post<SnAuthResult>('/cgi/id/auth', data)
|
||||
const factorResp = await sni.get<SnAuthFactor[]>('/cgi/id/auth/factors', {
|
||||
params: {
|
||||
ticketId: resp.data.ticket.id,
|
||||
},
|
||||
})
|
||||
onNext(resp.data.ticket, factorResp.data)
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert sx={{ mb: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', width: '100%', gap: 2, textAlign: 'center' }}>
|
||||
<TextField
|
||||
label="Username"
|
||||
helperText="You can also use email address and phone number"
|
||||
{...register('username', { required: true })}
|
||||
/>
|
||||
|
||||
<Button variant="contained" endIcon={<ArrowForward />} disabled={loading} type="submit">
|
||||
Next
|
||||
</Button>
|
||||
|
||||
<Typography variant="caption" sx={{ opacity: 0.75, mx: 2 }}>
|
||||
By continuing means you agree to our{' '}
|
||||
<NextLink href="/terms/privacy-policy" passHref>
|
||||
<Link component="span">Privacy Policy</Link>
|
||||
</NextLink>{' '}
|
||||
and{' '}
|
||||
<NextLink href="/terms/user-agreements" passHref>
|
||||
<Link component="span">User Agreements</Link>
|
||||
</NextLink>
|
||||
</Typography>
|
||||
</Box>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
import { Noto_Serif_TC } from 'next/font/google'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export interface TimeDiff {
|
||||
days: number
|
||||
hours: number
|
||||
minutes: number
|
||||
seconds: number
|
||||
isCountdown: boolean
|
||||
}
|
||||
|
||||
const serifFont = Noto_Serif_TC({
|
||||
weight: ['400', '500', '700'],
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
export function CountdownTimer({ targetDate, onUpdate }: { targetDate: Date; onUpdate: (diff: TimeDiff) => void }) {
|
||||
const [timeDiff, setTimeDiff] = useState({
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isCountdown: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const updateTimeDiff = () => {
|
||||
const now = new Date()
|
||||
const diff = targetDate.getTime() - now.getTime()
|
||||
|
||||
const absDiff = Math.abs(diff)
|
||||
const isCountdown = diff > 0
|
||||
|
||||
const days = Math.floor(absDiff / (1000 * 60 * 60 * 24))
|
||||
const hours = Math.floor((absDiff / (1000 * 60 * 60)) % 24)
|
||||
const minutes = Math.floor((absDiff / (1000 * 60)) % 60)
|
||||
const seconds = Math.floor((absDiff / 1000) % 60)
|
||||
|
||||
setTimeDiff({ days, hours, minutes, seconds, isCountdown })
|
||||
onUpdate({ days, hours, minutes, seconds, isCountdown })
|
||||
}
|
||||
|
||||
const intervalId = setInterval(updateTimeDiff, 1000)
|
||||
|
||||
return () => clearInterval(intervalId)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex gap-5">
|
||||
<div>
|
||||
<span className="countdown font-mono text-4xl">
|
||||
<span style={{ '--value': timeDiff.days } as any}></span>
|
||||
</span>
|
||||
<span className={serifFont.className}>天</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="countdown font-mono text-4xl">
|
||||
<span style={{ '--value': timeDiff.hours } as any}></span>
|
||||
</span>
|
||||
<span className={serifFont.className}>小时</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="countdown font-mono text-4xl">
|
||||
<span style={{ '--value': timeDiff.minutes } as any}></span>
|
||||
</span>
|
||||
<span className={serifFont.className}>分钟</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="countdown font-mono text-4xl">
|
||||
<span style={{ '--value': timeDiff.seconds } as any}></span>
|
||||
</span>
|
||||
<span className={serifFont.className}>秒</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import { checkAuthenticatedClient, redirectToLogin } from 'solar-js-sdk'
|
||||
import { JSX, useEffect } from 'react'
|
||||
import { DashboardLayout, Navigation } from '@toolpad/core'
|
||||
import { Box, Stack, Typography } from '@mui/material'
|
||||
import NextLink from 'next/link'
|
||||
|
||||
import HomeIcon from '@mui/icons-material/Home'
|
||||
import AppsIcon from '@mui/icons-material/Apps'
|
||||
|
||||
export function ConsoleLayout({ children }: { children: JSX.Element }) {
|
||||
useEffect(() => {
|
||||
if (!checkAuthenticatedClient()) redirectToLogin()
|
||||
}, [])
|
||||
|
||||
const navigation: Navigation = [
|
||||
{
|
||||
segment: '',
|
||||
title: 'Home',
|
||||
icon: <HomeIcon />,
|
||||
},
|
||||
{
|
||||
segment: 'console/matrix',
|
||||
title: 'Matrix',
|
||||
icon: <AppsIcon />,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<DashboardLayout
|
||||
navigation={navigation}
|
||||
branding={{
|
||||
homeUrl: '/console',
|
||||
}}
|
||||
slots={{
|
||||
appTitle(_) {
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
<NextLink passHref href="/console">
|
||||
<Typography variant="h6">Solar Network Console</Typography>
|
||||
</NextLink>
|
||||
</Stack>
|
||||
)
|
||||
},
|
||||
toolbarActions(_) {
|
||||
return <Box />
|
||||
},
|
||||
}}
|
||||
sidebarExpandedWidth={300}
|
||||
defaultSidebarCollapsed
|
||||
>
|
||||
{children}
|
||||
</DashboardLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export function getConsoleStaticProps(original: any) {
|
||||
if (original.props.title) {
|
||||
original.props.title = 'Solar Console | ' + original.props.title
|
||||
}
|
||||
original.props.showAppBar = false
|
||||
|
||||
return original
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
import { Collapse, Alert, TextField, Button, Box } from '@mui/material'
|
||||
import { useRouter } from 'next-nprogress-bar'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { MaProduct } from 'solar-js-sdk'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
|
||||
export interface MatrixProductForm {
|
||||
name: string
|
||||
alias: string
|
||||
description: string
|
||||
introduction: string
|
||||
icon: string
|
||||
previews: string[]
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export default function MaProductForm({
|
||||
onSubmit,
|
||||
onSuccess,
|
||||
defaultValue,
|
||||
}: {
|
||||
onSubmit: (data: MatrixProductForm) => Promise<any>
|
||||
onSuccess?: () => void
|
||||
defaultValue?: MaProduct
|
||||
}) {
|
||||
const { handleSubmit, register } = useForm<MatrixProductForm>({
|
||||
defaultValues: {
|
||||
name: defaultValue?.name ?? '',
|
||||
alias: defaultValue?.alias ?? '',
|
||||
description: defaultValue?.description ?? '',
|
||||
introduction: defaultValue?.meta?.introduction ?? '',
|
||||
icon: defaultValue?.icon ?? '',
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const [previews, setPreviews] = useState<string[]>([])
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue?.previews) {
|
||||
setPreviews(defaultValue.previews)
|
||||
}
|
||||
if (defaultValue?.tags) {
|
||||
setTags(defaultValue.tags)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [busy, setBusy] = useState<boolean>(false)
|
||||
|
||||
function callback() {
|
||||
if (onSuccess) {
|
||||
onSuccess()
|
||||
} else {
|
||||
router.push('/console/matrix')
|
||||
}
|
||||
}
|
||||
|
||||
async function submit(data: MatrixProductForm) {
|
||||
try {
|
||||
setBusy(true)
|
||||
await onSubmit({
|
||||
...data,
|
||||
previews,
|
||||
tags,
|
||||
})
|
||||
callback()
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(submit)}>
|
||||
<Box display="flex" flexDirection="column" maxWidth="sm" gap={2.5}>
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<TextField label="Icon" placeholder="Image URL or Attachment RID" {...register('icon')} />
|
||||
|
||||
<TextField
|
||||
label="Previews"
|
||||
placeholder="Comma separated, Image URL or Attachment RID, the first one will be used as the banner"
|
||||
value={previews.join(',')}
|
||||
onChange={(val) => setPreviews(val.target.value.split(',').map((v) => v.trim()))}
|
||||
/>
|
||||
|
||||
<TextField label="Name" {...register('name')} />
|
||||
|
||||
<TextField label="Alias" {...register('alias')} />
|
||||
|
||||
<TextField
|
||||
label="Tags"
|
||||
placeholder="Comma separated"
|
||||
value={tags.join(',')}
|
||||
onChange={(val) => setTags(val.target.value.split(',').map((v) => v.trim()))}
|
||||
/>
|
||||
|
||||
<TextField minRows={3} maxRows={3} multiline label="Description" {...register('description')} />
|
||||
|
||||
<TextField minRows={5} multiline label="Introduction" {...register('introduction')} />
|
||||
|
||||
<Box sx={{ mt: 5 }} display="flex" gap={2}>
|
||||
<Button variant="contained" type="submit" disabled={busy}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button onClick={callback} variant="outlined" disabled={busy}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
)
|
||||
}
|
@ -1,428 +0,0 @@
|
||||
import {
|
||||
Collapse,
|
||||
Alert,
|
||||
TextField,
|
||||
Button,
|
||||
Box,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Typography,
|
||||
Grid2 as Grid,
|
||||
IconButton,
|
||||
Card,
|
||||
} from '@mui/material'
|
||||
import { useRouter } from 'next-nprogress-bar'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import {
|
||||
MaProduct,
|
||||
MaRelease,
|
||||
MaReleaseAsset,
|
||||
MaReleaseInstaller,
|
||||
MaReleaseInstallerPatch,
|
||||
MaReleaseRunner,
|
||||
} from 'solar-js-sdk'
|
||||
import MonacoEditor from '@monaco-editor/react'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
|
||||
export interface MatrixReleaseForm {
|
||||
version: string
|
||||
type: number
|
||||
channel: string
|
||||
title: string
|
||||
description: string
|
||||
content: string
|
||||
assets: Record<string, MaReleaseAsset>
|
||||
installers: Record<string, MaReleaseInstaller>
|
||||
runners: Record<string, MaReleaseRunner>
|
||||
attachments: string[]
|
||||
}
|
||||
|
||||
export default function MaReleaseForm({
|
||||
onSubmit,
|
||||
onSuccess,
|
||||
parent,
|
||||
defaultValue,
|
||||
}: {
|
||||
onSubmit: (data: MatrixReleaseForm) => Promise<any>
|
||||
onSuccess?: () => void
|
||||
parent: Partial<MaProduct>
|
||||
defaultValue?: MaRelease
|
||||
}) {
|
||||
const { handleSubmit, register } = useForm<MatrixReleaseForm>({
|
||||
defaultValues: {
|
||||
title: defaultValue?.meta.title,
|
||||
version: defaultValue?.version,
|
||||
type: defaultValue?.type ?? 0,
|
||||
channel: defaultValue?.channel,
|
||||
description: defaultValue?.meta.description,
|
||||
content: defaultValue?.meta.content,
|
||||
attachments: defaultValue?.meta.attachments,
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue?.assets) {
|
||||
setAssets(Object.keys(defaultValue.assets).map((k) => ({ k, v: defaultValue.assets[k] })))
|
||||
}
|
||||
if (defaultValue?.installers) {
|
||||
setInstallers(Object.keys(defaultValue.installers).map((k) => ({ k, v: defaultValue.installers[k] })))
|
||||
}
|
||||
if (defaultValue?.runners) {
|
||||
setRunners(Object.keys(defaultValue.runners).map((k) => ({ k, v: defaultValue.runners[k] })))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const [assets, setAssets] = useState<{ k: string; v: MaReleaseAsset }[]>([])
|
||||
const [installers, setInstallers] = useState<{ k: string; v: MaReleaseInstaller }[]>([])
|
||||
const [runners, setRunners] = useState<{ k: string; v: MaReleaseRunner }[]>([])
|
||||
|
||||
function addAsset() {
|
||||
setAssets((val) => [...val, { k: '', v: { uri: '', contentType: '' } }])
|
||||
}
|
||||
|
||||
function addInstaller() {
|
||||
setInstallers((val) => [...val, { k: '', v: { workdir: '', script: '', patches: [] } }])
|
||||
}
|
||||
|
||||
function addRunner() {
|
||||
setRunners((val) => [...val, { k: '', v: { workdir: '', script: '', label: '' } }])
|
||||
}
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [busy, setBusy] = useState<boolean>(false)
|
||||
|
||||
function callback() {
|
||||
if (onSuccess) {
|
||||
onSuccess()
|
||||
} else {
|
||||
router.push(`/console/matrix/products/${parent?.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function submit(data: MatrixReleaseForm) {
|
||||
try {
|
||||
setBusy(true)
|
||||
await onSubmit({
|
||||
...data,
|
||||
assets: assets.reduce((a, { k, v }) => ({ ...a, [k]: v }), {}),
|
||||
installers: installers.reduce((a, { k, v }) => ({ ...a, [k]: v }), {}),
|
||||
runners: runners.reduce((a, { k, v }) => ({ ...a, [k]: v }), {}),
|
||||
})
|
||||
callback()
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(submit)}>
|
||||
<Box display="flex" flexDirection="column" maxWidth="sm" gap={2.5}>
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<TextField label="Version" placeholder="Major.Minor.Patch" {...register('version', { required: true })} />
|
||||
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="release-type">Type</InputLabel>
|
||||
<Select
|
||||
labelId="release-type"
|
||||
label="Type"
|
||||
defaultValue={defaultValue?.type}
|
||||
{...register('type', { required: true })}
|
||||
>
|
||||
<MenuItem value={0}>Full Release</MenuItem>
|
||||
<MenuItem value={1}>Patch Release</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField label="Title" {...register('title')} />
|
||||
|
||||
<TextField label="Alias" {...register('channel')} />
|
||||
|
||||
<TextField minRows={3} maxRows={3} multiline label="Description" {...register('description')} />
|
||||
|
||||
<TextField minRows={5} multiline label="Content" {...register('content')} />
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="h5">Assets</Typography>
|
||||
|
||||
{assets.map(({ k, v }, idx) => (
|
||||
<Card variant="outlined" key={idx}>
|
||||
<Box sx={{ pl: 2, pr: 4, py: 2 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={11}>
|
||||
<TextField
|
||||
label="Platform"
|
||||
sx={{ width: '100%' }}
|
||||
value={k}
|
||||
onChange={(val) => {
|
||||
setAssets((data) =>
|
||||
data.map((ele, index) => (index == idx ? { k: val.target.value, v: ele.v } : ele)),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={1} sx={{ display: 'grid', placeItems: 'center' }}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setAssets((data) => data.filter((_, index) => index != idx))
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid size={8}>
|
||||
<TextField
|
||||
label="URI"
|
||||
sx={{ width: '100%' }}
|
||||
value={v.uri}
|
||||
onChange={(val) => {
|
||||
setAssets((data) =>
|
||||
data.map((ele, index) =>
|
||||
index == idx ? { v: { ...ele.v, uri: val.target.value }, k: ele.k } : ele,
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<TextField
|
||||
label="Content Type"
|
||||
sx={{ width: '100%' }}
|
||||
value={v.contentType}
|
||||
onChange={(val) => {
|
||||
setAssets((data) =>
|
||||
data.map((ele, index) =>
|
||||
index == idx ? { v: { ...ele.v, contentType: val.target.value }, k: ele.k } : ele,
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<Box>
|
||||
<Button variant="outlined" onClick={addAsset}>
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="h5">Installers</Typography>
|
||||
|
||||
{installers.map(({ k, v }, idx) => (
|
||||
<Card variant="outlined" key={idx}>
|
||||
<Box sx={{ pl: 2, pr: 4, py: 2 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={4}>
|
||||
<TextField
|
||||
label="Platform"
|
||||
sx={{ width: '100%' }}
|
||||
value={k}
|
||||
onChange={(val) => {
|
||||
setInstallers((data) =>
|
||||
data.map((ele, index) => (index == idx ? { k: val.target.value, v: ele.v } : ele)),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={7}>
|
||||
<TextField
|
||||
label="Working Directory"
|
||||
sx={{ width: '100%' }}
|
||||
value={v.workdir}
|
||||
onChange={(val) => {
|
||||
setInstallers((data) =>
|
||||
data.map((ele, index) =>
|
||||
index == idx ? { k: ele.k, v: { ...ele.v, workdir: val.target.value } } : ele,
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={1} sx={{ display: 'grid', placeItems: 'center' }}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setInstallers((data) => data.filter((_, index) => index != idx))
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Typography variant="subtitle1" sx={{ mx: 1 }}>
|
||||
Script
|
||||
</Typography>
|
||||
<Card variant="outlined">
|
||||
<MonacoEditor
|
||||
height="140px"
|
||||
width="100%"
|
||||
options={{ minimap: { enabled: false } }}
|
||||
defaultValue={v.script}
|
||||
onChange={(val) =>
|
||||
setInstallers((data) =>
|
||||
data.map((ele, index) => (index == idx ? { v: { ...ele.v, script: val }, k: ele.k } : ele)),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Typography variant="subtitle1" sx={{ mx: 1 }}>
|
||||
Patches
|
||||
</Typography>
|
||||
<Card variant="outlined">
|
||||
<MonacoEditor
|
||||
height="140px"
|
||||
width="100%"
|
||||
options={{ minimap: { enabled: false } }}
|
||||
defaultValue={v.patches.map((p) => `${p.action}:${p.glob}`).join('\n')}
|
||||
onChange={(val) =>
|
||||
setInstallers((data) =>
|
||||
data.map((ele, index) =>
|
||||
index == idx
|
||||
? {
|
||||
v: {
|
||||
...ele.v,
|
||||
patches: val?.split('\n')?.map((p) => ({
|
||||
action: p.split(':')[0],
|
||||
glob: p.split(':')[1],
|
||||
})) as MaReleaseInstallerPatch[],
|
||||
},
|
||||
k: ele.k,
|
||||
}
|
||||
: ele,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<Box>
|
||||
<Button variant="outlined" onClick={addInstaller}>
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="h5">Runners</Typography>
|
||||
|
||||
{runners.map(({ k, v }, idx) => (
|
||||
<Card variant="outlined" key={idx}>
|
||||
<Box sx={{ pl: 2, pr: 4, py: 2 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={4}>
|
||||
<TextField
|
||||
label="Platform"
|
||||
sx={{ width: '100%' }}
|
||||
value={k}
|
||||
onChange={(val) => {
|
||||
setRunners((data) =>
|
||||
data.map((ele, index) => (index == idx ? { k: val.target.value, v: ele.v } : ele)),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={7}>
|
||||
<TextField
|
||||
label="Working Directory"
|
||||
sx={{ width: '100%' }}
|
||||
value={v.workdir}
|
||||
onChange={(val) => {
|
||||
setRunners((data) =>
|
||||
data.map((ele, index) =>
|
||||
index == idx ? { k: ele.k, v: { ...ele.v, workdir: val.target.value } } : ele,
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={1} sx={{ display: 'grid', placeItems: 'center' }}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setRunners((data) => data.filter((_, index) => index != idx))
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
label="Label"
|
||||
sx={{ width: '100%' }}
|
||||
value={v.label}
|
||||
onChange={(val) => {
|
||||
setRunners((data) =>
|
||||
data.map((ele, index) =>
|
||||
index == idx ? { k: ele.k, v: { ...ele.v, label: val.target.value } } : ele,
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Typography variant="subtitle1" sx={{ mx: 1 }}>
|
||||
Script
|
||||
</Typography>
|
||||
<Card variant="outlined">
|
||||
<MonacoEditor
|
||||
height="280px"
|
||||
width="100%"
|
||||
options={{ minimap: { enabled: false } }}
|
||||
defaultValue={v.script}
|
||||
onChange={(val) =>
|
||||
setRunners((data) =>
|
||||
data.map((ele, index) =>
|
||||
index == idx ? { v: { ...ele.v, script: val ?? '' }, k: ele.k } : ele,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<Box>
|
||||
<Button variant="outlined" onClick={addRunner}>
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 5 }} display="flex" gap={2}>
|
||||
<Button variant="contained" type="submit" disabled={busy}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button onClick={callback} variant="outlined" disabled={busy}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
)
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
import '@/styles/globals.css'
|
||||
import type { AppProps } from 'next/app'
|
||||
import { Box, createTheme, CssBaseline, ThemeProvider } from '@mui/material'
|
||||
import { Roboto } from 'next/font/google'
|
||||
import { CapAppBar } from '@/components/CapAppBar'
|
||||
import { PagesProgressBar as ProgressBar } from 'next-nprogress-bar'
|
||||
import { AppProvider } from '@toolpad/core/nextjs'
|
||||
import { useUserStore } from 'solar-js-sdk'
|
||||
import { useEffect } from 'react'
|
||||
import Head from 'next/head'
|
||||
|
||||
const fontRoboto = Roboto({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '500', '700'],
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
const siteTheme = createTheme({
|
||||
cssVariables: true,
|
||||
colorSchemes: {
|
||||
dark: true,
|
||||
},
|
||||
palette: {
|
||||
mode: 'light',
|
||||
primary: {
|
||||
main: '#3949ab',
|
||||
},
|
||||
secondary: {
|
||||
main: '#1e88e5',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const userStore = useUserStore()
|
||||
|
||||
useEffect(() => {
|
||||
userStore.fetchUser()
|
||||
}, [])
|
||||
|
||||
const title = pageProps.title
|
||||
? pageProps.title.startsWith('Solar Console')
|
||||
? pageProps.title
|
||||
: `${pageProps.title} | Solsynth LLC`
|
||||
: 'Solsynth LLC'
|
||||
|
||||
return (
|
||||
<>
|
||||
<style jsx global>{`
|
||||
html {
|
||||
font-family: ${fontRoboto.style.fontFamily};
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link rel="icon" href="/favicon.png" type="image/png" />
|
||||
<link rel="apple-touch-icon" href="/apple-icon.png" type="image/png" />
|
||||
</Head>
|
||||
|
||||
<AppProvider>
|
||||
<ThemeProvider theme={siteTheme}>
|
||||
<CssBaseline />
|
||||
<ProgressBar
|
||||
height="4px"
|
||||
color={siteTheme.palette.primary.main}
|
||||
options={{ showSpinner: false }}
|
||||
shallowRouting
|
||||
/>
|
||||
|
||||
{(pageProps.showAppBar ?? true) && <CapAppBar />}
|
||||
<Box sx={{ minHeight: 'calc(100vh - 64px)' }}>
|
||||
<Component {...pageProps} />
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
</AppProvider>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import {
|
||||
AppCacheProvider,
|
||||
DocumentHeadTags,
|
||||
DocumentHeadTagsProps,
|
||||
documentGetInitialProps,
|
||||
} from '@mui/material-nextjs/v15-pagesRouter'
|
||||
import { Html, Head, Main, NextScript, DocumentContext, DocumentProps } from 'next/document'
|
||||
import { GoogleAnalytics } from '@next/third-parties/google'
|
||||
|
||||
export default function Document(props: DocumentProps & DocumentHeadTagsProps) {
|
||||
return (
|
||||
<AppCacheProvider {...props}>
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<DocumentHeadTags {...props} />
|
||||
</Head>
|
||||
<body className="antialiased">
|
||||
<Main />
|
||||
<NextScript />
|
||||
<GoogleAnalytics gaId="G-ZFJ7RX0JXF" />
|
||||
</body>
|
||||
</Html>
|
||||
</AppCacheProvider>
|
||||
)
|
||||
}
|
||||
|
||||
Document.getInitialProps = async (ctx: DocumentContext) => {
|
||||
const finalProps = await documentGetInitialProps(ctx)
|
||||
return finalProps
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export default async function handler(_: NextApiRequest, res: NextApiResponse) {
|
||||
const solarNetworkApi = 'https://api.sn.solsynth.dev'
|
||||
|
||||
const resp = await axios.get(solarNetworkApi + '/cgi/id/well-known/jwks')
|
||||
|
||||
res.status(200).json(resp.data)
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export default async function handler(_: NextApiRequest, res: NextApiResponse) {
|
||||
const siteUrl = 'https://solsynth.dev'
|
||||
const solarNetworkApi = 'https://api.sn.solsynth.dev'
|
||||
|
||||
const resp = await axios.get(solarNetworkApi + '/cgi/id/well-known/openid-configuration')
|
||||
const out: Record<string, any> = resp.data
|
||||
|
||||
out['authorization_endpoint'] = siteUrl + '/auth/authorize'
|
||||
out['jwks_uri'] = siteUrl + '/.well-known/jwks'
|
||||
|
||||
for (const [k, v] of Object.entries(out)) {
|
||||
if (typeof v === 'string') {
|
||||
if (v.startsWith('https://id.solsynth.dev/api')) {
|
||||
out[k] = v.replace('https://id.solsynth.dev/api', solarNetworkApi + '/cgi/id')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json(out)
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
import { AttachmentItem } from '@/components/attachments/AttachmentItem'
|
||||
import { SnAttachment } from 'solar-js-sdk'
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { Box, ImageList, ImageListItem, Pagination, useMediaQuery, useTheme } from '@mui/material'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
let page: number = parseInt(context.query.page as string)
|
||||
if (isNaN(page)) page = 1
|
||||
|
||||
const countPerPage = 20
|
||||
|
||||
const { data: resp } = await sni.get<{ data: SnAttachment[]; count: number }>('/cgi/uc/attachments', {
|
||||
params: {
|
||||
take: countPerPage,
|
||||
offset: (page - 1) * countPerPage,
|
||||
},
|
||||
})
|
||||
|
||||
const attachments = resp.data
|
||||
|
||||
return { props: { attachments, page, pages: Math.ceil(resp.count / countPerPage) } }
|
||||
}
|
||||
|
||||
export default function AttachmentsPage({
|
||||
attachments,
|
||||
page,
|
||||
pages,
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const router = useRouter()
|
||||
const theme = useTheme()
|
||||
const breakpoints = useMediaQuery(theme.breakpoints.up('md'))
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ImageList variant="masonry" cols={breakpoints ? 3 : 2} gap={8} sx={{ mx: 2 }}>
|
||||
{attachments.map((item: SnAttachment) => (
|
||||
<ImageListItem key={item.rid}>
|
||||
<AttachmentItem item={item} borderRadius="0" />
|
||||
</ImageListItem>
|
||||
))}
|
||||
</ImageList>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mx: 'auto',
|
||||
mb: 5,
|
||||
mt: 3,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
placeItems: 'center',
|
||||
gap: 1.5,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Pagination count={pages} page={page} onChange={(_, page) => router.push('/attachments?page=' + page)} />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
import {
|
||||
styled,
|
||||
Button,
|
||||
Container,
|
||||
Card,
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
IconButton,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
LinearProgress,
|
||||
Typography,
|
||||
Alert,
|
||||
Collapse,
|
||||
} from '@mui/material'
|
||||
import { MultipartProgress, SnAttachment, UploadAttachmentTask } from 'solar-js-sdk'
|
||||
import { useState } from 'react'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import CloudUploadIcon from '@mui/icons-material/CloudUpload'
|
||||
import PlayIcon from '@mui/icons-material/PlayArrow'
|
||||
|
||||
const VisuallyHiddenInput = styled('input')({
|
||||
clip: 'rect(0 0 0 0)',
|
||||
clipPath: 'inset(50%)',
|
||||
height: 1,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
width: 1,
|
||||
})
|
||||
|
||||
interface FileUploadTask {
|
||||
file: File
|
||||
attachment?: SnAttachment
|
||||
}
|
||||
|
||||
export default function AttachmentNew() {
|
||||
const [files, setFiles] = useState<FileUploadTask[]>([])
|
||||
|
||||
const [busy, setBusy] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [task, setTask] = useState<FileUploadTask>()
|
||||
const [taskProgress, setTaskProgress] = useState<MultipartProgress>()
|
||||
|
||||
const [pool, setPool] = useState<string>('interactive')
|
||||
|
||||
async function upload() {
|
||||
if (files.length == 0) return
|
||||
|
||||
setBusy(true)
|
||||
|
||||
for (let idx = 0; idx < files.length; idx++) {
|
||||
if (files[idx].attachment) continue
|
||||
|
||||
try {
|
||||
const task = new UploadAttachmentTask(files[idx].file, pool)
|
||||
setTask(files[idx])
|
||||
task.onProgress = (progress) => setTaskProgress(progress)
|
||||
task.onError = (err) => setError(err)
|
||||
const attachment = await task.submit()
|
||||
setFiles((files) => files.map((f, i) => (i == idx ? { ...f, attachment } : f)))
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
setBusy(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setBusy(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
height: 'calc(100vh - 64px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<Box sx={{ width: '100%', mx: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button
|
||||
component="label"
|
||||
role={undefined}
|
||||
variant={files.length == 0 ? 'contained' : 'text'}
|
||||
tabIndex={-1}
|
||||
startIcon={<CloudUploadIcon />}
|
||||
>
|
||||
Pick files
|
||||
<VisuallyHiddenInput
|
||||
type="file"
|
||||
onChange={(event) =>
|
||||
setFiles(
|
||||
Array.from(event.target.files ?? []).map((f) => ({
|
||||
file: f,
|
||||
})),
|
||||
)
|
||||
}
|
||||
multiple
|
||||
/>
|
||||
</Button>
|
||||
{files.length > 0 && (
|
||||
<Button
|
||||
color="success"
|
||||
variant="contained"
|
||||
startIcon={<PlayIcon />}
|
||||
disabled={busy}
|
||||
onClick={() => upload()}
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert sx={{ mb: 5 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
{taskProgress && (
|
||||
<Box sx={{ mt: 5, width: '100%', textAlign: 'center' }}>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{task?.file.name ?? 'Waiting...'}
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
value={taskProgress?.value ? taskProgress.value * 100 : 0}
|
||||
sx={{ borderRadius: 4 }}
|
||||
variant="determinate"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{files.length > 0 && (
|
||||
<Box sx={{ mt: 5, width: '100%' }}>
|
||||
<Card variant="outlined">
|
||||
<List>
|
||||
{files?.map((f, idx) => (
|
||||
<ListItem
|
||||
dense
|
||||
key={idx}
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="delete"
|
||||
disabled={busy}
|
||||
sx={{ marginRight: 1 }}
|
||||
onClick={() => setFiles((files) => files.filter((_, i) => i != idx))}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ListItemText primary={f.file.name} secondary={f.attachment?.rid} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Card>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<FormControl fullWidth sx={{ mt: 5 }}>
|
||||
<InputLabel id="attachment-pool">Attachment Pool</InputLabel>
|
||||
<Select
|
||||
labelId="attachment-pool"
|
||||
value={pool}
|
||||
disabled={busy}
|
||||
label="Attachment Pool"
|
||||
onChange={(evt) => setPool(evt.target.value)}
|
||||
>
|
||||
<MenuItem value={'interactive'}>Interactive</MenuItem>
|
||||
<MenuItem value={'messaging'}>Messaging</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { Container, Box, Typography, Alert, Collapse, Button, CircularProgress, Card, CardContent } from '@mui/material'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { checkAuthenticatedClient, redirectToLogin, SnAuthTicket } from 'solar-js-sdk'
|
||||
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import CheckIcon from '@mui/icons-material/Check'
|
||||
|
||||
export default function AccountAuthorize() {
|
||||
useEffect(() => {
|
||||
if (!checkAuthenticatedClient()) redirectToLogin()
|
||||
}, [])
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const [thirdClient, setThirdClient] = useState<any>(null)
|
||||
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [reverting, setReverting] = useState(false)
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
function doCallback(ticket: SnAuthTicket) {
|
||||
const url = `${router.query['redirect_uri']}?code=${ticket.grantToken}&state=${router.query['state']}`
|
||||
window.open(url, '_self')
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
try {
|
||||
setReverting(true)
|
||||
const resp = await sni.get<{ ticket: SnAuthTicket; client: any }>(
|
||||
'/cgi/id/auth/o/authorize' + window.location.search,
|
||||
{},
|
||||
)
|
||||
if (resp.data.ticket) {
|
||||
return doCallback(resp.data.ticket)
|
||||
}
|
||||
setThirdClient(resp.data.client)
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setReverting(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetch()
|
||||
}, [])
|
||||
|
||||
async function confirm() {
|
||||
try {
|
||||
setBusy(true)
|
||||
const resp = await sni.post<{ ticket: SnAuthTicket }>('/cgi/id/auth/o/authorize' + window.location.search)
|
||||
return doCallback(resp.data.ticket)
|
||||
} catch (err: any) {
|
||||
setError(err.toString())
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
function decline() {
|
||||
if (window.history.length > 0) {
|
||||
window.history.back()
|
||||
} else {
|
||||
window.close()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
height: 'calc(100vh - 64px)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Typography variant="h5" component="h1">
|
||||
Connect with Solarpass
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" component="h2">
|
||||
Connect third-party services with Solar Network
|
||||
</Typography>
|
||||
|
||||
<Collapse in={!!error} sx={{ width: '100%' }}>
|
||||
<Alert sx={{ mt: 4 }} icon={<ErrorIcon fontSize="inherit" />} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
{reverting && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!reverting && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Card variant="outlined" sx={{ width: '100%' }}>
|
||||
<CardContent sx={{ textAlign: 'left', px: 2.5 }}>
|
||||
<Typography variant="h6">{thirdClient?.name}</Typography>
|
||||
<Typography variant="body2">{thirdClient?.description}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Button sx={{ mt: 3 }} startIcon={<CloseIcon />} onClick={() => decline()} disabled={busy} color="error">
|
||||
Decline
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ mt: 3 }}
|
||||
startIcon={<CheckIcon />}
|
||||
onClick={() => confirm()}
|
||||
disabled={busy}
|
||||
color="success"
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
import { SnLoginCheckpoint } from '@/components/auth/SnLoginCheckpoint'
|
||||
import { SnLoginRouter } from '@/components/auth/SnLoginRouter'
|
||||
import { SnLoginStart } from '@/components/auth/SnLoginStart'
|
||||
import { SnAuthFactor, SnAuthTicket } from 'solar-js-sdk'
|
||||
import { useUserStore } from 'solar-js-sdk'
|
||||
import { Box, Container, Typography } from '@mui/material'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function Login() {
|
||||
const [period, setPeriod] = useState<number>(0)
|
||||
const [ticket, setTicket] = useState<SnAuthTicket | null>(null)
|
||||
const [factorList, setFactorList] = useState<SnAuthFactor[]>([])
|
||||
const [factor, setFactor] = useState<SnAuthFactor | null>(null)
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
function doCallback() {
|
||||
if (router.query['redirect_url']) {
|
||||
let redirectUrl: string
|
||||
if (Array.isArray(router.query['redirect_url'])) {
|
||||
redirectUrl = router.query['redirect_url'][0]
|
||||
} else {
|
||||
redirectUrl = router.query['redirect_url'].toString()
|
||||
}
|
||||
|
||||
if (redirectUrl.startsWith('/')) {
|
||||
router.push(redirectUrl)
|
||||
} else {
|
||||
window.open(redirectUrl, '_self')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
router.push('/users/me')
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
switch (period) {
|
||||
case 1:
|
||||
return (
|
||||
<SnLoginRouter
|
||||
ticket={ticket!}
|
||||
factorList={factorList}
|
||||
onNext={(val) => {
|
||||
setPeriod(period + 1)
|
||||
setFactor(val)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
case 2:
|
||||
return (
|
||||
<SnLoginCheckpoint
|
||||
ticket={ticket!}
|
||||
factor={factor!}
|
||||
onNext={(val, done) => {
|
||||
if (!done) {
|
||||
setTicket(val)
|
||||
setPeriod(1)
|
||||
return
|
||||
}
|
||||
userStore.fetchUser()
|
||||
doCallback()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<SnLoginStart
|
||||
onNext={(val, fcs) => {
|
||||
setPeriod(period + 1)
|
||||
setTicket(val)
|
||||
setFactorList(fcs)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
height: 'calc(100vh - 64px)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Typography variant="h5" component="h1">
|
||||
Login
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" component="h2">
|
||||
Login via Solarpass
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mt: 3 }}>{renderForm()}</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
||||
import { Typography, Container, Box, Grid2 as Grid, Card, CardContent, CardActionArea } from '@mui/material'
|
||||
import NextLink from 'next/link'
|
||||
|
||||
import DynamicFormIcon from '@mui/icons-material/DynamicForm'
|
||||
import AppsIcon from '@mui/icons-material/Apps'
|
||||
|
||||
export function getStaticProps() {
|
||||
return getConsoleStaticProps({
|
||||
props: {
|
||||
title: 'Welcome',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default function ConsoleLanding() {
|
||||
return (
|
||||
<ConsoleLayout>
|
||||
<Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<Box>
|
||||
<DynamicFormIcon sx={{ fontSize: 64, mb: 2 }} />
|
||||
<Typography variant="subtitle2">Welcome to the</Typography>
|
||||
<Typography variant="h3" component="h1">
|
||||
Console
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">of the Solar Network</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container columns={{ xs: 1, sm: 2, md: 3 }} spacing={4}>
|
||||
<Grid size={1}>
|
||||
<NextLink passHref href="/console/matrix">
|
||||
<CardActionArea>
|
||||
<Card sx={{ width: '100%' }}>
|
||||
<CardContent>
|
||||
<AppsIcon sx={{ fontSize: 32, mb: 1.5 }} />
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Matrix
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
Publish and versioning your application with Matrix Marketplace.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardActionArea>
|
||||
</NextLink>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</ConsoleLayout>
|
||||
)
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
||||
import { MaProduct } from 'solar-js-sdk'
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { Typography, Container, Box, Button, Grid2 as Grid, Card, CardContent, CardActions } from '@mui/material'
|
||||
import NextLink from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export async function getStaticProps() {
|
||||
return getConsoleStaticProps({
|
||||
props: {
|
||||
title: 'Matrix Marketplace',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default function MatrixMarketplace() {
|
||||
const [products, setProducts] = useState<MaProduct[]>([])
|
||||
|
||||
async function fetchProducts() {
|
||||
const { data: resp } = await sni.get<{ data: MaProduct[] }>('/cgi/ma/products/created', {
|
||||
params: {
|
||||
take: 10,
|
||||
},
|
||||
})
|
||||
setProducts(resp.data)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts()
|
||||
}, [])
|
||||
|
||||
async function deleteProduct(id: number) {
|
||||
const yes = confirm(`Are you sure you want to delete this product #${id}?`)
|
||||
if (!yes) return
|
||||
|
||||
await sni.delete('/cgi/ma/products/' + id)
|
||||
await fetchProducts()
|
||||
}
|
||||
|
||||
return (
|
||||
<ConsoleLayout>
|
||||
<Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<Box>
|
||||
<Typography variant="h3" component="h1">
|
||||
Matrix Marketplace
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
The new way to release your app, implement version check and auto updating.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" flexDirection="column" gap={2}>
|
||||
<Box>
|
||||
<NextLink passHref href="/console/matrix/products/new">
|
||||
<Button variant="contained">Create a product</Button>
|
||||
</NextLink>
|
||||
</Box>
|
||||
|
||||
<Grid container columns={{ xs: 1, sm: 2, md: 3 }} spacing={4}>
|
||||
{products.map((p) => (
|
||||
<Grid size={1} key={p.id}>
|
||||
<Card sx={{ width: '100%' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
{p.name}
|
||||
</Typography>
|
||||
<Typography variant="body1">{p.description}</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<NextLink passHref href={`/console/matrix/products/${p.id}`}>
|
||||
<Button size="small">Details</Button>
|
||||
</NextLink>
|
||||
<NextLink passHref href={`/console/matrix/products/${p.id}/edit`}>
|
||||
<Button size="small">Edit</Button>
|
||||
</NextLink>
|
||||
<Button size="small" color="error" onClick={() => deleteProduct(p.id)}>
|
||||
Delete
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Container>
|
||||
</ConsoleLayout>
|
||||
)
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
||||
import { Typography, Container, Box } from '@mui/material'
|
||||
import { MaProduct, sni } from 'solar-js-sdk'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
import MaProductForm, { MatrixProductForm } from '@/components/matrix/MaProductForm'
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = (async (context) => {
|
||||
const id = context.params!.id
|
||||
|
||||
const { data } = await sni.get<MaProduct>('/cgi/ma/products/' + id)
|
||||
|
||||
return getConsoleStaticProps({
|
||||
props: {
|
||||
title: `Edit Product "${data.name}"`,
|
||||
product: data,
|
||||
},
|
||||
})
|
||||
}) satisfies GetServerSideProps<{ product: MaProduct }>
|
||||
|
||||
export default function ProductEdit({ product }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
async function onSubmit(data: MatrixProductForm) {
|
||||
await sni.put('/cgi/ma/products/' + product.id, data)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConsoleLayout>
|
||||
<Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<Box>
|
||||
<Typography variant="h3" component="h1">
|
||||
Edit product
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">{product.name}</Typography>
|
||||
</Box>
|
||||
|
||||
<MaProductForm onSubmit={onSubmit} defaultValue={product} />
|
||||
</Container>
|
||||
</ConsoleLayout>
|
||||
)
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
||||
import { Box, Button, Container, Typography, Grid2 as Grid, Card, CardContent, CardActions } from '@mui/material'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
import { sni, MaProduct, MaRelease, getAttachmentUrl } from 'solar-js-sdk'
|
||||
import { useEffect, useState } from 'react'
|
||||
import NextLink from 'next/link'
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = (async (context) => {
|
||||
const id = context.params!.id
|
||||
|
||||
const { data } = await sni.get<MaProduct>('/cgi/ma/products/' + id)
|
||||
|
||||
return getConsoleStaticProps({
|
||||
props: {
|
||||
title: `Product "${data.name}"`,
|
||||
product: data,
|
||||
},
|
||||
})
|
||||
}) satisfies GetServerSideProps<{ product: MaProduct }>
|
||||
|
||||
export default function ProductDetails({ product }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const [releases, setReleases] = useState<MaRelease[]>([])
|
||||
|
||||
async function fetchReleases() {
|
||||
const { data: resp } = await sni.get<{ data: MaRelease[] }>('/cgi/ma/products/' + product.id + '/releases', {
|
||||
params: {
|
||||
take: 10,
|
||||
},
|
||||
})
|
||||
|
||||
setReleases(resp.data)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchReleases()
|
||||
}, [])
|
||||
|
||||
async function deleteRelease(id: number) {
|
||||
const yes = confirm(`Are you sure you want to delete this release #${id}?`)
|
||||
if (!yes) return
|
||||
|
||||
await sni.delete('/cgi/ma/products/' + product.id + '/releases/' + id)
|
||||
await fetchReleases()
|
||||
}
|
||||
|
||||
return (
|
||||
<ConsoleLayout>
|
||||
<>
|
||||
{product.previews && (
|
||||
<img
|
||||
src={getAttachmentUrl(product.previews[0])}
|
||||
alt={product.name}
|
||||
style={{ objectFit: 'cover', aspectRatio: 16 / 5 }}
|
||||
className='border-b border-1'
|
||||
/>
|
||||
)}
|
||||
|
||||
<Container sx={{ pt: (product.previews ? 8 : 16), pb: 16, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<Box maxWidth="sm">
|
||||
<Typography variant="h3" component="h1">
|
||||
{product.name}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">{product.description}</Typography>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" flexDirection="column" gap={2}>
|
||||
<Typography variant="h4" component="h2">
|
||||
Releases
|
||||
</Typography>
|
||||
|
||||
<NextLink passHref href={`/console/matrix/products/${product.id}/releases/new`}>
|
||||
<Button variant="contained">Create a release</Button>
|
||||
</NextLink>
|
||||
|
||||
<Grid container columns={{ xs: 1, sm: 2, md: 3 }} spacing={2}>
|
||||
{releases.map((r: any) => (
|
||||
<Grid size={1} key={r.id}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="caption">{r.version}</Typography>
|
||||
<Typography variant="h5" component="h2">
|
||||
{r.meta.title}
|
||||
</Typography>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
{r.type == 0 ? 'Full Release' : 'Patch Release'}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1">{r.meta.description}</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<NextLink passHref href={`/console/matrix/products/${r.productId}/releases/${r.id}/edit`}>
|
||||
<Button size="small">Edit</Button>
|
||||
</NextLink>
|
||||
<Button size="small" color="error" onClick={() => deleteRelease(r.id)}>
|
||||
Delete
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Container>
|
||||
</>
|
||||
</ConsoleLayout>
|
||||
)
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
||||
import { Typography, Container, Box } from '@mui/material'
|
||||
import { sni } from 'solar-js-sdk'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
import MaReleaseForm, { MatrixReleaseForm } from '@/components/matrix/MaReleaseForm'
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = (async (context) => {
|
||||
const id = context.params!.id
|
||||
const releaseId = context.params!.release
|
||||
|
||||
const { data } = await sni.get<any>('/cgi/ma/products/' + id + '/releases/' + releaseId)
|
||||
|
||||
return getConsoleStaticProps({
|
||||
props: {
|
||||
title: `Edit Release v${data.version}`,
|
||||
release: data,
|
||||
},
|
||||
})
|
||||
}) satisfies GetServerSideProps<{ release: any }>
|
||||
|
||||
export default function ReleaseEdit({ release }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
async function onSubmit(data: MatrixReleaseForm) {
|
||||
await sni.put('/cgi/ma/products/' + release.productId + '/releases/' + release.id, data)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConsoleLayout>
|
||||
<Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<Box>
|
||||
<Typography variant="h3" component="h1">
|
||||
Edit releases
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">{release.meta.title}</Typography>
|
||||
</Box>
|
||||
|
||||
<MaReleaseForm onSubmit={onSubmit} defaultValue={release} parent={{ id: release.productId }} />
|
||||
</Container>
|
||||
</ConsoleLayout>
|
||||
)
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
||||
import { Typography, Container, Box } from '@mui/material'
|
||||
import { MaProduct, sni } from 'solar-js-sdk'
|
||||
|
||||
import MaReleaseForm, { MatrixReleaseForm } from '@/components/matrix/MaReleaseForm'
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = (async (context) => {
|
||||
const id = context.params!.id
|
||||
|
||||
const { data } = await sni.get<MaProduct>('/cgi/ma/products/' + id)
|
||||
|
||||
return getConsoleStaticProps({
|
||||
props: {
|
||||
title: `New Release for "${data.name}"`,
|
||||
product: data,
|
||||
},
|
||||
})
|
||||
}) satisfies GetServerSideProps<{ product: MaProduct }>
|
||||
|
||||
export default function ReleaseNew({ product }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
async function onSubmit(data: MatrixReleaseForm) {
|
||||
await sni.post(`/cgi/ma/products/${product.id}/releases`, data)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConsoleLayout>
|
||||
<Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<Box>
|
||||
<Typography variant="h3" component="h1">
|
||||
Create a release
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">for {product.name}</Typography>
|
||||
</Box>
|
||||
|
||||
<MaReleaseForm onSubmit={onSubmit} parent={product} />
|
||||
</Container>
|
||||
</ConsoleLayout>
|
||||
)
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import { ConsoleLayout, getConsoleStaticProps } from '@/components/layouts/ConsoleLayout'
|
||||
import { Typography, Container } from '@mui/material'
|
||||
import { sni } from 'solar-js-sdk'
|
||||
|
||||
import MaProductForm, { MatrixProductForm } from '@/components/matrix/MaProductForm'
|
||||
|
||||
export async function getStaticProps() {
|
||||
return getConsoleStaticProps({
|
||||
props: {
|
||||
title: 'New Product',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default function ProductNew() {
|
||||
async function onSubmit(data: MatrixProductForm) {
|
||||
await sni.post('/cgi/ma/products', data)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConsoleLayout>
|
||||
<Container sx={{ py: 16, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<Typography variant="h3" component="h1">
|
||||
Create a product
|
||||
</Typography>
|
||||
|
||||
<MaProductForm onSubmit={onSubmit} />
|
||||
</Container>
|
||||
</ConsoleLayout>
|
||||
)
|
||||
}
|