= ({ showAboutSection = true, ...rest }) => {
17 | const { freq, scale, transform, opacity } = useSpring({
18 | from: {
19 | scale: 10,
20 | opacity: 0,
21 | transform: 'scale(0.8)',
22 | freq: '0.0125, 0.0',
23 | },
24 | to: { scale: 150, opacity: 1, transform: 'scale(0.9)', freq: '0.0, 0.0' },
25 | config: { duration: 3000 },
26 | })
27 |
28 | return (
29 |
30 |
31 |
36 |
37 |
38 |
45 |
53 |
54 |
55 |
56 |
60 |
61 |
62 |
63 | {showAboutSection && (
64 |
65 |
70 | Create and share gorgeous animated snippets of your code with the
71 | world.
72 |
73 |
74 | )}
75 |
76 | )
77 | }
78 |
--------------------------------------------------------------------------------
/packages/client/src/theme.tsx:
--------------------------------------------------------------------------------
1 | import { theme } from '@chakra-ui/core'
2 | import React from 'react'
3 |
4 | // Let's say you want to add custom colors
5 | export const customTheme = {
6 | ...theme,
7 | colors: {
8 | ...theme.colors,
9 | },
10 | icons: {
11 | ...theme.icons,
12 | /*
13 | * All icons sourced from FontAwesome. License pulled from https://github.com/FortAwesome/Font-Awesome/blob/4e6402443679e0a9d12c7401ac8783ef4646657f/less/fontawesome.less
14 | *
15 | * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com
16 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
17 | */
18 | pause: {
19 | path: (
20 |
24 | ),
25 | viewBox: '0 0 448 512',
26 | },
27 | play: {
28 | path: (
29 |
33 | ),
34 | viewBox: '0 0 448 512',
35 | },
36 | palette: {
37 | path: (
38 |
42 | ),
43 | viewBox: '0 0 512 512',
44 | },
45 | codingLanguage: {
46 | path: (
47 |
51 | ),
52 | viewBox: '0 0 640 512',
53 | },
54 | google: {
55 | path: (
56 |
60 | ),
61 | viewBox: '0 0 496 512',
62 | },
63 | github: {
64 | path: (
65 |
69 | ),
70 | viewBox: '0 0 488 512',
71 | },
72 | },
73 | } as const
74 |
--------------------------------------------------------------------------------
/packages/functions/src/vendor/timesnap/lib/immediate-canvas-handler.js:
--------------------------------------------------------------------------------
1 | /**
2 | * BSD 3-Clause License
3 | *
4 | * Copyright (c) 2018-2020, Steve Tung
5 | * All rights reserved.
6 | *
7 | * Redistribution and use in source and binary forms, with or without
8 | * modification, are permitted provided that the following conditions are met:
9 | *
10 | * * Redistributions of source code must retain the above copyright notice, this
11 | * list of conditions and the following disclaimer.
12 | *
13 | * * Redistributions in binary form must reproduce the above copyright notice,
14 | * this list of conditions and the following disclaimer in the documentation
15 | * and/or other materials provided with the distribution.
16 | *
17 | * * Neither the name of the copyright holder nor the names of its
18 | * contributors may be used to endorse or promote products derived from
19 | * this software without specific prior written permission.
20 | *
21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 | */
32 |
33 | const timeHandler = require('./overwrite-time');
34 | const makeCanvasCapturer = require('./make-canvas-capturer');
35 |
36 | var overwriteTime = timeHandler.overwriteTime;
37 | var oldGoToTime = timeHandler.goToTime;
38 |
39 | const canvasCapturer = makeCanvasCapturer(function (page) {
40 | return page.evaluate(function () {
41 | return window._timesnap_canvasData;
42 | }).then(function (dataUrl) {
43 | var data = dataUrl.slice(dataUrl.indexOf(',') + 1);
44 | return new Buffer(data, 'base64');
45 | });
46 | });
47 |
48 | module.exports = function (config) {
49 | var capturer = canvasCapturer(config);
50 | var canvasCaptureMode = capturer.canvasCaptureMode;
51 | var canvasSelector = capturer.canvasSelector;
52 | var quality = capturer.quality;
53 | var page = config.page;
54 | var goToTime;
55 | if (config.alwaysSaveCanvasData) {
56 | goToTime = function (browserFrames, time) {
57 | // Goes to a certain time. Can't go backwards
58 | return page.evaluate(function (ms, canvasSelector, type, quality) {
59 | window._timesnap_processUntilTime(ms);
60 | var canvasElement = document.querySelector(canvasSelector);
61 | window._timesnap_canvasData = canvasElement.toDataURL(type, quality);
62 | }, time, canvasSelector, canvasCaptureMode, quality);
63 | };
64 | } else {
65 | goToTime = oldGoToTime;
66 | }
67 |
68 | const goToTimeAndAnimateForCapture = function (browserFrames, time) {
69 | // Goes to a certain time. Can't go backwards
70 | return page.evaluate(function (ms, canvasSelector, type, quality) {
71 | window._timesnap_processUntilTime(ms);
72 | return window._timesnap_runFramePreparers(ms, function () {
73 | window._timesnap_runAnimationFrames();
74 | var canvasElement = document.querySelector(canvasSelector);
75 | window._timesnap_canvasData = canvasElement.toDataURL(type, quality);
76 | });
77 | }, time, canvasSelector, canvasCaptureMode, quality);
78 | };
79 |
80 | var goToTimeAndAnimate;
81 | if (config.alwaysSaveCanvasData) {
82 | goToTimeAndAnimate = goToTimeAndAnimateForCapture;
83 | } else {
84 | goToTimeAndAnimate = function (browserFrames, time) {
85 | // Goes to a certain time. Can't go backwards
86 | return page.evaluate(function (ms) {
87 | window._timesnap_processUntilTime(ms);
88 | return window._timesnap_runFramePreparers(ms, window._timesnap_runAnimationFrames);
89 | }, time);
90 | };
91 | }
92 |
93 | return {
94 | capturer: capturer,
95 | timeHandler: {
96 | overwriteTime,
97 | goToTime,
98 | goToTimeAndAnimate,
99 | goToTimeAndAnimateForCapture
100 | }
101 | };
102 | };
103 |
--------------------------------------------------------------------------------
/packages/functions/src/vendor/timesnap/lib/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * BSD 3-Clause License
3 | *
4 | * Copyright (c) 2018-2020, Steve Tung
5 | * All rights reserved.
6 | *
7 | * Redistribution and use in source and binary forms, with or without
8 | * modification, are permitted provided that the following conditions are met:
9 | *
10 | * * Redistributions of source code must retain the above copyright notice, this
11 | * list of conditions and the following disclaimer.
12 | *
13 | * * Redistributions in binary form must reproduce the above copyright notice,
14 | * this list of conditions and the following disclaimer in the documentation
15 | * and/or other materials provided with the distribution.
16 | *
17 | * * Neither the name of the copyright holder nor the names of its
18 | * contributors may be used to endorse or promote products derived from
19 | * this software without specific prior written permission.
20 | *
21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 | */
32 | const fs = require('fs');
33 | const path = require('path');
34 | const sprintf = require('sprintf-js').sprintf;
35 |
36 | const promiseLoop = function (condition, body) {
37 | var loop = function () {
38 | if (condition()) {
39 | return body().then(loop);
40 | }
41 | };
42 | return Promise.resolve().then(loop);
43 | };
44 |
45 | const getBrowserFrames = function (frame) {
46 | return [frame].concat(...frame.childFrames().map(getBrowserFrames));
47 | };
48 |
49 | const getSelectorDimensions = function (page, selector) {
50 | return page.evaluate(function (selector) {
51 | var el = document.querySelector(selector);
52 | var dim = el.getBoundingClientRect();
53 | if (el) {
54 | return {
55 | left: dim.left,
56 | top: dim.top,
57 | right: dim.right,
58 | bottom: dim.bottom,
59 | scrollX: window.scrollX,
60 | scrollY: window.scrollY,
61 | x: dim.x,
62 | y: dim.y,
63 | width: dim.width,
64 | height: dim.height
65 | };
66 | }
67 | }, selector);
68 | };
69 |
70 | const makeFilePathConverter = function (config) {
71 | var fileNameConverter = config.fileNameConverter;
72 | if (!fileNameConverter) {
73 | if (config.outputPattern) {
74 | fileNameConverter = function (num) {
75 | return sprintf(config.outputPattern, num);
76 | };
77 | } else if (config.frameProcessor && !config.outputDirectory) {
78 | fileNameConverter = function () {
79 | return undefined;
80 | };
81 | } else {
82 | fileNameConverter = function (num, maxNum) {
83 | var extension = config.screenshotType === 'jpeg' ? 'd.jpg' : 'd.png';
84 | var outputPattern = '%0' + maxNum.toString().length + extension;
85 | return sprintf(outputPattern, num);
86 | };
87 | }
88 | }
89 | return function (num, maxNum) {
90 | var fileName = fileNameConverter(num, maxNum);
91 | if (fileName) {
92 | return path.resolve(config.outputPath, fileName);
93 | } else {
94 | return undefined;
95 | }
96 | };
97 | };
98 |
99 | const writeFile = function (filePath, buffer) {
100 | makeFileDirectoryIfNeeded(filePath);
101 | return new Promise(function (resolve, reject) {
102 | fs.writeFile(filePath, buffer, 'binary', function (err) {
103 | if (err) {
104 | reject(err);
105 | }
106 | resolve();
107 | });
108 | });
109 | };
110 |
111 | const makeFileDirectoryIfNeeded = function (filepath) {
112 | var dir = path.parse(filepath).dir, ind, currDir;
113 | var directories = dir.split(path.sep);
114 | for (ind = 1; ind <= directories.length; ind++) {
115 | currDir = directories.slice(0, ind).join(path.sep);
116 | if (currDir && !fs.existsSync(currDir)) {
117 | fs.mkdirSync(currDir);
118 | }
119 | }
120 | };
121 |
122 | module.exports = {
123 | promiseLoop,
124 | getBrowserFrames,
125 | getSelectorDimensions,
126 | writeFile,
127 | makeFilePathConverter,
128 | makeFileDirectoryIfNeeded
129 | };
130 |
--------------------------------------------------------------------------------
/packages/client/src/components/Builder.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled/macro'
2 | import { SnippetDocument } from '@waves/shared'
3 | import React, { ComponentType, FC } from 'react'
4 |
5 | import { CODE_THEMES_DICT } from '../code-themes'
6 | import {
7 | BUILDER_MOBILE_BREAKPOINT,
8 | BUILDER_MOBILE_TINY_BREAKPOINT,
9 | } from '../const'
10 | import {
11 | PreviewProvider,
12 | SnippetProvider,
13 | usePreviewState,
14 | useSnippetDispatch,
15 | useSnippetState,
16 | } from '../context'
17 | import { Box, Flex, Text } from './core'
18 | import { Panel } from './Panel'
19 | import { Preview } from './Preview'
20 | import { PreviewContainer } from './PreviewContainer'
21 | import { TitleToolbar } from './TitleToolbar'
22 | import { Toolbar } from './Toolbar'
23 |
24 | type BuilderProps = {
25 | snippet?: SnippetDocument
26 | }
27 |
28 | const StyledBuilder = styled(Box)`
29 | @media (max-width: ${BUILDER_MOBILE_TINY_BREAKPOINT}) {
30 | border: none;
31 | padding: 0;
32 | }
33 | `
34 | const StyledBuilderContent = styled(Flex)`
35 | @media (max-width: ${BUILDER_MOBILE_BREAKPOINT}) {
36 | flex-direction: column-reverse;
37 | height: auto;
38 | }
39 | `
40 | const PreviewWrapper = styled.div`
41 | overflow-x: auto;
42 |
43 | @media (max-width: ${BUILDER_MOBILE_TINY_BREAKPOINT}) {
44 | margin: 0 -1rem;
45 | }
46 | `
47 |
48 | const MobileHelperText = styled(Text)`
49 | @media (min-width: ${BUILDER_MOBILE_TINY_BREAKPOINT}) {
50 | display: none;
51 | }
52 | `
53 |
54 | export const BuilderComponent: FC = () => {
55 | const {
56 | theme,
57 | backgroundColor,
58 | defaultWindowTitle,
59 | cycle,
60 | immediate,
61 | cycleSpeed,
62 | steps,
63 | springPreset,
64 | showLineNumbers,
65 | windowControlsType,
66 | windowControlsPosition,
67 | showBackground,
68 | } = useSnippetState()
69 | const snippetDispatch = useSnippetDispatch()
70 | const { currentStep } = usePreviewState()
71 | const themeObject = CODE_THEMES_DICT[theme]
72 |
73 | return (
74 |
83 |
84 |
85 |
86 |
96 |
97 |
98 |
101 | snippetDispatch({
102 | type: 'updateSnippetState',
103 | defaultWindowTitle: e.target.value,
104 | })
105 | }
106 | showBackground={showBackground}
107 | title={defaultWindowTitle}
108 | windowBackground={themeObject.theme.colors.background}
109 | windowControlsPosition={
110 | windowControlsPosition ?? themeObject.windowControlsPosition
111 | }
112 | windowControlsType={
113 | windowControlsType ?? themeObject.windowControlsType
114 | }
115 | >
116 |
125 |
126 |
127 |
128 | Scroll left and right!
129 |
130 |
131 |
132 |
133 | )
134 | }
135 |
136 | const withSnippetProvider = (
137 | Component: ComponentType
,
138 | ): FC
=> (props) => (
139 |
140 |
141 |
142 | )
143 |
144 | const withPreviewProvider =
(
145 | Component: ComponentType
,
146 | ): FC
=> (props) => {
147 | const { startingStep } = useSnippetState()
148 |
149 | return (
150 |
151 |
152 |
153 | )
154 | }
155 |
156 | export const Builder: FC = withSnippetProvider(
157 | withPreviewProvider(BuilderComponent),
158 | )
159 |
--------------------------------------------------------------------------------
/packages/functions/src/utils/validateFirebaseIdToken.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from 'firebase-functions'
2 |
3 | import { reportError } from './errors'
4 | import { auth } from './store'
5 |
6 | /**
7 | * Copyright 2016 Google Inc. All Rights Reserved.
8 | *
9 | * Licensed under the Apache License, Version 2.0 (the "License");
10 | * you may not use this file except in compliance with the License.
11 | * You may obtain a copy of the License at
12 | *
13 | * http://www.apache.org/licenses/LICENSE-2.0
14 | *
15 | * Unless required by applicable law or agreed to in writing, software
16 | * distributed under the License is distributed on an "AS IS" BASIS,
17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 | * See the License for the specific language governing permissions and
19 | * limitations under the License.
20 | *
21 | * https://github.com/firebase/functions-samples/blob/Node-8/authorized-https-endpoint/functions/index.js
22 | */
23 |
24 | // Express middleware that validates Firebase ID Tokens passed in the Authorization HTTP header.
25 | // The Firebase ID token needs to be passed as a Bearer token in the Authorization HTTP header like this:
26 | // `Authorization: Bearer `.
27 | // when decoded successfully, the ID Token content will be added as `req.user`.
28 | export const isAuthenticatedRoute = async (
29 | req: Request,
30 | res: Response,
31 | next: any,
32 | ) => {
33 | // console.log('Check if request is authorized with Firebase ID token');
34 |
35 | if (
36 | (!req.headers.authorization ||
37 | !req.headers.authorization.startsWith('Bearer ')) &&
38 | !(req.cookies && req.cookies.__session)
39 | ) {
40 | console.error(
41 | 'No Firebase ID token was passed as a Bearer token in the Authorization header.',
42 | 'Make sure you authorize your request by providing the following HTTP header:',
43 | 'Authorization: Bearer ',
44 | 'or by passing a "__session" cookie.',
45 | )
46 | reportError(
47 | 'No Firebase ID token was passed as a Bearer token in the Authorization header.',
48 | )
49 |
50 | res.status(403).send('Unauthorized')
51 |
52 | return
53 | }
54 |
55 | let idToken
56 | if (
57 | req.headers.authorization &&
58 | req.headers.authorization.startsWith('Bearer ')
59 | ) {
60 | // console.log('Found "Authorization" header');
61 | // Read the ID Token from the Authorization header.
62 | idToken = req.headers.authorization.split('Bearer ')[1]
63 | } else if (req.cookies) {
64 | // console.log('Found "__session" cookie');
65 | // Read the ID Token from cookie.
66 | idToken = req.cookies.__session
67 | } else {
68 | // No cookie
69 | res.status(403).send('Unauthorized')
70 | return
71 | }
72 |
73 | try {
74 | const decodedIdToken = await auth.verifyIdToken(idToken)
75 | console.log('ID Token correctly decoded', decodedIdToken)
76 |
77 | res.locals.user = decodedIdToken
78 | next()
79 | return
80 | } catch (error) {
81 | console.error('Error while verifying Firebase ID token:', error)
82 | reportError(error)
83 |
84 | res.status(403).send('Unauthorized')
85 | return
86 | }
87 | }
88 |
89 | /**
90 | * Sometimes we want to add the user to the middleware if they exist without enforcing auth.
91 | * This lets us append special metadata to storage buckets or other spots while allowing the APIs
92 | * to be public
93 | */
94 | export const addUserIfExists = async (
95 | req: Request,
96 | res: Response,
97 | next: any,
98 | ) => {
99 | if (
100 | (!req.headers.authorization ||
101 | !req.headers.authorization.startsWith('Bearer ')) &&
102 | !(req.cookies && req.cookies.__session)
103 | ) {
104 | next()
105 | return
106 | }
107 |
108 | let idToken
109 | if (
110 | req.headers.authorization &&
111 | req.headers.authorization.startsWith('Bearer ')
112 | ) {
113 | // console.log('Found "Authorization" header');
114 | // Read the ID Token from the Authorization header.
115 | idToken = req.headers.authorization.split('Bearer ')[1]
116 | } else if (req.cookies) {
117 | // console.log('Found "__session" cookie');
118 | // Read the ID Token from cookie.
119 | idToken = req.cookies.__session
120 | } else {
121 | // No cookie
122 | next()
123 | return
124 | }
125 |
126 | try {
127 | const decodedIdToken = await auth.verifyIdToken(idToken)
128 | console.log('ID Token correctly decoded', decodedIdToken)
129 | res.locals.user = decodedIdToken
130 | next()
131 | return
132 | } catch (error) {
133 | console.error('Error while verifying Firebase ID token:', error)
134 | reportError(error)
135 |
136 | // We still throw here because this smells weird if they're coming at us with a bombed out token.
137 | res.status(403).send('Unauthorized')
138 | return
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🌊 [Wave Snippets](https://wavesnippets.com/)
2 |
3 | 
4 |
5 | ## Create gorgeous animated snippets to share with the world.
6 |
7 | [](https://app.netlify.com/sites/wavesnippets/deploys)
8 | [](https://opensource.org/licenses/MIT)
9 |
10 | ### [Go to App](https://wavesnippets.com/)
11 |
12 | Wave Snippets lets you create beautiful, animated snippets of your code to help you illustrate complex concepts through motion. It allows you to create a series of code steps and describe the code as you go. Once you're finished, a high quality gif and video file is delivered straight to your inbox for you to share with the world!
13 |
14 | ### Features
15 |
16 | - 💯 Over 30 supported languages
17 | - 💅 8 different themes to choose from
18 | - 😎 GIF and Video Export
19 | - 🌊 No login necessary
20 | - 🙏 Auth to save your snippet library
21 | - ✏️ Annotate your code with titles and descriptions
22 | - 📸 Customize the appearance of the background and window
23 | - 🔮 4 different physics based animations
24 | - 💰 Line number support
25 | - 📍 99.4% TypeScript
26 | - 💡 Dark mode support
27 |
28 | ## How Does it Work?
29 |
30 | 1. Add your code step by step.
31 | 2. Add titles and descriptions for each step.
32 | 3. Customize the look and feel.
33 | 4. Export when you're ready.
34 | 5. We take care of the animations, GIF building, video encoding, and everything else!
35 |
36 | ## Examples
37 |
38 | 
39 |
40 | ## Roadmap
41 |
42 | There's a ton of things I want to add!
43 |
44 | - Snippet embeds
45 | - A snippet gallery of the most liked snippets
46 | - Typing effects for a snippet
47 | - Terminal mode effects
48 | - Snippet storage
49 | - One click shares to Twitter and Github
50 | - Export customization
51 |
52 | Want to help or add something else? [Open an issue](https://github.com/mwood23/wave-snippets/issues/new).
53 |
54 | ## Contributing
55 |
56 | PRs are welcome! If you find a bug or have a feature request please file a [Github issue](https://github.com/mwood23/wave-snippets/issues/new). If you'd like to PR some changes, do the following:
57 |
58 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then clone it to your local device
59 | 2. Go to the repo and run `yarn install && yarn bootstrap`. This project runs on a particular version of yarn and npm so you will need to have the same versions as mentioned in the `package.json`. I suggest using [nvm](https://github.com/nvm-sh/nvm) and [yvm](https://github.com/nvm-sh/nvm).
60 | 3. Setup a Firebase project on your personal account
61 | 4. Create a `.env.local` file and fill in the following fields
62 |
63 | ```
64 | REACT_APP_CLOUD_FUNCTIONS_URL
65 | REACT_APP_FIREBASE_API_KEY
66 | REACT_APP_FIREBASE_AUTH_DOMAIN
67 | REACT_APP_FIREBASE_DATABASE_URL
68 | REACT_APP_FIREBASE_PROJECT_ID
69 | REACT_APP_FIREBASE_STORAGE_BUCKET
70 | REACT_APP_FIREBASE_MESSAGING_SENDER_ID
71 | REACT_APP_FIREBASE_APP_ID
72 | REACT_APP_FIREBASE_MEASUREMENT_ID
73 | ```
74 |
75 | 3. Run the app with `yarn dev:client`.
76 | 4. Make your changes.
77 | 5. Submit a PR with your changes.
78 |
79 | If you are making changes to the Cloud Functions you will need to create your own Firebase project and deploy it unfortunately. At the time of writing, there is no emulator for Google Storage, and event functions like when a user signs up. Reach out to the maintainer for help setting this up.
80 |
81 | ## A Big Thank You To...
82 |
83 | [Carbon](https://carbon.now.sh/): Carbon was a big inspiration for me to take this project on an all the snippets made from it has made me and a bunch of other people better developers.
84 |
85 | [Rodrigo Pombo](https://twitter.com/pomber): The author code surfer and another big inspiration to try to make this work. His open source work is great and saved the day with thinking ahead to break out the core of Code Surfer into its own package for me to stumble upon.
86 |
87 | [Tungs](https://github.com/tungs): The author of Timecut and Timesnap. Who would have guessed how difficult building gifs and videos in the browser would be. I was just about ready to give up on this project until I stumbled across this medium post and decided to give it a try.
88 |
89 | [Paul Henschel](https://twitter.com/0xca0a): The author of React Spring. Discovering his library a couple years ago was a gamechanger for seeing what animations on the web could be. This project uses React Spring under the hood to animate between the steps.
90 |
91 | [Chakra UI](https://github.com/chakra-ui/chakra-ui): This was my first app using Chakra UI as the design system framework and it was a huge relief. Their TS support is good, components work great, and it never got in my way like most of design system frameworks do. Highly recommend.
92 |
93 | ###### Made by [Marcus Wood](https://marcuswood.io/)
94 |
--------------------------------------------------------------------------------
/packages/functions/src/vendor/timesnap/lib/capture-screenshot.js:
--------------------------------------------------------------------------------
1 | /**
2 | * BSD 3-Clause License
3 | *
4 | * Copyright (c) 2018-2020, Steve Tung
5 | * All rights reserved.
6 | *
7 | * Redistribution and use in source and binary forms, with or without
8 | * modification, are permitted provided that the following conditions are met:
9 | *
10 | * * Redistributions of source code must retain the above copyright notice, this
11 | * list of conditions and the following disclaimer.
12 | *
13 | * * Redistributions in binary form must reproduce the above copyright notice,
14 | * this list of conditions and the following disclaimer in the documentation
15 | * and/or other materials provided with the distribution.
16 | *
17 | * * Neither the name of the copyright holder nor the names of its
18 | * contributors may be used to endorse or promote products derived from
19 | * this software without specific prior written permission.
20 | *
21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 | */
32 |
33 | const { getSelectorDimensions, makeFilePathConverter, makeFileDirectoryIfNeeded } = require('./utils.js');
34 |
35 | module.exports = function (config) {
36 | var page = config.page;
37 | var log = config.log;
38 | var frameProcessor = config.frameProcessor;
39 | var screenshotClip;
40 | var filePathConverter = makeFilePathConverter(config);
41 | return {
42 | beforeCapture: function () {
43 | return Promise.resolve().then(function () {
44 | if (config.selector) {
45 | return getSelectorDimensions(page, config.selector).then(function (dimensions) {
46 | if (!dimensions) {
47 | log('Warning: no element found for ' + config.selector);
48 | return;
49 | }
50 | return dimensions;
51 | });
52 | }
53 | }).then(function (dimensions) {
54 | var viewport = page.viewport();
55 | var x = config.xOffset || config.left || 0;
56 | var y = config.yOffset || config.top || 0;
57 | var right = config.right || 0;
58 | var bottom = config.bottom || 0;
59 | var width;
60 | var height;
61 | if (dimensions) {
62 | width = config.width || (dimensions.width - x - right);
63 | height = config.height || (dimensions.height - y - bottom);
64 | x += dimensions.scrollX + dimensions.left;
65 | y += dimensions.scrollY + dimensions.top;
66 | } else {
67 | width = config.width || (viewport.width - x - right);
68 | height = config.height || (viewport.height - y - bottom);
69 | }
70 | width = Math.ceil(width);
71 | if (config.roundToEvenWidth && (width % 2 === 1)) {
72 | width++;
73 | }
74 | height = Math.ceil(height);
75 | if (config.roundToEvenHeight && (height % 2 === 1)) {
76 | height++;
77 | }
78 | screenshotClip = {
79 | x: x,
80 | y: y,
81 | width: width,
82 | height: height
83 | };
84 | if (screenshotClip.height <= 0) {
85 | throw new Error('Capture height is ' + (screenshotClip.height < 0 ? 'negative!' : '0!'));
86 | }
87 | if (screenshotClip.width <= 0) {
88 | throw new Error('Capture width is ' + (screenshotClip.width < 0 ? 'negative!' : '0!'));
89 | }
90 | });
91 | },
92 | capture: function (sameConfig, frameCount, framesToCapture) {
93 | var filePath = filePathConverter(frameCount, framesToCapture);
94 | if (filePath) {
95 | makeFileDirectoryIfNeeded(filePath);
96 | }
97 | log('Capturing Frame ' + frameCount + (filePath ? ' to ' + filePath : '') + '...');
98 | var p = page.screenshot({
99 | type: config.screenshotType,
100 | quality: typeof config.screenshotQuality === 'number' ? config.screenshotQuality * 100 : config.screenshotQuality,
101 | path: filePath,
102 | clip: screenshotClip,
103 | omitBackground: config.transparentBackground ? true : false
104 | });
105 | if (frameProcessor) {
106 | p = p.then(function (buffer) {
107 | return frameProcessor(buffer, frameCount, framesToCapture);
108 | });
109 | }
110 | return p;
111 | }
112 | };
113 | };
114 |
--------------------------------------------------------------------------------
/packages/client/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { Form, Formik } from 'formik'
2 | import React, { FC, useRef } from 'react'
3 | import {
4 | Link as RouterLink,
5 | LinkProps as RouterLinkProps,
6 | } from 'react-router-dom'
7 | import { object, string } from 'yup'
8 |
9 | import { useConvertKit } from '../hooks/useConvertKit'
10 | import {
11 | Box,
12 | Button,
13 | Flex,
14 | FormControl,
15 | FormErrorMessage,
16 | FormLabel,
17 | Input,
18 | Link,
19 | LinkProps,
20 | Popover,
21 | PopoverCloseButton,
22 | PopoverContent,
23 | PopoverTrigger,
24 | Text,
25 | useColorMode,
26 | useDisclosure,
27 | } from './core'
28 |
29 | type FooterProps = {}
30 |
31 | const WavesFooterLink: FC = ({ children, ...props }) => (
32 |
33 | {children}
34 |
35 | )
36 |
37 | const InternalLink: FC = ({
38 | children,
39 | ...props
40 | }) => (
41 |
46 | {children}
47 |
48 | )
49 |
50 | const NewsletterSignup = () => {
51 | const [subscribeToNewsletter] = useConvertKit()
52 | const emailInput = useRef(null)
53 | const { colorMode } = useColorMode()
54 | const color = { light: 'cyan.600', dark: 'cyan.400' }
55 | const { onOpen, isOpen, onClose } = useDisclosure()
56 |
57 | return (
58 |
67 |
68 |
81 |
82 |
83 |
84 | {
89 | setSubmitting(true)
90 | await subscribeToNewsletter(email)
91 | setSubmitting(false)
92 |
93 | onClose()
94 | }}
95 | validationSchema={object().shape({
96 | email: string()
97 | .required('Email is required.')
98 | .email('Must be a valid email.'),
99 | })}
100 | >
101 | {({
102 | handleSubmit,
103 | handleBlur,
104 | handleChange,
105 | values,
106 | touched,
107 | errors,
108 | isValid,
109 | isSubmitting,
110 | }) => (
111 |
134 | )}
135 |
136 |
137 |
138 | )
139 | }
140 |
141 | export const Footer: FC = () => (
142 |
150 |
156 | About
157 |
161 | Feedback
162 |
163 |
167 | Source
168 |
169 |
170 | Terms
171 | Privacy
172 |
173 |
174 |
175 | created by{' '}
176 |
177 | Marcus Wood
178 |
179 |
180 |
181 |
182 | )
183 |
--------------------------------------------------------------------------------
/packages/functions/src/vendor/timesnap/lib/overwrite-random.js:
--------------------------------------------------------------------------------
1 | /**
2 | * BSD 3-Clause License
3 | *
4 | * Copyright (c) 2018-2020, Steve Tung
5 | * All rights reserved.
6 | *
7 | * Redistribution and use in source and binary forms, with or without
8 | * modification, are permitted provided that the following conditions are met:
9 | *
10 | * * Redistributions of source code must retain the above copyright notice, this
11 | * list of conditions and the following disclaimer.
12 | *
13 | * * Redistributions in binary form must reproduce the above copyright notice,
14 | * this list of conditions and the following disclaimer in the documentation
15 | * and/or other materials provided with the distribution.
16 | *
17 | * * Neither the name of the copyright holder nor the names of its
18 | * contributors may be used to endorse or promote products derived from
19 | * this software without specific prior written permission.
20 | *
21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 | */
32 |
33 | // unrandomizer seed constants
34 | // default seed values are only used if all of the seed values end up being 0
35 | const defaultSeed1 = 10;
36 | const defaultSeed2 = 20;
37 | const defaultSeed3 = 0;
38 | const defaultSeed4 = 0;
39 | const seedIterations = 10;
40 | const randomSeedLimit = 1000000000;
41 |
42 | const overwriteRandom = function (page, unrandom, log) {
43 | if (unrandom === undefined || unrandom === false) {
44 | return;
45 | }
46 | var args, seed;
47 | if (Array.isArray(unrandom)) {
48 | args = unrandom;
49 | } else if (unrandom === 'random-seed') {
50 | seed = Math.floor(Math.random() * randomSeedLimit) + 1;
51 | log('Generated seed: ' + seed);
52 | args = [ seed ];
53 | } else if (typeof unrandom === 'string') {
54 | args = unrandom.split(',').map(n=>parseInt(n));
55 | } else if (typeof unrandom === 'number') {
56 | args = [ unrandom ];
57 | } else {
58 | args = [];
59 | }
60 | return overwritePageRandom(page, ...args);
61 | };
62 |
63 | const overwritePageRandom = function (page, seed1 = 0, seed2 = 0, seed3 = 0, seed4 = 0) {
64 | return page.evaluateOnNewDocument(function (config) {
65 | (function (exports) {
66 | let shift1 = 23;
67 | let shift2 = 17;
68 | let shift3 = 26;
69 |
70 | let state0 = new ArrayBuffer(8);
71 | let state1 = new ArrayBuffer(8);
72 |
73 | let state0SInts = new Int32Array(state0);
74 | let state1SInts = new Int32Array(state1);
75 |
76 | let state0UInt = new Uint32Array(state0);
77 | let state1UInt = new Uint32Array(state1);
78 |
79 | state0UInt[0] = config.seed1;
80 | state0UInt[1] = config.seed3;
81 | state1UInt[0] = config.seed2;
82 | state1UInt[1] = config.seed4;
83 |
84 | if (!state0SInts[0] && !state0SInts[1] && !state1SInts[0] && !state1SInts[1]) {
85 | // if the states are all zero, it does not advance to a new state
86 | // in this case, set the states to the default seeds
87 | state0UInt[0] = config.defaultSeed1;
88 | state0UInt[1] = config.defaultSeed3;
89 | state1UInt[0] = config.defaultSeed2;
90 | state1UInt[1] = config.defaultSeed4;
91 | }
92 |
93 | let _xorshift128 = function () {
94 | let xA = state1SInts[0];
95 | let xB = state1SInts[1];
96 | let yA = state0SInts[0];
97 | let yB = state0SInts[1];
98 |
99 | yA = yA ^ ((yA << shift1) | (yB >>> (32 - shift1)));
100 | yB = yB ^ (yB << shift1);
101 |
102 | yB = yB ^ ((yA << (32 - shift2)) | (yB >>> shift2));
103 | yA = yA ^ (yA >>> shift2);
104 |
105 | yA = yA ^ xA;
106 | yB = yB ^ xB;
107 |
108 | yB = yB ^ ((xA << (32 - shift3)) | (xB >>> shift3));
109 | yA = yA ^ (xA >>> shift3);
110 |
111 | state0SInts[0] = xA;
112 | state0SInts[1] = xB;
113 | state1SInts[0] = yA;
114 | state1SInts[1] = yB;
115 | };
116 |
117 | let byteLimit = Math.pow(2, 32);
118 | let mantissaLimit = Math.pow(2, 53);
119 |
120 | let _statesToDouble = function () {
121 | let aSum = state0UInt[0] + state1UInt[0];
122 | let bSum = state0UInt[1] + state1UInt[1];
123 | if (bSum >= byteLimit) {
124 | aSum = aSum + 1;
125 | bSum -= byteLimit;
126 | }
127 | aSum = aSum & 0x001FFFFF;
128 | return (aSum * byteLimit + bSum) / mantissaLimit;
129 | };
130 |
131 | for (let i = 0; i < config.seedIterations; i++) {
132 | _xorshift128();
133 | }
134 |
135 | // overwriting built-in functions...
136 | exports.Math.random = function () {
137 | _xorshift128();
138 | return _statesToDouble();
139 | };
140 | })(this);
141 | }, {
142 | seed1, seed2, seed3, seed4,
143 | defaultSeed1, defaultSeed2, defaultSeed3, defaultSeed4,
144 | seedIterations
145 | });
146 | };
147 |
148 |
149 | module.exports = {
150 | overwriteRandom
151 | };
--------------------------------------------------------------------------------
/packages/client/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
20 | ),
21 | )
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void
26 | }
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
32 | if (publicUrl.origin !== window.location.origin) {
33 | // Our service worker won't work if PUBLIC_URL is on a different origin
34 | // from what our page is served on. This might happen if a CDN is used to
35 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
36 | return
37 | }
38 |
39 | window.addEventListener('load', () => {
40 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
41 |
42 | if (isLocalhost) {
43 | // This is running on localhost. Let's check if a service worker still exists or not.
44 | checkValidServiceWorker(swUrl, config)
45 |
46 | // Add some additional logging to localhost, pointing developers to the
47 | // service worker/PWA documentation.
48 | navigator.serviceWorker.ready.then(() => {
49 | console.log(
50 | 'This web app is being served cache-first by a service ' +
51 | 'worker. To learn more, visit https://bit.ly/CRA-PWA',
52 | )
53 | })
54 | } else {
55 | // Is not localhost. Just register service worker
56 | registerValidSW(swUrl, config)
57 | }
58 | })
59 | }
60 | }
61 |
62 | function registerValidSW(swUrl: string, config?: Config) {
63 | navigator.serviceWorker
64 | .register(swUrl)
65 | .then((registration) => {
66 | registration.onupdatefound = () => {
67 | const installingWorker = registration.installing
68 | if (installingWorker == null) {
69 | return
70 | }
71 | installingWorker.onstatechange = () => {
72 | if (installingWorker.state === 'installed') {
73 | if (navigator.serviceWorker.controller) {
74 | // At this point, the updated precached content has been fetched,
75 | // but the previous service worker will still serve the older
76 | // content until all client tabs are closed.
77 | console.log(
78 | 'New content is available and will be used when all ' +
79 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.',
80 | )
81 |
82 | // Execute callback
83 | if (config?.onUpdate) {
84 | config.onUpdate(registration)
85 | }
86 | } else {
87 | // At this point, everything has been precached.
88 | // It's the perfect time to display a
89 | // "Content is cached for offline use." message.
90 | console.log('Content is cached for offline use.')
91 |
92 | // Execute callback
93 | if (config?.onSuccess) {
94 | config.onSuccess(registration)
95 | }
96 | }
97 | }
98 | }
99 | }
100 | })
101 | .catch((error) => {
102 | console.error('Error during service worker registration:', error)
103 | })
104 | }
105 |
106 | function checkValidServiceWorker(swUrl: string, config?: Config) {
107 | // Check if the service worker can be found. If it can't reload the page.
108 | fetch(swUrl, {
109 | headers: { 'Service-Worker': 'script' },
110 | })
111 | .then((response) => {
112 | // Ensure service worker exists, and that we really are getting a JS file.
113 | const contentType = response.headers.get('content-type')
114 | if (
115 | response.status === 404 ||
116 | (contentType != null && contentType.indexOf('javascript') === -1)
117 | ) {
118 | // No service worker found. Probably a different app. Reload the page.
119 | navigator.serviceWorker.ready.then((registration) => {
120 | registration.unregister().then(() => {
121 | window.location.reload()
122 | })
123 | })
124 | } else {
125 | // Service worker found. Proceed as normal.
126 | registerValidSW(swUrl, config)
127 | }
128 | })
129 | .catch(() => {
130 | console.log(
131 | 'No internet connection found. App is running in offline mode.',
132 | )
133 | })
134 | }
135 |
136 | export function unregister() {
137 | if ('serviceWorker' in navigator) {
138 | navigator.serviceWorker.ready
139 | .then((registration) => {
140 | registration.unregister()
141 | })
142 | .catch((error) => {
143 | console.error(error.message)
144 | })
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/packages/client/src/components/Nav.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled/macro'
2 | import React, { FC, useEffect } from 'react'
3 | import { useHistory, useLocation, useRouteMatch } from 'react-router-dom'
4 |
5 | import { useAuthDispatch, useAuthState } from '../context'
6 | import { useOAuth } from '../hooks/useOAuth'
7 | import { TEMPLATES, TEMPLATES_DICT } from '../templates'
8 | import { isMatchParamATemplate } from '../utils'
9 | import {
10 | Avatar,
11 | Box,
12 | Button,
13 | Icon,
14 | IconButton,
15 | Menu,
16 | MenuButton,
17 | MenuItem,
18 | MenuList,
19 | useColorMode,
20 | } from './core'
21 | import { useCreateToast } from './Toast'
22 |
23 | type NavProps = {}
24 |
25 | const StyledName = styled.span``
26 |
27 | const StyledProfileMenuButton = styled(MenuButton)`
28 | @media (max-width: 985px) {
29 | min-width: auto;
30 |
31 | ${StyledName} {
32 | display: none;
33 | }
34 | }
35 | `
36 |
37 | export const Nav: FC = () => {
38 | const history = useHistory()
39 | const location = useLocation()
40 | const { params } = useRouteMatch<{ snippetID?: string }>()
41 | const { colorMode, toggleColorMode } = useColorMode()
42 | const { user, isLoading } = useAuthState()
43 | const { logout } = useAuthDispatch()
44 | const [
45 | loginWithGoogle,
46 | { loading: googleLoading, error: googleError },
47 | ] = useOAuth({ provider: 'google' })
48 | const [
49 | loginWithGithub,
50 | { loading: githubLoading, error: githubError },
51 | ] = useOAuth({ provider: 'github' })
52 |
53 | const toast = useCreateToast()
54 |
55 | useEffect(() => {
56 | if (googleError || githubError) {
57 | toast(googleError ?? githubError, { type: 'error' })
58 | }
59 | // eslint-disable-next-line react-hooks/exhaustive-deps
60 | }, [googleError, githubError])
61 |
62 | return (
63 |
64 |
71 | {/*
72 | // @ts-ignore */}
73 | {/* */}
76 | {(location.pathname === '/' ||
77 | isMatchParamATemplate(params?.snippetID)) && (
78 |
101 | )}
102 | {user ? (
103 |
156 | ) : (
157 |
187 | )}
188 |
189 | )
190 | }
191 |
--------------------------------------------------------------------------------
/packages/client/src/pages/About.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react'
2 |
3 | import {
4 | Box,
5 | Heading,
6 | Hero,
7 | Link,
8 | List,
9 | ListItem,
10 | Page,
11 | Text,
12 | } from '../components'
13 |
14 | export const AboutPage: FC = () => (
15 |
16 |
17 |
18 |
19 | What is Wave Snippets?
20 |
21 |
22 | Wave Snippets lets you create beautiful, animated snippets of your code
23 | to help you illustrate complex concepts through motion. It allows you to
24 | create a series of code steps and describe the code as you go. Once
25 | you're finished, a high quality gif and video file is delivered
26 | straight to your inbox for you to share with the world!
27 |
28 |
29 |
30 |
31 |
32 | How does it work?
33 |
34 |
35 | Add your code step by step
36 | Add titles and descriptions for each step
37 | Customize the look and feel
38 | Export when you're ready
39 |
40 | We take care of the animations, GIF building, video encoding, and
41 | everything else!
42 |
43 |
44 |
45 |
46 |
47 |
48 | What's on the Roadmap?
49 |
50 |
51 | So many things! Once I was able to export the snippets consistently the
52 | ideas started flowing. Here are some things I have planned right now:
53 |
54 |
55 | Snippet embeds
56 | A snippet gallery of the most liked snippets
57 | Typing effects for a snippet
58 | Terminal mode effects
59 | Snippet storage
60 | One click shares to Twitter and Github
61 | Export customization
62 |
63 |
64 | Have a feature you'd like to request? The best way is to{' '}
65 |
66 | reach out to me
67 | {' '}
68 | on Twitter!
69 |
70 |
71 |
72 |
73 |
74 | How is this Different than Carbon?
75 |
76 |
77 |
78 | Carbon
79 |
80 | is an awesome app for images of your code, but it doesn't do
81 | videos. It was one of my main inspirations for creating this app, and
82 | highly recommend checking them out if you want to create static images.
83 |
84 |
85 |
86 |
87 |
88 | A Big Thank You To...
89 |
90 |
91 |
92 |
93 | Carbon
94 |
95 |
96 | : Carbon was a big inspiration for me to take this project on an all the
97 | snippets made from it has made me and a bunch of other people better
98 | developers.
99 |
100 |
101 |
102 |
103 | Rodrigo Pombo
104 |
105 |
106 | : The author code surfer and another big inspiration to try to make this
107 | work. His open source work is great and saved the day with thinking
108 | ahead to break out the core of Code Surfer into its{' '}
109 |
113 | own package
114 | {' '}
115 | for me to stumble upon.
116 |
117 |
118 |
119 |
120 | Tungs
121 |
122 |
123 | {/* TODO Backlink to my blog here */}: The author of Timecut and
124 | Timesnap. Who would have guessed how difficult building gifs and videos
125 | in the browser would be. I was just about ready to give up on this
126 | project until I stumbled across{' '}
127 |
128 | this medium post
129 | {' '}
130 | and decided to give it a try.
131 |
132 |
133 |
134 |
135 | Paul Henschel
136 |
137 |
138 | : The author of React Spring. Discovering his library a couple years ago
139 | was a gamechanger for seeing what animations on the web could be. This
140 | project uses React Spring under the hood to animate between the steps.
141 |
142 |
143 |
144 |
145 | Chakra UI
146 |
147 |
148 | : This was my first app using Chakra UI as the design system framework
149 | and it was a huge relief. Their TS support is good, components work
150 | great, and it never got in my way like most of design system frameworks
151 | do. Highly recommend.
152 |
153 |
154 |
155 | )
156 |
--------------------------------------------------------------------------------
/packages/client/src/components/TitleToolbar.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled'
2 | import { map, sortBy } from 'ramda'
3 | import React, { FC, useRef } from 'react'
4 |
5 | import { MAX_NUMBER_OF_TAGS, TAGS, Tag as TagType } from '../const'
6 | import { useSnippetDispatch, useSnippetState } from '../context'
7 | import { useSearch } from '../hooks'
8 | import { getColorByTagGroup, pipeVal } from '../utils'
9 | import {
10 | Box,
11 | Flex,
12 | IconButton,
13 | Input,
14 | Popover,
15 | PopoverContent,
16 | PopoverHeader,
17 | PopoverTrigger,
18 | TagIcon,
19 | TagLabel,
20 | Text,
21 | } from './core'
22 | import { SnippetTag, SnippetTags } from './SnippetTags'
23 |
24 | type TitleToolbarProps = {}
25 |
26 | const TagTriggerButton = styled(IconButton)`
27 | /* emotion-disable-server-rendering-unsafe-selector-warning-please-do-not-use-this-
28 | the-warning-exists-for-a-reason */
29 | &[aria-expanded='true'] {
30 | transform: rotate(-45deg);
31 | }
32 | `
33 |
34 | const StyledTitleToolbar = styled(Flex)`
35 | @media (max-width: 710px) {
36 | flex-direction: column;
37 | align-items: flex-start;
38 |
39 | & > * {
40 | margin-bottom: 1rem;
41 | }
42 | }
43 | `
44 |
45 | export const TitleToolbar: FC = () => {
46 | const snippetDispatch = useSnippetDispatch()
47 | const { name, tags: activeSnippetTags } = useSnippetState()
48 | const { searchText, setSearchText, results } = useSearch({
49 | collection: TAGS,
50 | uidFieldName: 'value',
51 | // @ts-ignore Types getting lost even though they're right :(
52 | indexes: ['name', 'aliases'],
53 | })
54 | const searchInputRef = useRef()
55 | const hasMaxTags = activeSnippetTags.length >= MAX_NUMBER_OF_TAGS
56 |
57 | return (
58 |
59 | {
61 | snippetDispatch({
62 | type: 'updateSnippetState',
63 | name: e.target.value,
64 | })
65 | }}
66 | placeholder="Add name..."
67 | value={name ?? ''}
68 | variant="unstyled"
69 | width="300px"
70 | />
71 |
72 |
73 | {
75 | snippetDispatch({
76 | // Move this to its own action if this gets more complicated than a one liner
77 | type: 'updateSnippetState',
78 | tags: activeSnippetTags.filter((activeTag) => activeTag !== t),
79 | })
80 | }}
81 | tags={activeSnippetTags}
82 | />
83 |
84 |
89 |
90 |
97 |
98 |
106 |
107 | {
109 | setSearchText(e.target.value)
110 | }}
111 | placeholder="Search..."
112 | pr="4"
113 | ref={searchInputRef}
114 | size="sm"
115 | value={searchText}
116 | variant="unstyled"
117 | />
118 |
126 |
127 | {activeSnippetTags.length}/{MAX_NUMBER_OF_TAGS}
128 |
129 |
130 |
131 |
132 | {pipeVal(
133 | results,
134 | sortBy((a) => a.name),
135 | map((t) => {
136 | const isActivelySelected = activeSnippetTags.some(
137 | (activeTags) => activeTags === t.value,
138 | )
139 | return (
140 | {
146 | snippetDispatch({
147 | // Move this to its own action if this gets more complicated than a one liner
148 | type: 'updateSnippetState',
149 | tags: [...activeSnippetTags, t.value],
150 | })
151 | }}
152 | opacity={hasMaxTags ? 0.8 : 1}
153 | pointerEvents={
154 | hasMaxTags || isActivelySelected ? 'none' : 'auto'
155 | }
156 | rounded="full"
157 | size="sm"
158 | variantColor={getColorByTagGroup(t.group)}
159 | >
160 | {isActivelySelected && }
161 | {t.name}
162 |
163 | )
164 | }),
165 | )}
166 |
167 |
168 |
169 |
170 |
171 | )
172 | }
173 |
--------------------------------------------------------------------------------
/packages/client/src/components/Preview.tsx:
--------------------------------------------------------------------------------
1 | import { CodeSurfer } from '@code-surfer/standalone'
2 | import styled from '@emotion/styled'
3 | import { InputStep } from '@waves/shared'
4 | import React, { forwardRef, useEffect, useState } from 'react'
5 | import { animated, useSpring } from 'react-spring'
6 | import { ThemeProvider } from 'theme-ui'
7 | import { useDebouncedCallback } from 'use-debounce'
8 |
9 | import { CODE_THEMES_DICT } from '../code-themes'
10 | import {
11 | ANIMATION_PRESETS_DICT,
12 | DEFAULT_ANIMATION_PRESET,
13 | DEFAULT_CYCLE_SPEED,
14 | DEFAULT_PREVIEW_THEME,
15 | DEFAULT_SHOW_NUMBERS,
16 | } from '../const'
17 | import { usePreviewDispatch, usePreviewState } from '../context'
18 | import { noop } from '../utils'
19 | import { Box, Spinner } from './core'
20 | import { TITLE_BAR_HEIGHT } from './WindowTitleBar'
21 |
22 | const AnimatedCodeSurfer = animated(CodeSurfer)
23 |
24 | export type PreviewProps = {
25 | steps: InputStep[]
26 | springPreset?: string
27 | theme?: string
28 | playOnInit?: boolean
29 | cycle?: boolean
30 | cycleSpeed?: number
31 | immediate?: boolean
32 | showLineNumbers?: boolean
33 | onAnimationCycleEnd?: () => void
34 | /**
35 | * EXPERIMENTAL
36 | *
37 | * Disables the fixed height and width and fills the containing element.
38 | */
39 | responsive?: boolean
40 | }
41 |
42 | const PreviewContainer = styled(Box)`
43 | /* .cs-content {
44 | display: flex;
45 | align-items: center;
46 | } */
47 |
48 | /* .cs-scaled-content {
49 | transform-origin: center !important;
50 | } */
51 | `
52 |
53 | export const Preview = forwardRef(
54 | (
55 | {
56 | steps,
57 | springPreset = DEFAULT_ANIMATION_PRESET,
58 | theme = DEFAULT_PREVIEW_THEME,
59 | showLineNumbers = DEFAULT_SHOW_NUMBERS,
60 | cycleSpeed = DEFAULT_CYCLE_SPEED,
61 | playOnInit = false,
62 | immediate,
63 | cycle,
64 | onAnimationCycleEnd = noop,
65 | responsive = true,
66 | },
67 | ref,
68 | ) => {
69 | const dispatch = usePreviewDispatch()
70 | const { currentStep, isPlaying, pauseAnimation } = usePreviewState()
71 | const [codeSurferState, setCodeSurferState] = useState<{
72 | key: number
73 | steps: InputStep[]
74 | }>({
75 | key: 1,
76 | steps,
77 | })
78 | const [loading, setLoading] = useState(false)
79 |
80 | const remountPreview = () => {
81 | setLoading(false)
82 | setCodeSurferState((previousState) => ({
83 | key: previousState.key + 1,
84 | steps,
85 | }))
86 | }
87 | const totalSteps = codeSurferState.steps.length - 1
88 | const chosenTheme = CODE_THEMES_DICT[theme]
89 | const [debouncedCallback] = useDebouncedCallback(remountPreview, 1500)
90 |
91 | useEffect(() => {
92 | setLoading(true)
93 | debouncedCallback()
94 | // Show line numbers is global to a snippet right now and why it's passed in
95 | // eslint-disable-next-line react-hooks/exhaustive-deps
96 | }, [steps, showLineNumbers])
97 |
98 | // How many more can we go?!
99 | const determineStepToGoTo = () =>
100 | totalSteps >= 1
101 | ? currentStep === totalSteps
102 | ? cycle || (!playOnInit && isPlaying)
103 | ? 0
104 | : currentStep
105 | : currentStep + 1
106 | : currentStep
107 |
108 | useEffect(() => {
109 | if (isPlaying) {
110 | dispatch({
111 | type: 'updatePreviewState',
112 | currentStep: determineStepToGoTo(),
113 | })
114 | }
115 | // eslint-disable-next-line react-hooks/exhaustive-deps
116 | }, [isPlaying])
117 |
118 | useEffect(() => {
119 | if (playOnInit) {
120 | dispatch({
121 | type: 'updatePreviewState',
122 | isPlaying: true,
123 | })
124 | }
125 | // eslint-disable-next-line react-hooks/exhaustive-deps
126 | }, [playOnInit])
127 |
128 | const props = useSpring({
129 | progress: currentStep,
130 | config: ANIMATION_PRESETS_DICT[springPreset].config,
131 | delay: Number(cycleSpeed),
132 | onRest: () => {
133 | if (isPlaying) {
134 | dispatch({
135 | type: 'updatePreviewState',
136 | currentStep: determineStepToGoTo(),
137 | })
138 | }
139 |
140 | if (currentStep === totalSteps) {
141 | onAnimationCycleEnd()
142 | }
143 | },
144 | immediate,
145 | pause: pauseAnimation,
146 | })
147 |
148 | return (
149 |
158 | {codeSurferState.key !== 1 && loading && (
159 |
166 | )}
167 |
168 | {/* Reinitialize theme-ui around code surfer so that all the built in themes work
169 | // @ts-ignore */}
170 |
171 | ({
179 | ...s,
180 | showNumbers: showLineNumbers,
181 | }))}
182 | theme={CODE_THEMES_DICT[theme].theme}
183 | />
184 |
185 |
186 | )
187 | },
188 | )
189 |
--------------------------------------------------------------------------------
/packages/client/src/components/Autocomplete.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled'
2 | import React, { useEffect, useState } from 'react'
3 | import Autosuggest from 'react-autosuggest'
4 |
5 | import { useSearch } from '../hooks'
6 | import { isNil, noop } from '../utils'
7 | import {
8 | Box,
9 | Icon,
10 | Input,
11 | InputGroup,
12 | InputLeftElement,
13 | useColorMode,
14 | } from './core'
15 |
16 | type GenericAutocompleteOption = { name: string; aliases?: string[] }
17 |
18 | type AutocompleteProps