Skip to content

Commit 6df07ff

Browse files
Merge pull request #147 from Program-AR/url-share
Url share
2 parents 12addc9 + 56d65be commit 6df07ff

15 files changed

Lines changed: 390 additions & 20 deletions

File tree

.github/workflows/build.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jobs:
3232
runs-on: ubuntu-latest
3333
env:
3434
REACT_APP_API_URL: ${{ secrets.API_URL }}
35+
REACT_APP_PB_APP_URL: ${{ secrets.APP_URL }}
3536
REACT_APP_GOOGLE_ANALYTICS_KEY: ${{ secrets.GOOGLE_ANALYTICS_KEY }}
3637
REACT_APP_VERSION: ${{github.ref_name}}
3738
if: startsWith(github.ref, 'refs/tags')
@@ -55,6 +56,7 @@ jobs:
5556
runs-on: macos-latest
5657
env:
5758
REACT_APP_API_URL: ${{ secrets.API_URL }}
59+
REACT_APP_PB_APP_URL: ${{ secrets.APP_URL }}
5860
REACT_APP_GOOGLE_ANALYTICS_KEY: ${{ secrets.GOOGLE_ANALYTICS_KEY }}
5961
REACT_APP_VERSION: ${{github.ref_name}}
6062
if: startsWith(github.ref, 'refs/tags')
@@ -77,6 +79,7 @@ jobs:
7779
runs-on: windows-latest
7880
env:
7981
REACT_APP_API_URL: ${{ secrets.API_URL }}
82+
REACT_APP_PB_APP_URL: ${{ secrets.APP_URL }}
8083
REACT_APP_GOOGLE_ANALYTICS_KEY: ${{ secrets.GOOGLE_ANALYTICS_KEY }}
8184
REACT_APP_VERSION: ${{github.ref_name}}
8285
if: startsWith(github.ref, 'refs/tags')
@@ -99,6 +102,7 @@ jobs:
99102
runs-on: ubuntu-latest
100103
env:
101104
REACT_APP_API_URL: ${{ secrets.API_URL }}
105+
REACT_APP_PB_APP_URL: ${{ secrets.APP_URL }}
102106
REACT_APP_GOOGLE_ANALYTICS_KEY: ${{ secrets.GOOGLE_ANALYTICS_KEY }}
103107
REACT_APP_VERSION: ${{github.ref_name}}
104108
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'

locales/en-us/creator.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,16 @@
9595
"buttons": {
9696
"discardChallenge": "Discard challenge",
9797
"discardChallengeShort": "Discard",
98-
"download": "Download",
98+
"download": "Download challenge",
99+
"downloadShort": "Download",
100+
"share": "Share challenge",
101+
"shareUrl": "Share via url",
102+
"shareUrlShort": "Share",
103+
"save": "Save challenge",
104+
"saveShort": "Save",
105+
"savedCorrectly": "Challenge was saved correctly",
106+
"copyToClipboard": "Copy to clipboard",
107+
"copiedToClipboard": "Copied to clipboard",
99108
"preview": "Challenge preview",
100109
"previewShort": "Preview",
101110
"keepEditing": "Keep editing",
@@ -108,7 +117,9 @@
108117

109118
},
110119
"editorHeader": "Edit challenge",
111-
"previewModeHeader": "Challenge preview"
120+
"previewModeHeader": "Challenge preview",
121+
"serverError": "There's a problem with the server, try again later",
122+
"loginWarning": "You need to be logged in to share a challenge via url."
112123
},
113124
"title": {
114125
"title": "Write the challenge's title for ",

locales/es-ar/creator.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,16 @@
9595
"buttons": {
9696
"discardChallenge": "Descartar desafío",
9797
"discardChallengeShort": "Descartar",
98-
"download": "Descargar",
98+
"download": "Descargar desafío",
99+
"downloadShort": "Descargar",
100+
"share": "Compartir desafío",
101+
"shareUrl": "Compartir por url",
102+
"shareUrlShort": "Compartir",
103+
"copyToClipboard": "Copiar al portapapeles",
104+
"copiedToClipboard": "Copiado al portapapeles",
105+
"save": "Guardar desafío",
106+
"saveShort": "Guardar",
107+
"savedCorrectly": "El desafío fue guardado correctamente",
99108
"preview": "Ver desafío",
100109
"previewShort": "Ver",
101110
"keepEditing": "Seguir editando",
@@ -106,7 +115,9 @@
106115
"loadChallengeShort": "Abrir"
107116
},
108117
"editorHeader": "Editar desafío",
109-
"previewModeHeader": "Ver desafío"
118+
"previewModeHeader": "Ver desafío",
119+
"serverError": "Problema con el servidor, intente más tarde",
120+
"loginWarning": "Para compartir un desafío por URL es necesario estar loggeado."
110121
},
111122
"title": {
112123
"title": "Escribí el título del desafío para ",

sample.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
REACT_APP_API_URL=http://localhost:3001
2+
REACT_APP_PB_APP_URL=http://localhost:3000
23
REACT_APP_VERSION=$npm_package_version
34
REACT_APP_GOOGLE_ANALYTICS_KEY=G-xxxxxx #Not necessary, can be omitted for development

src/App.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import { useLocation } from 'react-router-dom';
1717
import ReactGA from "react-ga4";
1818
import { CreatorViewMode } from './components/creator/Editor/CreatorViewMode';
1919
import { useThemeContext } from './theme/ThemeContext';
20+
import { SharedChallengeView } from './components/creator/SharedChallengeView';
21+
import { PilasBloquesApi } from './pbApi';
22+
import { Ember } from './emberCommunication';
2023

2124
const AnalyticsComponent = () => {
2225
const location = useLocation();
@@ -38,6 +41,16 @@ const router = createHashRouter([{
3841
element: <Home/>,
3942
errorElement: <PBError />
4043
},
44+
{
45+
path: "/desafio/guardado/:id",
46+
element: <SharedChallengeView/>,
47+
errorElement: <PBError />,
48+
loader: async ({ params }) => {
49+
const challenge = await PilasBloquesApi.getSharedChallenge(params.id!);
50+
Ember.importChallenge(challenge)
51+
return challenge
52+
},
53+
},
4154
{
4255
path: "/libros/:id",
4356
element: <BookView/>,
@@ -103,5 +116,4 @@ function App() {
103116
);
104117
}
105118

106-
export default App;
107-
119+
export default App;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { CreatorActionButton } from "../CreatorActionButton";
2+
import DownloadIcon from '@mui/icons-material/Download';
3+
import { useContext, useState } from "react";
4+
import { Dialog, DialogContent, DialogTitle, InputAdornment, Stack, TextField } from "@mui/material";
5+
import { CreatorContext } from "../../CreatorContext";
6+
import { ShareButtons, CopyToClipboardButton } from "./ShareModalButtons";
7+
import { useTranslation } from "react-i18next";
8+
9+
export const ShareButton = () => {
10+
11+
const [dialogOpen, setDialogOpen] = useState<boolean>(false)
12+
13+
return <>
14+
<ShareDialog open={dialogOpen} setDialogOpen={setDialogOpen} />
15+
<CreatorActionButton onClick={() => { setDialogOpen(true) }} startIcon={<DownloadIcon />} nametag='share' isshortversion={true} />
16+
</>
17+
18+
}
19+
20+
const ShareDialog = ({ open, setDialogOpen }: { open: boolean, setDialogOpen: (open: boolean) => void }) => {
21+
22+
const { t } = useTranslation('creator');
23+
24+
return <>
25+
<Dialog open={open} onClose={() => { setDialogOpen(false) }}>
26+
<DialogTitle>{t('editor.buttons.share')}</DialogTitle>
27+
<DialogContent >
28+
<ShareModal />
29+
</DialogContent>
30+
</Dialog >
31+
</>
32+
}
33+
34+
export const ShareModal = () => {
35+
const { sharedId } = useContext(CreatorContext)
36+
37+
const sharedLink = process.env.REACT_APP_PB_APP_URL + `/#/desafio/guardado/${sharedId}`
38+
39+
return <Stack>
40+
{sharedId ?
41+
<Stack direction='row'>
42+
<TextField
43+
sx={{ width: '100%', margin: 1}}
44+
defaultValue={sharedLink}
45+
InputProps={{
46+
readOnly: true,
47+
endAdornment: (
48+
<InputAdornment position="end">
49+
<CopyToClipboardButton textToCopy={sharedLink} />
50+
</InputAdornment>
51+
)
52+
}}
53+
/>
54+
</Stack>
55+
: <></>
56+
}
57+
<ShareButtons />
58+
</Stack>
59+
}
60+
61+
62+
63+
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import ShareIcon from '@mui/icons-material/Share';
2+
import SaveIcon from '@mui/icons-material/Save';
3+
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
4+
import { ReactNode, useContext, useState } from "react"
5+
import { IconButtonTooltip } from "../../SceneEdition/IconButtonTooltip"
6+
import { Snackbar, Stack, Tooltip } from "@mui/material"
7+
import { CreatorContext } from '../../CreatorContext';
8+
import { LocalStorage } from '../../../../../localStorage';
9+
import { PilasBloquesApi } from '../../../../../pbApi';
10+
import { CreatorActionButton } from '../CreatorActionButton';
11+
import { DialogSnackbar } from '../../../../dialogSnackbar/DialogSnackbar';
12+
import { useTranslation } from 'react-i18next';
13+
import { SerializedChallenge } from '../../../../serializedChallenge';
14+
import { DownloadButton } from '../DownloadButton';
15+
16+
export const CopyToClipboardButton = ({ textToCopy }: { textToCopy: string }) => {
17+
18+
const [openSnackbar, setOpenSnackbar] = useState(false)
19+
20+
const { t } = useTranslation('creator');
21+
22+
const handleClick = () => {
23+
setOpenSnackbar(true)
24+
navigator.clipboard.writeText(textToCopy)
25+
}
26+
27+
return <>
28+
<IconButtonTooltip icon={<ContentCopyIcon />} onClick={handleClick} tooltip={t('editor.buttons.copyToClipboard')} />
29+
<Snackbar
30+
open={openSnackbar}
31+
onClose={() => setOpenSnackbar(false)}
32+
autoHideDuration={2000}
33+
message={t('editor.buttons.copiedToClipboard')}
34+
/>
35+
</>
36+
}
37+
38+
export const ShareButtons = () => {
39+
const { sharedId } = useContext(CreatorContext)
40+
41+
return <>
42+
<Stack direction="row" justifyContent="space-between" alignItems='center'>
43+
{// If the challenge has already been saved, show Save, else show Share, which saves for the first time.
44+
sharedId ? <SaveButton /> : <ShareUrlButton />
45+
}
46+
<DownloadButton />
47+
</Stack>
48+
</>
49+
}
50+
51+
const ShareUrlButton = () =>
52+
<ChallengeUpsertButton Icon={<ShareIcon />} nametag="shareUrl" challengeUpsert={PilasBloquesApi.shareChallenge} />
53+
54+
const SaveButton = () =>
55+
<ChallengeUpsertButton Icon={<SaveIcon />} nametag="save" challengeUpsert={PilasBloquesApi.saveChallenge} />
56+
57+
export const ChallengeUpsertButton = ({ Icon, challengeUpsert, nametag }: { Icon: ReactNode, nametag: string, challengeUpsert: (challenge: SerializedChallenge) => Promise<SerializedChallenge> }) => {
58+
59+
const { setSharedId } = useContext(CreatorContext)
60+
const userLoggedIn = !!LocalStorage.getUser()
61+
const [serverError, setServerError] = useState<boolean>(false)
62+
const { t } = useTranslation('creator');
63+
const [savedSnackbar, setSavedSnackbarOpen] = useState(false)
64+
65+
const handleClick = async () => {
66+
try {
67+
const savedChallenge = await challengeUpsert(LocalStorage.getCreatorChallenge()!)
68+
setSharedId(savedChallenge.sharedId!)
69+
setSavedSnackbarOpen(true)
70+
}
71+
catch (error) {
72+
setServerError(true)
73+
}
74+
}
75+
76+
return <>
77+
<Snackbar
78+
open={savedSnackbar}
79+
onClose={() => setSavedSnackbarOpen(false)}
80+
autoHideDuration={2000}
81+
message={t('editor.buttons.savedCorrectly')}
82+
/>
83+
<Tooltip title={!userLoggedIn ? t('editor.loginWarning') : ''} followCursor>
84+
<div>
85+
<CreatorActionButton data-testid="upsertButton" onClick={handleClick} disabled={!userLoggedIn} startIcon={Icon} variant='contained' nametag={nametag} />
86+
</div>
87+
</Tooltip>
88+
<DialogSnackbar open={serverError} onClose={() => setServerError(false)} message={t('editor.serverError')} />
89+
</>
90+
}

src/components/creator/Editor/CreatorContext.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ export type CreatorContextType = {
1111
index: number;
1212
setIndex: (index: number) => void;
1313
setMaps: (maps: any) => void;
14-
maps: SceneMap[]
14+
maps: SceneMap[];
15+
sharedId: string
16+
setSharedId: (id: string) => void
1517
};
1618

1719
const defaultCreatorContext = {
@@ -22,7 +24,9 @@ const defaultCreatorContext = {
2224
index: 0,
2325
setIndex: () => { },
2426
setMaps: () => { },
25-
maps: defaultChallenge('Duba').scene.maps
27+
maps: defaultChallenge('Duba').scene.maps,
28+
sharedId: "",
29+
setSharedId: (id: string) => {}
2630
}
2731

2832
export const CreatorContext = React.createContext<CreatorContextType>(defaultCreatorContext);
@@ -39,6 +43,7 @@ export const CreatorContextProvider: React.FC<CreatorProviderProps> = ({ childre
3943
const challenge = LocalStorage.getCreatorChallenge() || defaultChallenge("Duba")
4044
const [maps, setMaps] = useState(challenge.scene.maps)
4145
const [index, setIndex] = useState(defaultIndex)
46+
const [sharedId, setSharedId] = useState(challenge.sharedId || "")
4247

4348
const currentMap = maps[index] || challenge.scene.maps[index]
4449

@@ -49,11 +54,12 @@ export const CreatorContextProvider: React.FC<CreatorProviderProps> = ({ childre
4954

5055
useEffect(() => {
5156
challenge.scene.maps = maps
57+
challenge.sharedId = sharedId
5258
LocalStorage.saveCreatorChallenge(challenge)
53-
}, [maps, challenge])
59+
}, [maps, challenge, sharedId])
5460

5561
return (
56-
<CreatorContext.Provider value={{ selectedTool, setSelectedTool, currentMap, setCurrentMap, index, setIndex, setMaps, maps}}>
62+
<CreatorContext.Provider value={{ selectedTool, setSelectedTool, currentMap, setCurrentMap, sharedId: sharedId, setSharedId: setSharedId, index, setIndex, setMaps, maps}}>
5763
{children}
5864
</CreatorContext.Provider>
5965
);

src/components/creator/Editor/CreatorViewMode.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,20 @@ export const CreatorViewMode = () => {
3030
return (<>
3131
{challengeExists ? (
3232
<>
33-
<Header CenterComponent={<CreatorViewHeader challenge={challengeBeingEdited} />} SubHeader={<EditorSubHeader viewButton={<ReturnToEditionButton />} />} />
33+
<Header CenterComponent={<CreatorViewHeader title={challengeBeingEdited.title} />} SubHeader={<EditorSubHeader viewButton={<ReturnToEditionButton />} />} />
3434
<EmberView height='calc(100% - var(--creator-subheader-height))' path={EMBER_IMPORTED_CHALLENGE_PATH} />
3535
</>
3636
) : <></>}
3737
</>)
3838
}
3939

40-
const CreatorViewHeader = ({ challenge }: { challenge: SerializedChallenge }) => {
40+
export const CreatorViewHeader = ({ title }: { title: string }) => {
4141
const { t } = useTranslation('creator')
4242

4343
return <BetaBadge smaller={true}>
4444
<PBreadcrumbs>
4545
<HeaderText text={t("editor.previewModeHeader")} />
46-
<Typography>{challenge.title}</Typography>
46+
<Typography>{title}</Typography>
4747
</PBreadcrumbs>
4848
</BetaBadge>
4949

src/components/creator/Editor/Editor.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import { SceneEdition } from "./SceneEdition/SceneEdition";
44
import { CreatorContextProvider } from "./CreatorContext";
55
import { CreatorSubHeader } from "./EditorSubHeader/CreatorSubHeader";
66
import { useTranslation } from "react-i18next";
7-
import { DownloadButton } from "./ActionButtons/DownloadButton";
87
import { DiscardChallengeButton } from "./ActionButtons/DiscardChallengeButton";
98
import { PreviewButton } from "./ActionButtons/PreviewButton";
109
import { BetaBadge } from "../BetaBadge";
1110
import { useThemeContext } from "../../../theme/ThemeContext";
1211
import { useNavigate } from "react-router-dom";
1312
import { LocalStorage } from "../../../localStorage";
1413
import { useEffect } from "react";
14+
import { ShareButton } from "./ActionButtons/ShareChallenge/ShareButton";
1515

1616
export const CreatorEditor = () => {
1717
const { theme } = useThemeContext()
@@ -51,5 +51,5 @@ export const EditorSubHeader = (props: EditorSubHeaderProps) =>
5151
<CreatorSubHeader>
5252
<DiscardChallengeButton />
5353
{props.viewButton}
54-
<DownloadButton />
54+
<ShareButton/>
5555
</CreatorSubHeader>

0 commit comments

Comments
 (0)