diff --git a/.vscode/settings.json b/.vscode/settings.json index 57ac932..48464d5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,6 +13,7 @@ "**/artifacts": true, "**/dist": true, "**/storybook-static": true, + "**/coverage": true, "**/logs": true, "**/out": true, "**/obj": true, @@ -24,6 +25,7 @@ "**/*.user": true, "**/*.xproj": true, "**/*.gitattributes": true, + "**/*.storybook": true, ".vs:": true, ".vscode:": true } diff --git a/package.json b/package.json index 6e56120..3c71b6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeslide.net", - "version": "0.3.7", + "version": "0.4.0", "private": true, "dependencies": { "@ant-design/icons": "^4.8.1", diff --git a/src/App.tsx b/src/App.tsx index 62451bc..27bc1f0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,11 +7,9 @@ */ import { ConfigProvider, Layout, Tour, TourProps } from 'antd'; -import * as React from 'react'; -import { useRouteMatch } from 'react-router'; import { ClipboardContainer } from '@app/core'; import { EditorView, ShapeView, PagesView, HeaderView, AnimationView, ToolView } from '@app/wireframes/components'; -import { getSelection, loadDiagramFromServer, newDiagram, setIsTourOpen, useStore } from '@app/wireframes/model'; +import { getSelection, newDiagram, setIsTourInit, setIsTourOpen, useStore } from '@app/wireframes/model'; import { vogues } from './const'; import { CustomDragLayer } from './wireframes/components/CustomDragLayer'; import { OverlayContainer } from './wireframes/contexts/OverlayContext'; @@ -20,24 +18,17 @@ import { useAppDispatch } from './store'; export const App = () => { const dispatch = useAppDispatch(); - const route = useRouteMatch<{ token?: string }>(); - const routeToken = route.params.token || null; - const routeTokenSnapshot = React.useRef(routeToken); const selectedSet = useStore(getSelection); const sidebarWidth = useStore(s => s.ui.sidebarSize); const footerHeight = useStore(s => s.ui.footerSize); const applicationMode = useStore(s => s.ui.selectedMode); const isTourOpen = useStore(s => s.ui.isTourOpen); + const isTourInit = useStore(s => s.ui.isTourInit); const tourRefs = Array(7).fill(0).map(() => useRef(null)); useEffect(() => { - const token = routeTokenSnapshot.current; - - if (token && token.length > 0) { - dispatch(loadDiagramFromServer({ tokenToRead: token, navigate: false })); - } else { - dispatch(newDiagram(false)); - } + dispatch(newDiagram(false)); + dispatch(setIsTourInit(false)); }, [dispatch]); const margin = { @@ -50,42 +41,51 @@ export const App = () => { const walkthroughTour: TourProps['steps'] = [ { - title: 'Step 1: Starting Your Project', - description: 'Start a fresh project, open an existing one, or save your work to your local machine. The documentation for coding syntax is also here.', + title: 'Get started with a presentation', + description: 'Start a fresh presentation, open an existing one, or save your presentation to your local machine. You can also click here to see the documentation for coding syntax.', target: () => tourRefs[0].current, }, { - title: 'Step 2: Designing Your Presentation', - description: 'Use the canvas to layout and customize your presentation\'s structure.', + title: 'Design Your Presentation', + description: 'Use the canvas to layout and customize your presentation\'s structure. Canvas is where you can add shapes, text, and images.', target: () => tourRefs[1].current, }, { - title: 'Step 3: Adding Shapes', + title: 'Add Shapes', description: 'Add visual elements to your canvas by clicking on any shape.', target: () => tourRefs[2].current, }, { - title: 'Step 4: Modifying Appearance', - description: 'Edit your objects\'s appearance, including color, font size, stroke, and more, to match your vision.', + title: 'Modify Appearance', + description: 'Edit your objects\'s appearance such as color, font size, and stroke.', target: () => tourRefs[3].current, }, { - title: 'Step 5: Writing Code', + title: 'Write Code', description: 'Switch to coding mode to set object\'s occurrences. If you\'re stuck on syntax, the documentation is under the button at the top left corner.', target: () => tourRefs[4].current, }, { - title: 'Step 6: Managing Pages', + title: 'Manage Pages', description: 'Add new pages and continue unfolding your presentation.', target: () => tourRefs[5].current, }, { - title: 'Step 7: Launching Your Presentation', - description: 'Start your presentation. If you need to make changes, you can always come back and edit.', + title: 'Launch Your Presentation', + description: 'Start your presentation. If you need to make changes, you can always come back and edit. Enjoy!', target: () => tourRefs[6].current, }, ]; + const firstTour: TourProps['steps'] = [ + { + title: 'Welcome to CodeSlide', + description: 'CodeSlide is a tool for creating presentations with code. You can create slides with shapes, text, and images, and animate them with code. Let\'s get started!', + target: null, + }, + ...walkthroughTour + ]; + return ( { dispatch(setIsTourOpen(false))} - steps={walkthroughTour} + steps={isTourInit ? firstTour : walkthroughTour} /> diff --git a/src/const/texts.ts b/src/const/texts.ts index cbd8284..833c776 100644 --- a/src/const/texts.ts +++ b/src/const/texts.ts @@ -105,7 +105,7 @@ export const texts = { ungroupTooltip: 'Ungroup Items', unsaved: 'unsaved', visual: 'Visual', - walkthrough: 'Walkthrough', + walkthrough: 'Tutorial', width: 'Width', zoomIn: 'Zoom In', zoomOut: 'Zoom Out', diff --git a/src/index.tsx b/src/index.tsx index 7c3918e..f3625d8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -12,18 +12,15 @@ import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import * as ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; -import { Route, Router } from 'react-router'; import { App } from './App'; import { registerServiceWorker } from './registerServiceWorker'; import './index.scss'; -import { history, store } from './store'; +import { store } from './store'; const Root = ( - - - + ); diff --git a/src/store.ts b/src/store.ts index 2679507..076cc25 100644 --- a/src/store.ts +++ b/src/store.ts @@ -7,7 +7,6 @@ */ import { configureStore } from '@reduxjs/toolkit'; -import { createBrowserHistory } from 'history'; import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import { assets, buildAlignment, buildAppearance, buildDiagrams, buildGrouping, buildItems, buildOrdering, createClassReducer, loading, loadingMiddleware, mergeAction, rootLoading, selectDiagram, selectItems, toastMiddleware, ui, undoable } from './wireframes/model/actions'; import { EditorState } from './wireframes/model/editor-state'; @@ -37,20 +36,21 @@ const undoableReducer = undoable( ], }); -export const history = createBrowserHistory(); - export const store = configureStore({ reducer: { // Contains the store for the left sidebar. assets: assets(createInitialAssetsState()), + // Actual editor content. editor: rootLoading(undoableReducer, editorReducer), + // Loading state, e.g. when something has been loaded. loading: loading(createInitialLoadingState()), + // General UI behavior. ui: ui(createInitialUIState()), }, - middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false }).concat(toastMiddleware(), loadingMiddleware(history)), + middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false }).concat(toastMiddleware(), loadingMiddleware()), }); export type RootState = ReturnType; diff --git a/src/wireframes/components/RecentView.tsx b/src/wireframes/components/RecentView.tsx index ae78943..2e1e466 100644 --- a/src/wireframes/components/RecentView.tsx +++ b/src/wireframes/components/RecentView.tsx @@ -8,21 +8,13 @@ import { Empty } from 'antd'; import * as React from 'react'; -import { useEventCallback } from '@app/core'; import { texts } from '@app/const'; -import { loadDiagramFromServer, RecentDiagram, useStore } from '@app/wireframes/model'; -import { useAppDispatch } from '@app/store'; -import { RecentItem } from './menu'; +import { useStore } from '@app/wireframes/model'; import './styles/RecentView.scss'; export const RecentView = () => { - const dispatch = useAppDispatch(); const recent = useStore(x => x.loading.recentDiagrams); - const doLoad = useEventCallback((item: RecentDiagram) => { - dispatch(loadDiagramFromServer({ tokenToRead: item.tokenToRead, tokenToWrite: item.tokenToWrite, navigate: true })); - }); - const orderedRecent = React.useMemo(() => { const result = Object.entries(recent).map(([tokenToRead, value]) => { return { ...value, tokenToRead }; @@ -36,10 +28,6 @@ export const RecentView = () => { return ( <>
- {orderedRecent.map((item) => - , - )} - {orderedRecent.length === 0 && } diff --git a/src/wireframes/components/actions/use-server.ts b/src/wireframes/components/actions/use-server.ts index 41308bf..87f4b88 100644 --- a/src/wireframes/components/actions/use-server.ts +++ b/src/wireframes/components/actions/use-server.ts @@ -42,12 +42,12 @@ export function useServer() { return { item: item, id: id }; } - const getSlides = () => { + const getSlides = (allFrames: Map) => { let frame3D: string[][][] = new Array(diagrams.length); - + // Get frames diagrams.map((diagram, i) => { - const frames = diagram.frames ?? []; + const frames = allFrames.get(diagram.id) ?? []; frame3D[i] = []; frames.map((frame, j) => { @@ -85,29 +85,34 @@ export function useServer() { } const fetchParser = async () => { + let frames: Map = new Map(); + for (let diagram of diagrams) { const script = diagram.script; if (!script) continue; - const frames = await parseFrames(script); - dispatch(changeFrames(diagram.id, frames)); + const frame = await parseFrames(script); + frames.set(diagram.id, frame); + dispatch(changeFrames(diagram.id, frame)); } + + return frames; } - const fetchCompiler = async () => { - const { fileName, title, size, backgroundColor, config, frame } = getSlides(); + const fetchCompiler = async (frames: Map) => { + const { fileName, title, size, backgroundColor, config, frame } = getSlides(frames); const linkPresentation = await compileSlides(fileName, title, size, backgroundColor, config, frame); return linkPresentation; } - const fetchApi = () => { + const fetchApi = async () => { // Fetch frames from parser - fetchParser(); - + const frames = await fetchParser(); + // Fetch presentation from compiler - const links = fetchCompiler(); + const links = await fetchCompiler(frames); return links; } @@ -120,14 +125,16 @@ export function useServer() { window.open(linkPdf, '_blank'); } catch (err) { messageApi.error(`${err}`); - } finally { - messageApi.open({ - key: messageKey, - type: 'success', - content: `Preparing completed. Your presentation will be opened in a new tab.`, - duration: 1, - }); - } + return; + } + + messageApi.open({ + key: messageKey, + type: 'success', + content: `Preparing completed. Your presentation will be opened in a new tab.`, + duration: 1, + }); + return; } const fetchSlide = async (messageApi: MessageInstance, messageKey: string) => { @@ -138,14 +145,16 @@ export function useServer() { window.open(linkSlide, '_blank'); } catch (err) { messageApi.error(`${err}`); - } finally { - messageApi.open({ - key: messageKey, - type: 'success', - content: `Preparing completed. Your presentation will be opened in a new tab.`, - duration: 1, - }); + return; } + + messageApi.open({ + key: messageKey, + type: 'success', + content: `Preparing completed. Your presentation will be opened in a new tab.`, + duration: 1, + }); + return; } return { slide: fetchSlide, pdf: fetchPdf }; diff --git a/src/wireframes/components/headers/PresentHeader.tsx b/src/wireframes/components/headers/PresentHeader.tsx index e147d5f..a7fb8b3 100644 --- a/src/wireframes/components/headers/PresentHeader.tsx +++ b/src/wireframes/components/headers/PresentHeader.tsx @@ -25,11 +25,9 @@ export const PresentHeader = React.memo(() => { icon={} onClick={() => { setLoading(true); - try { - forServer.slide(messageApi, messageKey); - } finally { - setLoading(false) - } + forServer + .slide(messageApi, messageKey) + .finally(() => setLoading(false)); }} className="header-cta-right" type="text" shape='round' diff --git a/src/wireframes/model/actions/api.ts b/src/wireframes/model/actions/api.ts index 0dbc8be..9c3a5c0 100644 --- a/src/wireframes/model/actions/api.ts +++ b/src/wireframes/model/actions/api.ts @@ -27,18 +27,6 @@ const strToList = (str: string) => { return result; } -export async function fetchDiagram(readToken: string) { - const response = await fetch(`${SERVER_URL}/store/${readToken}`); - - if (!response.ok) { - throw Error('Failed to load diagram'); - } - - const stored = await response.json(); - - return stored; -} - export const parseFrames = async (script: string) => { const response = await fetch(`${SERVER_URL}/parser`, { method: 'POST', diff --git a/src/wireframes/model/actions/loading.ts b/src/wireframes/model/actions/loading.ts index 9688a08..7642eb3 100644 --- a/src/wireframes/model/actions/loading.ts +++ b/src/wireframes/model/actions/loading.ts @@ -7,12 +7,10 @@ */ import { createAction, createAsyncThunk, createReducer, Middleware } from '@reduxjs/toolkit'; -import { History } from 'history'; import { saveAs } from 'file-saver'; import { AnyAction, Reducer } from 'redux'; import { texts } from '@app/const'; import { EditorState, EditorStateInStore, LoadingState, Serializer, UndoableState } from './../internal'; -import { fetchDiagram } from './api'; import { addDiagram, selectDiagram } from './diagrams'; import { selectItems } from './items'; import { migrateOldAction } from './obsolete'; @@ -30,13 +28,6 @@ export const loadDiagramFromFile = return { stored }; }); -export const loadDiagramFromServer = - createAsyncThunk('diagram/load/server', async (args: { tokenToRead: string; tokenToWrite?: string; navigate: boolean }) => { - const stored = await fetchDiagram(args.tokenToRead); - - return { tokenToRead: args.tokenToRead, tokenToWrite: args.tokenToWrite, stored }; - }); - export const loadDiagramInternal = createAction('diagram/load/actions', (stored: any, requestId: string) => { return { payload: { stored, requestId } }; @@ -52,29 +43,17 @@ export const downloadDiagramToFile = saveAs(bodyBlob, 'diagram.json'); }); -export function loadingMiddleware(history: History): Middleware { +export function loadingMiddleware(): Middleware { const middleware: Middleware = store => next => action => { - if (loadDiagramFromServer.pending.match(action) || loadDiagramFromFile.pending.match(action)) { + if (loadDiagramFromFile.pending.match(action)) { store.dispatch(showToast(texts.common.loadingDiagram, 'loading', action.meta.requestId)); } try { const result = next(action); - if (newDiagram.match(action) ) { - if (action.payload.navigate) { - history.push(''); - } - } else if (loadDiagramFromServer.fulfilled.match(action)) { - if (action.meta.arg.navigate) { - history.push(action.payload.tokenToRead); - } - - store.dispatch(loadDiagramInternal(action.payload.stored, action.meta.requestId)); - } else if (loadDiagramFromFile.fulfilled.match(action)) { + if (loadDiagramFromFile.fulfilled.match(action)) { store.dispatch(loadDiagramInternal(action.payload.stored, action.meta.requestId)); - } else if (loadDiagramFromServer.rejected.match(action) || loadDiagramFromFile.rejected.match(action)) { - store.dispatch(showToast(texts.common.loadingDiagramFailed, 'error', action.meta.requestId)); } else if (loadDiagramInternal.match(action)) { store.dispatch(showToast(texts.common.loadingDiagramDone, 'success', action.payload.requestId)); } else if (downloadDiagramToFile.fulfilled.match(action)) { @@ -101,17 +80,6 @@ export function loading(initialState: LoadingState) { state.isLoading = false; state.tokenToRead = null; state.tokenToWrite = null; - }) - .addCase(loadDiagramFromServer.pending, (state) => { - state.isLoading = true; - }) - .addCase(loadDiagramFromServer.rejected, (state) => { - state.isLoading = false; - }) - .addCase(loadDiagramFromServer.fulfilled, (state, action) => { - state.isLoading = false; - state.tokenToRead = action.payload?.tokenToRead; - state.tokenToWrite = action.payload?.tokenToWrite; })); } diff --git a/src/wireframes/model/actions/ui.ts b/src/wireframes/model/actions/ui.ts index eebdbf3..23f5790 100644 --- a/src/wireframes/model/actions/ui.ts +++ b/src/wireframes/model/actions/ui.ts @@ -48,6 +48,11 @@ export const setAnimation = return { payload: { animation } }; }); +export const setIsTourInit = + createAction('ui/isTourInit', (isInit: boolean) => { + return { payload: { isInit } }; + }); + export const setIsTourOpen = createAction('ui/isTourOpen', (isOpen: boolean) => { return { payload: { isOpen } }; diff --git a/src/wireframes/model/ui-state.ts b/src/wireframes/model/ui-state.ts index 73878d3..f2c70f1 100644 --- a/src/wireframes/model/ui-state.ts +++ b/src/wireframes/model/ui-state.ts @@ -34,7 +34,10 @@ export interface UIState { // The color tab. selectedColor: string; - // The tour step. + // The tour init (first time). + isTourInit: boolean; + + // The tour start. isTourOpen: boolean; // The filter for the diagram. @@ -53,6 +56,7 @@ export const createInitialUIState: () => UIState = () => { selectedMode: 'design', selectedAnimation: 'script', sidebarSize: vogues.common.close, - isTourOpen: false, + isTourInit: true, + isTourOpen: true, }; };