├── .env.example ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── components ├── button-effect.js ├── code-highlight.js ├── empty-state.js ├── github-icon.js ├── journey-card.js ├── loading-dots.js ├── ne_110m_admin_0_countries.geojson.json.json ├── play-pause-icon.js └── three-globe.js ├── const └── index.js ├── hooks └── use-prefers-reduced-motion.js ├── neon ├── index.js └── schema.sql ├── next.config.js ├── package.json ├── pages ├── _app.js ├── _document.js ├── analytics.js ├── api │ ├── create-edge.js │ ├── read.js │ └── update-entry.js └── index.js ├── postcss.config.js ├── static ├── cta-elephant.avif ├── favicon.png ├── logo.svg └── open-graph-image.jpg ├── styles └── globals.css ├── tailwind.config.js ├── utils ├── code-block-theme.js ├── format-date.js ├── format-number.js ├── get-edge-distance.js ├── hex-to-rgb.js ├── send-gtag-event.js └── theme.js ├── vercel.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL= 2 | # For local develoment both of the below need to be defined in your .env files, but they won't need values 3 | NEXT_PUBLIC_REWRITE_PREFIX= 4 | NEXT_PUBLIC_REWRITE_URL= 5 | 6 | # This can safely be ignored if you're not using Google Analytics Tag Manager 7 | NEXT_PUBLIC_GTM= 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.0.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Paul Scanlon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Ping Thing 4 | 5 |

6 | 7 | # Ping Thing 8 | 9 | Ping a Neon Serverless Postgres database using a Vercel Edge Function to see the journey your request makes. 10 | 11 | 🚀 Live Preview: [https://neon.tech/demos/ping-thing](https://neon.tech/demos/ping-thing) 12 | 13 | Read more about how this app was made on the Neon blog: [How to use Postgres at the Edge](https://neon.tech/blog/how-to-use-postgres-at-the-edge) 14 | 15 | ## Getting Started 16 | 17 | - Sign up to [Neon](https://neon.tech/). 18 | - Follow our [Create your first project](https://neon.tech/docs/get-started-with-neon/setting-up-a-project) guide. 19 | 20 | ## Local Development 21 | 22 | Install Dependencies. 23 | 24 | ``` 25 | yarn 26 | ``` 27 | 28 | Rename `.env.example` to `.env` and add your Neon database connection string. 29 | 30 | ``` 31 | DATABASE_URL= 32 | ``` 33 | 34 | SQL schema 35 | 36 | ``` 37 | CREATE TABLE locations ( 38 | id SERIAL PRIMARY KEY, 39 | date TIMESTAMP WITH TIME ZONE NOT NULL, 40 | flag VARCHAR, 41 | country VARCHAR, 42 | city VARCHAR, 43 | lat DECIMAL, 44 | lng DECIMAL, 45 | runtime VARCHAR, 46 | start_time NUMERIC, 47 | end_time NUMERIC, 48 | miles DECIMAL (10,2), 49 | kilometers DECIMAL (10,2) 50 | ); 51 | ``` 52 | 53 | Start development server. 54 | 55 | ``` 56 | yarn dev 57 | ``` 58 | -------------------------------------------------------------------------------- /components/button-effect.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const ButtonEffect = ({ children, disabled }) => { 5 | return ( 6 |
7 | {children} 8 | {disabled ? null : ( 9 |
10 | 14 | 18 |
19 | )} 20 |
21 | ); 22 | }; 23 | 24 | ButtonEffect.propTypes = { 25 | /** React children */ 26 | children: PropTypes.element.isRequired, 27 | /** Determins if lines should be shown */ 28 | disabled: PropTypes.bool, 29 | }; 30 | 31 | export default ButtonEffect; 32 | -------------------------------------------------------------------------------- /components/code-highlight.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { CodeBlock } from 'react-code-blocks'; 3 | import customTheme from '../utils/code-block-theme'; 4 | 5 | const CodeHighlight = ({ text }) => { 6 | return ( 7 |
8 |
 9 |         
15 |       
16 |
17 | ); 18 | }; 19 | 20 | CodeHighlight.propTypes = { 21 | /** the code snippet to display */ 22 | text: PropTypes.string.isRequired, 23 | }; 24 | 25 | export default CodeHighlight; 26 | -------------------------------------------------------------------------------- /components/empty-state.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const EmptyState = ({ animation = 'animate-dark-fade' }) => { 5 | return ( 6 |
7 |
8 |
9 | ); 10 | }; 11 | 12 | EmptyState.propTypes = { 13 | /** The type of the animation phase */ 14 | animation: PropTypes.string, 15 | }; 16 | 17 | export default EmptyState; 18 | -------------------------------------------------------------------------------- /components/github-icon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const GitHubIcon = ({ className = 'h-8 w-8' }) => { 5 | return ( 6 | 12 | 17 | 18 | ); 19 | }; 20 | 21 | GitHubIcon.propTypes = { 22 | /** CSS classes */ 23 | className: PropTypes.string, 24 | }; 25 | 26 | export default GitHubIcon; 27 | -------------------------------------------------------------------------------- /components/journey-card.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { formatNumber } from '../utils/format-number'; 5 | import EmptyState from '../components/empty-state'; 6 | 7 | const JourneyCard = ({ journey }) => { 8 | const { 9 | flag, 10 | country, 11 | city, 12 | diff, 13 | miles, 14 | kilometers, 15 | color, 16 | stroke, 17 | stages, 18 | } = journey; 19 | 20 | return ( 21 |
22 |
23 | Your Request 24 |

25 | Details of your request made to Neon using an{' '} 26 | 32 | Edge Function 33 | 34 | . 35 |

36 |
37 |
42 |
43 |
44 | 56 | 61 | 62 | {stages.map((details, index) => { 63 | const { color, stroke } = details; 64 | return ( 65 | 66 |
67 |
68 |
69 | 80 | 85 | 86 | 87 | ); 88 | })} 89 |
90 |
91 |
92 |
93 | Start 94 | {city || } 95 |
96 |
97 | Country 98 | {country ? ( 99 |
100 | 105 | {flag} 106 | 107 | {country} 108 |
109 | ) : ( 110 | 111 | )} 112 |
113 |
114 | {stages.map((details, index) => { 115 | const { stop, type, region, flag } = details; 116 | return ( 117 |
118 |
119 | {type} 120 | {stop} 121 |
122 |
123 | Region 124 |
125 | 130 | {flag} 131 | 132 | {region} 133 |
134 |
135 |
136 | ); 137 | })} 138 |
139 |
140 |
141 |
142 | Mph 143 | 144 | {diff ? ( 145 | formatNumber((miles * 2) / (diff / 1000)) 146 | ) : ( 147 | 148 | )} 149 | 150 |
151 |
152 | Sec 153 | 154 | {diff ? (diff / 1000).toFixed(2) : } 155 | 156 |
157 |
158 | Mi 159 | 160 | {miles ? formatNumber(miles * 2) : } 161 | 162 |
163 | 164 |
165 | Km 166 | 167 | {kilometers ? formatNumber(kilometers * 2) : } 168 | 169 |
170 |
171 |
172 |
173 | ); 174 | }; 175 | 176 | JourneyCard.proptypes = { 177 | /** The journey details */ 178 | journey: PropTypes.shape({ 179 | /** The runtime of the runtime */ 180 | runtime: PropTypes.string, 181 | /** The flag of the country */ 182 | flag: PropTypes.string.isRquired, 183 | /** The country of the request */ 184 | country: PropTypes.string.isRquired, 185 | /** The runtime of the city */ 186 | city: PropTypes.string.isRquired, 187 | /** The start_time - end_time */ 188 | diff: PropTypes.number.isRquired, 189 | /** The distance in miles */ 190 | miles: PropTypes.number.isRquired, 191 | /** The distance in kilometers */ 192 | kilometers: PropTypes.number.isRquired, 193 | /** The colour of the current location */ 194 | color: PropTypes.string.isRquired, 195 | /** The details of the stops */ 196 | stops: PropTypes.shape({ 197 | /** The runtime of the stop*/ 198 | runtime: PropTypes.string.isRquired, 199 | /** The flag emoji */ 200 | flag: PropTypes.string.isRquired, 201 | /** The runtime of the region */ 202 | region: PropTypes.string.isRquired, 203 | /** The color of the stop */ 204 | color: PropTypes.string.isRquired, 205 | }), 206 | }).isRquired, 207 | }; 208 | 209 | export default JourneyCard; 210 | -------------------------------------------------------------------------------- /components/loading-dots.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const LoadingDots = ({ className }) => { 5 | const dots = new Array(3).fill(''); 6 | 7 | return ( 8 |
9 | {dots.map((_, index) => { 10 | return ( 11 |
18 | ); 19 | })} 20 |
21 | ); 22 | }; 23 | 24 | LoadingDots.propTypes = { 25 | /** CSS classes */ 26 | className: PropTypes.string, 27 | }; 28 | 29 | export default LoadingDots; 30 | -------------------------------------------------------------------------------- /components/play-pause-icon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const PlayPauseIcon = ({ isPlaying, className = 'h-12 w-12' }) => { 5 | return ( 6 | 12 | 21 | 22 | ); 23 | }; 24 | 25 | PlayPauseIcon.propTypes = { 26 | /** The animation status of the globe */ 27 | isPlaying: PropTypes.bool.isRequired, 28 | /** CSS classes */ 29 | className: PropTypes.string, 30 | }; 31 | 32 | export default PlayPauseIcon; 33 | -------------------------------------------------------------------------------- /components/three-globe.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, memo } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Globe from 'react-globe.gl'; 5 | import * as THREE from 'three'; 6 | 7 | import { theme } from '../utils/theme'; 8 | import usePrefersReducedMotion from '../hooks/use-prefers-reduced-motion'; 9 | 10 | import goeJson from './ne_110m_admin_0_countries.geojson.json'; 11 | 12 | const ThreeGlobe = memo(({ isPlaying, data }) => { 13 | const globeEl = useRef(); 14 | const prefersReducedMotion = usePrefersReducedMotion(); 15 | 16 | const globeReady = () => { 17 | if (globeEl.current) { 18 | globeEl.current.controls().autoRotate = isPlaying; 19 | globeEl.current.controls().enableZoom = false; 20 | 21 | globeEl.current.pointOfView({ 22 | lat: 19.054339351561637, 23 | lng: -50.421161072148465, 24 | altitude: 2, 25 | }); 26 | } 27 | }; 28 | 29 | useEffect(() => { 30 | if (globeEl.current) { 31 | globeEl.current.controls().autoRotate = isPlaying; 32 | } 33 | }, [isPlaying]); 34 | 35 | useEffect(() => { 36 | if (globeEl.current) { 37 | globeEl.current.controls().autoRotate = !prefersReducedMotion; 38 | } 39 | }, [prefersReducedMotion]); 40 | 41 | return ( 42 |
43 | { 65 | return ['#313131', '#1e1e1e', '#4d4d4d', '#373737'][ 66 | geometry.properties.abbrev_len % 4 67 | ]; 68 | }} 69 | customLayerData={[...Array(500).keys()].map(() => ({ 70 | lat: (Math.random() - 0.5) * 180, 71 | lng: (Math.random() - 0.5) * 360, 72 | alt: Math.random() * 1.4 + 0.1, 73 | }))} 74 | customThreeObject={() => 75 | new THREE.Mesh( 76 | new THREE.SphereGeometry(0.3), 77 | new THREE.MeshBasicMaterial({ 78 | color: '#797979', 79 | opacity: 0.6, 80 | transparent: true, 81 | }) 82 | ) 83 | } 84 | customThreeObjectUpdate={(obj, d) => { 85 | Object.assign( 86 | obj.position, 87 | globeEl.current?.getCoords(d.lat, d.lng, d.alt) 88 | ); 89 | }} 90 | pointColor='color' 91 | pointAltitude='altitude' 92 | pointRadius='radius' 93 | // pointsMerge={true} // this breaks the html markers when they re-render: https://github.com/vasturiano/react-globe.gl/issues/140 94 | arcsData={data.arcs} 95 | arcColor='color' 96 | arcStroke='stroke' 97 | arcDashGap={0.05} 98 | arcDashLength='dash' 99 | arcAltitudeAutoScale={0.3} 100 | arcDashAnimateTime={2000} 101 | ringsData={data.rings} 102 | ringColor={(ring) => (t) => { 103 | return `rgba(${ring.color}, ${Math.sqrt(1 - t)})`; 104 | }} 105 | ringMaxRadius='maxR' 106 | ringPropagationSpeed='propagationSpeed' 107 | ringRepeatPeriod='repeatPeriod' 108 | ringResolution={64} 109 | htmlElementsData={data.points} 110 | htmlAltitude={0.1} 111 | htmlElement={(d) => { 112 | const el = document.createElement('div'); 113 | el.innerHTML = ` 114 |
115 |
116 |
119 |
120 | ${d.stop} 121 | ${d.region || d.city} 122 |
123 |
124 |
125 | `; 126 | return el; 127 | }} 128 | /> 129 |
130 | ); 131 | }); 132 | 133 | ThreeGlobe.propTypes = { 134 | /** Status of animation */ 135 | isPlaying: PropTypes.bool.isRequired, 136 | /** The locations of points and arcs */ 137 | data: PropTypes.any.isRequired, 138 | }; 139 | 140 | export default ThreeGlobe; 141 | -------------------------------------------------------------------------------- /const/index.js: -------------------------------------------------------------------------------- 1 | import { theme } from '../utils/theme'; 2 | import { hexToRgb } from '../utils/hex-to-rgb'; 3 | 4 | export const POINT_RADIUS_LG = 1; 5 | export const POINT_RADIUS_SM = 0.5; 6 | export const POINT_ALTITUDE = 0.02; 7 | 8 | const RING_MAX_RADIUS = 20; 9 | const RING_PROPERGATION_SPEED = 4; 10 | const RING_REPEAT_PERIOD = 800; 11 | 12 | export const EDGE = 'edge'; 13 | export const NEON = 'neon'; 14 | 15 | export const userPoint = { 16 | stop: 'You', 17 | stroke: theme.colors.brand.secondary, 18 | color: theme.colors.brand.secondary, 19 | }; 20 | 21 | export const neonPoint = { 22 | stop: 'Neon Database', 23 | type: 'end', 24 | lat: 38.95329973388636, 25 | lng: -77.45615256400193, 26 | flag: '🇺🇸', 27 | region: 'aws-us-east-1', 28 | stroke: theme.colors.brand.primary, 29 | color: theme.colors.brand.primary, 30 | altitude: POINT_ALTITUDE, 31 | radius: POINT_RADIUS_LG, 32 | size: 200, 33 | }; 34 | 35 | export const proxyPoint = { 36 | stop: 'Neon Proxy', 37 | type: 'through', 38 | lat: 38.95329973388636, 39 | lng: -77.45615256400193, 40 | flag: '🇺🇸', 41 | region: 'aws-us-east-1', 42 | stroke: theme.colors.brand['primary-light'], 43 | color: theme.colors.brand.background, 44 | altitude: POINT_ALTITUDE, 45 | radius: POINT_RADIUS_LG, 46 | size: 200, 47 | }; 48 | 49 | export const neonRing = { 50 | lat: neonPoint.lat, 51 | lng: neonPoint.lng, 52 | color: hexToRgb(neonPoint.color), 53 | maxR: RING_MAX_RADIUS, 54 | propagationSpeed: RING_PROPERGATION_SPEED, 55 | repeatPeriod: RING_REPEAT_PERIOD, 56 | }; 57 | 58 | export const serverlessDriverString = `import { neon } from '@neondatabase/serverless'; 59 | 60 | export default async function handler(req, res) { 61 | const sql = neon(process.env.DATABASE_URL); 62 | const response = await sql\`SELECT * FROM table_name\`; 63 | 64 | return Response.json({ 65 | message: 'A Ok!', 66 | data: response 67 | }); 68 | } 69 | 70 | export const config = { 71 | runtime: 'edge', 72 | };`; 73 | -------------------------------------------------------------------------------- /hooks/use-prefers-reduced-motion.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const usePrefersReducedMotion = () => { 4 | const [prefersReducedMotion, setPrefersReducedMotion] = useState(true); 5 | 6 | useEffect(() => { 7 | const query = window.matchMedia('(prefers-reduced-motion: no-preference)'); 8 | 9 | setPrefersReducedMotion(!query.matches); 10 | 11 | const setState = (event) => { 12 | setPrefersReducedMotion(!event.matches); 13 | }; 14 | 15 | query.addEventListener('change', setState); 16 | 17 | return () => { 18 | query.removeEventListener('change', setState); 19 | }; 20 | }, []); 21 | 22 | return prefersReducedMotion; 23 | }; 24 | 25 | export default usePrefersReducedMotion; 26 | -------------------------------------------------------------------------------- /neon/index.js: -------------------------------------------------------------------------------- 1 | const { neon } = require('@neondatabase/serverless'); 2 | 3 | module.exports = { 4 | sql: neon(process.env.DATABASE_URL), 5 | }; 6 | -------------------------------------------------------------------------------- /neon/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE locations ( 2 | id SERIAL PRIMARY KEY, 3 | date TIMESTAMP WITH TIME ZONE NOT NULL, 4 | flag VARCHAR, 5 | country VARCHAR, 6 | city VARCHAR, 7 | lat DECIMAL, 8 | lng DECIMAL, 9 | runtime VARCHAR, 10 | start_time NUMERIC, 11 | end_time NUMERIC, 12 | miles DECIMAL (10,2), 13 | kilometers DECIMAL (10,2) 14 | ); 15 | 16 | 17 | TRUNCATE TABLE locations; -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const isProd = process.env.NODE_ENV === 'production'; 2 | 3 | module.exports = { 4 | assetPrefix: isProd ? process.env.NEXT_PUBLIC_REWRITE_PREFIX : undefined, 5 | basePath: isProd ? process.env.NEXT_PUBLIC_REWRITE_PREFIX : undefined, 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ping-thing", 3 | "version": "1.0.0", 4 | "description": "Neon Demo", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/PaulieScanlon/ping-thing.git" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/PaulieScanlon/ping-thing/issues" 22 | }, 23 | "homepage": "https://github.com/PaulieScanlon/ping-thing#readme", 24 | "dependencies": { 25 | "@neondatabase/serverless": "^0.5.6", 26 | "@vercel/edge": "^1.1.0", 27 | "geolib": "^3.3.4", 28 | "next": "^13.4.12", 29 | "react": "^18.2.0", 30 | "react-code-blocks": "0.0.9-0", 31 | "react-dom": "^18.2.0", 32 | "react-globe.gl": "^2.24.3", 33 | "react-query": "^3.39.3" 34 | }, 35 | "devDependencies": { 36 | "@tailwindcss/typography": "^0.5.9", 37 | "autoprefixer": "^10.4.14", 38 | "postcss": "^8.4.27", 39 | "prop-types": "^15.8.1", 40 | "tailwindcss": "^3.3.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import Head from 'next/head'; 3 | import Image from 'next/image'; 4 | 5 | import { QueryClient, QueryClientProvider } from 'react-query'; 6 | import { IBM_Plex_Sans } from 'next/font/google'; 7 | 8 | import logo from '../static/logo.svg'; 9 | import GitHubIcon from '../components/github-icon'; 10 | import { sendGtagEvent } from '../utils/send-gtag-event'; 11 | 12 | import '../styles/globals.css'; 13 | 14 | const ibmPlexSans = IBM_Plex_Sans({ 15 | subsets: ['latin'], 16 | weight: ['100', '200', '300', '400', '500', '600', '700'], 17 | display: 'swap', 18 | variable: '--font-ibm-plex-sans', 19 | }); 20 | 21 | const queryClient = new QueryClient(); 22 | 23 | const App = ({ Component, pageProps }) => { 24 | const url = process.env.NEXT_PUBLIC_REWRITE_URL || ''; 25 | const title = 'Ping Thing'; 26 | const description = 'A @neondatabase/serverless demo'; 27 | const image = 'open-graph-image.jpg'; 28 | 29 | return ( 30 | 31 | 32 | {title} 33 | 34 | 35 | {/* Primary Meta Tags */} 36 | 37 | 38 | 39 | 40 | {/* Open Graph / Facebook */} 41 | 42 | 43 | 44 | 45 | 46 | 47 | {/* Twitter */} 48 | 49 | 50 | 51 | 52 | 53 | 54 | {/* favicon */} 55 | 56 | 57 | 58 | 59 |
60 | 87 |
88 |
89 | 90 |
91 |
92 |
93 | ); 94 | }; 95 | 96 | export default App; 97 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Script from 'next/script'; 3 | import { Html, Head, Main, NextScript } from 'next/document'; 4 | 5 | const Document = () => { 6 | const isProd = process.env.NODE_ENV === 'production'; 7 | 8 | return ( 9 | 10 | 11 | {isProd ? ( 12 | 26 | ) : null} 27 | 28 | 29 |
30 | 31 | {isProd ? ( 32 |