7Go
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
aN&pqAi8D$ugow+D|17ZoXW!E0^uGWAOmNx&
literal 0
HcmV?d00001
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..05186130ef4503c04f5c2a5e3eca3b3e9212f86d
GIT binary patch
literal 4286
zcmeHLdr*{B6#pnwN)eE^;4Um63n)o~1w95D3(Ks0mE)lKie8rGqtc>OQbtFcX(pR8
zVV8aLR1^exibP_pG}b5&3kl5!W{QY{0>93=+ehS})gMfM)G~L^xA(jE{?7fK-#PaV
zl4M7JzP=KFOBN?da+f4YO^v#w7;4-vKJ;$Xa9gu=NRrA)kHKy}_oTsZqz-lPu0QQfNjR~QISBPJ3p`))L+xQgh^I;TG*~|PJL{nso`wm27QB(LAB$!eVL92w
z&le+UO*IzMZ?`*B2aN;9XO7igCKyudu=14ynD=ZU;-Ydf!QXF7tU5s#&
zzQkKgE0MUe8jkclhP1#jk+m>b*u1VBgK)oeM2BVz>_8WT=)WZ{@fJX1c$|u!Z#iy&
zykWeX$8(=@gic`lJJXs2%eZI$#RJgM?9DnzN=Jfe=i}}o_f^bx114jKBhkf`f
zY+j$|dJorN&NKTk_t||=IBJC-97oT)G;qBm1)Ni|zqWPtB9>}TBO(Gb;OnS^x4qWp
z!vK89bNgGHPIe$JHWvpgPob@?6Z|=L^cT#YRsbiv?Rfm*-I(=sA(XT}`^ngx%YGyE
z!ndh0d$4M0CE}+SB6?glJcnwsFA*5mulESsez_BuE_I@(r&s)LsQVS8om0hr
z$GRHeWv@ZvI|t$GmWFMg9Kqy=c2Qk3VJlY+alHJcx~Z+Y~E0fnESKw>Y{S2f3F%#m+Xg5
zdkoX3>=khday7z1+K$a1)e8JmC)?3*^n!TiuZr>(tWB)K`EwoExV{Fi!%{F#VTN8?
zhZ#@hO3)W>o$cV;1$zjgpCmVSk#
z52|6*)FblV3@nN-#)fs@prr656fS9)J+%OlK^br%zJo|7!F2wN9;U&>;B?#msgLnT
zNMFGE%~^rb)!8jrPz_#QScU+qNz8qOIs+DC
z1M+tNNZPkypFZ6#w8M3UIZ}{L;vU?ExwG=2+3`KruRbJvWBn*e<9yBsx*L%2LMg&j
zW~?A?3iF!CRv?q~w1(moLHUyWVGX7~nNPW$4|n?%Z2HIwEA43p@uX5{(9v;S?0siW
zBk}$@SRb6nT;u#HU*kw)ZxatkzHLQi<B>*`Qe
z(t_ylbi7ACEQ~J_8sW2va86lM8yg%MQH}-++PDD^T
zmdr1r9O=SW2Tmc(&jb&uk(`_CBR8&te$G0CDJ_@~U`7@Bx{ma}dPNmBuB{Q=z58Yr
zJcgx;=NV7z71&&Ju3WKk@cgtwQ6I#4#I;xWK{boG=DgmTY{k_pUAXw`HO!onPh(Ta
z2jYv*4Dx|3*B|C%a?~#3I1gEtMns2Y!{_ca43Rz+^;6}d$9G503;Zp~hw;LkV)4wH
zaOt3+ho$<6r@J-t16a6%TvR4Z~vu9<9kEw>>lq_;A3UGGLG@%GF4
ztC$v>2QSJmdH=G{Jcnb)_cHk%gI=XNC!aYLRO^+lS~>@{7%}tFjuBa=!3a#*p!4%Y4Ys!3rZR`bIIgyW3XlIsWx^
z7m#H*4%Wx!BrCRTv|{nRQp|g<7&GGbl7>t13URi9a;dbah3a$rCgWC@o)B(HOqGnZv2k(
qO2+LUS9#vwwIWCQ>Ed?t`s4hQdAD2h|95}H`oF}5{C|Ld%l-!U^{0FQ
literal 0
HcmV?d00001
diff --git a/src/assets/react.svg b/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/error-page.jsx b/src/error-page.jsx
new file mode 100644
index 0000000..798274d
--- /dev/null
+++ b/src/error-page.jsx
@@ -0,0 +1,16 @@
+import { useRouteError } from "react-router-dom";
+
+export default function ErrorPage() {
+ const error = useRouteError();
+ console.error(error);
+
+ return (
+
+
Oops!
+
Sorry, an unexpected error has occurred.
+
+ {error.statusText || error.message}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/global.css b/src/global.css
new file mode 100644
index 0000000..7749438
--- /dev/null
+++ b/src/global.css
@@ -0,0 +1,15 @@
+.logo {
+ float: left;
+ width: 120px;
+ height: 31px;
+ margin: 16px 24px 16px 0;
+ background: rgba(255, 255, 255, 0.3);
+}
+
+#error-page {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+}
\ No newline at end of file
diff --git a/src/main.jsx b/src/main.jsx
new file mode 100644
index 0000000..b5142c3
--- /dev/null
+++ b/src/main.jsx
@@ -0,0 +1,48 @@
+import React from "react";
+import { configure } from "mobx";
+import ReactDOM from "react-dom/client";
+import {
+ createBrowserRouter,
+ RouterProvider,
+} from "react-router-dom";
+import RootStore from "./stores/Root";
+import { StoreContext } from './stores/StoreContext.js';
+import "./global.css";
+import App from "./views/App";
+import Index from "./views/index";
+import ErrorPage from "./error-page";
+import Plan from "./views/Plan";
+
+configure({
+ useProxies: "ifavailable",
+ enforceActions: "always",
+ computedRequiresReaction: true,
+ reactionRequiresObservable: true,
+ observableRequiresReaction: true,
+ disableErrorBoundaries: true
+});
+
+const router = createBrowserRouter([
+ {
+ path: "/",
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ {
+ path: "plan/:planId",
+ element: ,
+ }
+ ]
+ }
+]);
+
+const rootStore = new RootStore();
+
+ReactDOM.createRoot(document.getElementById("root")).render(
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/stores/Plan.js b/src/stores/Plan.js
new file mode 100644
index 0000000..8bf24c5
--- /dev/null
+++ b/src/stores/Plan.js
@@ -0,0 +1,20 @@
+import { makeAutoObservable } from "mobx";
+
+class Plan {
+ count = 0;
+
+ constructor(root) {
+ makeAutoObservable(this, { rootStore: false });
+ this.root = root;
+ }
+
+ increase() {
+ this.count += 1;
+ }
+
+ decrease() {
+ this.count -= 1;
+ }
+}
+
+export default Plan;
\ No newline at end of file
diff --git a/src/stores/Root.js b/src/stores/Root.js
new file mode 100644
index 0000000..f369e8f
--- /dev/null
+++ b/src/stores/Root.js
@@ -0,0 +1,11 @@
+import { makeAutoObservable } from "mobx";
+import Plan from "./Plan";
+
+class Root {
+ constructor() {
+ this.plan = new Plan(this);
+ makeAutoObservable(this);
+ }
+}
+
+export default Root;
\ No newline at end of file
diff --git a/src/stores/StoreContext.js b/src/stores/StoreContext.js
new file mode 100644
index 0000000..aca4a62
--- /dev/null
+++ b/src/stores/StoreContext.js
@@ -0,0 +1,7 @@
+import { createContext, useContext } from "react";
+
+export const StoreContext = createContext();
+
+export function useStore() {
+ return useContext(StoreContext);
+}
\ No newline at end of file
diff --git a/src/utils/commons.js b/src/utils/commons.js
new file mode 100644
index 0000000..82bfc79
--- /dev/null
+++ b/src/utils/commons.js
@@ -0,0 +1,124 @@
+export function copy(obj) {
+ return JSON.parse(JSON.stringify(obj));
+}
+
+export function formatDate(date) {
+ if (isEmpty(date)) {
+ return "NaN";
+ }
+
+ const year = date.getFullYear();
+ const month = date.getMonth() + 1;
+ const day = date.getDate();
+
+ const monthStr = ("" + month).padStart(2, 0);
+ const dayStr = ("" + day).padStart(2, 0);
+ const formatted = year + "-" + monthStr + "-" + dayStr;
+
+ return formatted;
+}
+
+export function formatTime(date) {
+ const hours = date.getHours();
+ const minutes = date.getMinutes();
+
+ const hoursStr = ("" + hours).padStart(2, 0);
+ const minutesStr = ("" + minutes).padStart(2, 0);
+ const formatted = hoursStr + ":" + minutesStr;
+
+ return formatted;
+}
+
+export function formatDatetime(date) {
+ const year = date.getFullYear();
+ const month = date.getMonth() + 1;
+ const day = date.getDate();
+
+ const monthStr = ("" + month).padStart(2, 0);
+ const dayStr = ("" + day).padStart(2, 0);
+
+ const hours = date.getHours();
+ const minutes = date.getMinutes();
+
+ const hoursStr = ("" + hours).padStart(2, 0);
+ const minutesStr = ("" + minutes).padStart(2, 0);
+
+ const formatted = year + "-" + monthStr + "-" + dayStr + " " + hoursStr + ":" + minutesStr;
+
+ return formatted;
+}
+
+export function camelCase(name) {
+ return name.substr(0, 1).toLowerCase() + name.substr(1);
+}
+
+export class UrlBuilder {
+ constructor(url) {
+ this.url = url;
+ this.paramList = [];
+ }
+
+ append(name, value) {
+ if (isNotEmpty(value)) {
+ this.paramList.push({ name: name, value: value });
+ }
+ return this;
+ }
+
+ build() {
+ this.paramList.forEach((e, i, a) => {
+ if (i === 0) {
+ this.url += "?";
+ } else {
+ this.url += "&";
+ }
+ this.url += e.name + "=" + e.value;
+ });
+ return this.url;
+ }
+}
+
+export function isNotEmpty(val) {
+ return val !== undefined && val !== null && val !== "";
+}
+
+export function isEmpty(val) {
+ return val === undefined || val === null || val === "";
+}
+
+export function prepareUrl(url) {
+ return new UrlBuilder(url);
+}
+
+export function debounce(fn, delay = 500) {
+ let timer;
+ return e => {
+ e.persist();
+ clearTimeout(timer);
+ timer = setTimeout(() => {
+ fn(e);
+ }, delay);
+ };
+}
+
+export function throttle(fn, delay, atleast) {
+ let timeout = null,
+ startTime = new Date();
+ return function () {
+ let curTime = new Date();
+ clearTimeout(timeout);
+ if (curTime - startTime >= atleast) {
+ fn();
+ startTime = curTime;
+ } else {
+ timeout = setTimeout(fn, delay);
+ }
+ };
+}
+
+export function clickUrl(url) {
+ const httpLink = document.createElement("a");
+ httpLink.href = url;
+ httpLink.target = "_blank";
+ httpLink.click();
+}
\ No newline at end of file
diff --git a/src/utils/request.js b/src/utils/request.js
new file mode 100644
index 0000000..e28de42
--- /dev/null
+++ b/src/utils/request.js
@@ -0,0 +1,69 @@
+function checkStatus(response) {
+ if (response.status >= 200 && response.status < 300) {
+ return response;
+ } else {
+ const message =
+ 'Fetch error: ' + response.url + ' ' + response.status + ' (' +
+ response.statusText + ')';
+ const error = new Error(message);
+ error.response = response;
+ throw error;
+ }
+}
+
+export function fetchText(url) {
+ return fetch(url)
+ .then(checkStatus)
+ .then(response => response.text())
+ .catch(error => {
+ throw error;
+ });
+}
+
+export function fetchJSON(url) {
+ return fetch(url)
+ .then(checkStatus)
+ .then(response => response.json())
+ .catch(error => {
+ throw error;
+ });
+}
+
+export function postForm(url, data) {
+ return fetch(url, {
+ method: 'POST',
+ body: data
+ }).then(checkStatus)
+ .then(response => response.json())
+ .catch(error => {
+ throw error;
+ });
+}
+
+export function postJSON(url, obj) {
+ return fetch(url, {
+ method: 'POST',
+ body: JSON.stringify(obj),
+ headers: {
+ 'Content-type': 'application/json; charset=UTF-8'
+ }
+ }).then(checkStatus)
+ .then(response => response.json())
+ .catch(error => {
+ throw error;
+ });
+}
+
+export function postStream(url, obj) {
+ return fetch(url, {
+ method: 'POST',
+ body: JSON.stringify(obj),
+ headers: {
+ 'Content-type': 'application/octet-stream'
+ }
+ }).then(checkStatus)
+ .then(response => response.json())
+ .catch(error => {
+ throw error;
+ });
+}
diff --git a/src/views/App.jsx b/src/views/App.jsx
new file mode 100644
index 0000000..e36848e
--- /dev/null
+++ b/src/views/App.jsx
@@ -0,0 +1,145 @@
+import { Outlet, Link, useNavigation } from "react-router-dom";
+import { useState, useEffect } from 'react';
+import { Breadcrumb, Layout, Menu, ConfigProvider, theme, Dropdown, Space, Row, Col } from 'antd';
+import {
+ MenuFoldOutlined,
+ MenuUnfoldOutlined,
+ UploadOutlined,
+ UserOutlined,
+ VideoCameraOutlined,
+ DownOutlined
+} from '@ant-design/icons';
+import 'antd/dist/reset.css';
+
+const { Header, Content, Footer, Sider } = Layout;
+const items = [
+ {
+ label: (
+
+ Profile
+
+ ),
+ key: '0',
+ },
+ {
+ label: (
+
+ Privacy
+
+ ),
+ key: '1',
+ },
+ {
+ type: 'divider',
+ },
+ {
+ label: (
+
+ Logout
+
+ ),
+ key: '3',
+ },
+]
+export default function App() {
+ const {
+ token: { colorBgContainer },
+ } = theme.useToken();
+ return (
+
+
+
+
+
+
+ ,
+ label: Jim,
+ },
+ {
+ key: '2',
+ icon: ,
+ label: Bill,
+ },
+ ]}
+ />
+
+
+
+ Home
+ Plan
+ Recent
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/views/Index.jsx b/src/views/Index.jsx
new file mode 100644
index 0000000..2b55a12
--- /dev/null
+++ b/src/views/Index.jsx
@@ -0,0 +1,13 @@
+export default function Index() {
+ return (
+
+ This is a demo for React Router.
+
+ Check out{" "}
+
+ the docs at reactrouter.com
+
+ .
+
+ );
+}
\ No newline at end of file
diff --git a/src/views/Plan.jsx b/src/views/Plan.jsx
new file mode 100644
index 0000000..ae195a3
--- /dev/null
+++ b/src/views/Plan.jsx
@@ -0,0 +1,143 @@
+import { Form, useParams } from "react-router-dom";
+import { useState, useEffect } from 'react';
+import { observer } from "mobx-react";
+import { Row, Col, Typography, Space, DatePicker, Button, Select, Table, Tag } from 'antd';
+import {
+ SearchOutlined,
+} from '@ant-design/icons';
+import { useStore } from '../stores/StoreContext.js';
+
+const columns = [
+ {
+ title: 'Name',
+ dataIndex: 'name',
+ key: 'name',
+ render: (text) => {text} ,
+ },
+ {
+ title: 'Age',
+ dataIndex: 'age',
+ key: 'age',
+ },
+ {
+ title: 'Address',
+ dataIndex: 'address',
+ key: 'address',
+ },
+ {
+ title: 'Tags',
+ key: 'tags',
+ dataIndex: 'tags',
+ render: (_, { tags }) => (
+ <>
+ {tags.map((tag) => {
+ let color = tag.length > 5 ? 'geekblue' : 'green';
+ if (tag === 'loser') {
+ color = 'volcano';
+ }
+ return (
+
+ {tag.toUpperCase()}
+
+ );
+ })}
+ >
+ ),
+ },
+ {
+ title: 'Action',
+ key: 'action',
+ render: (_, record) => (
+
+ Invite {record.name}
+ Delete
+
+ ),
+ },
+];
+const data = [
+ {
+ key: '1',
+ name: 'John Brown',
+ age: 32,
+ address: 'New York No. 1 Lake Park',
+ tags: ['nice', 'developer'],
+ },
+ {
+ key: '2',
+ name: 'Jim Green',
+ age: 42,
+ address: 'London No. 1 Lake Park',
+ tags: ['loser'],
+ },
+ {
+ key: '3',
+ name: 'Joe Black',
+ age: 32,
+ address: 'Sydney No. 1 Lake Park',
+ tags: ['cool', 'teacher'],
+ },
+];
+
+function Plan() {
+
+ const {planId} = useParams();
+ const [count, setCount] = useState(0);
+ const store = useStore();
+ const planStore = store.plan;
+
+ useEffect(() => {
+ console.info('planId: ' + planId);
+ }, [planId]);
+
+ const contact = {
+ first: "Your",
+ last: "Name",
+ avatar: "https://placekitten.com/g/200/200",
+ twitter: "your_handle",
+ notes: "Some notes",
+ favorite: true,
+ };
+
+ return (
+
+
+
+ Parameter: {planId}
+
+
+
+ planStore.increase()}>increase {planStore.count}
+ planStore.decrease()}>decrease {planStore.count}
+
+
+
+
+
+
+
+
+ );
+}
+
+function Favorite({ contact }) {
+ // yes, this is a `let` for later
+ let favorite = contact.favorite;
+ return (
+
+ );
+}
+
+export default observer(Plan);
\ No newline at end of file
diff --git a/start.bat b/start.bat
new file mode 100644
index 0000000..b896a08
--- /dev/null
+++ b/start.bat
@@ -0,0 +1 @@
+npm run dev
\ No newline at end of file
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..5a33944
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})