Merge branch 'bumi-graphql-functions'

This commit is contained in:
=Mtg_Dev
2021-12-10 21:46:26 +02:00
62 changed files with 54152 additions and 1569 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
.netlify
.env
build
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies

View File

@@ -2,6 +2,30 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Database
Set the `DATABASE_URL` environment variable for your PostgreSQL DB. (e.g. `postgres://bumi@127.0.0.1:5432/bolt_fun_dev`)
### `prisma studio`
prisma studio runs an UI for the DB
### `prisma migrate dev`
Create a migration from the schema.prisma file
### `prisma migrate deploy`
Apply pending migrations to the database
## GraphQL
GraphQL endpoint is available as netlify function on: `.netlify/functions/graphql`
Use the Apollo GraphQL Studio to to inspect the GraphQL API: [https://studio.apollographql.com/sandbox/explorer](https://studio.apollographql.com/sandbox/explorer)
## Available Scripts
In the project directory, you can run:

View File

@@ -1,26 +0,0 @@
{
"files": {
"main.css": "./static/css/main.72d30049.chunk.css",
"main.js": "./static/js/main.2388372a.chunk.js",
"main.js.map": "./static/js/main.2388372a.chunk.js.map",
"runtime-main.js": "./static/js/runtime-main.ce5efd86.js",
"runtime-main.js.map": "./static/js/runtime-main.ce5efd86.js.map",
"static/css/2.4cfadce7.chunk.css": "./static/css/2.4cfadce7.chunk.css",
"static/js/2.416fbd02.chunk.js": "./static/js/2.416fbd02.chunk.js",
"static/js/2.416fbd02.chunk.js.map": "./static/js/2.416fbd02.chunk.js.map",
"static/js/3.f6cea3fe.chunk.js": "./static/js/3.f6cea3fe.chunk.js",
"static/js/3.f6cea3fe.chunk.js.map": "./static/js/3.f6cea3fe.chunk.js.map",
"index.html": "./index.html",
"static/css/2.4cfadce7.chunk.css.map": "./static/css/2.4cfadce7.chunk.css.map",
"static/css/main.72d30049.chunk.css.map": "./static/css/main.72d30049.chunk.css.map",
"static/js/2.416fbd02.chunk.js.LICENSE.txt": "./static/js/2.416fbd02.chunk.js.LICENSE.txt",
"static/media/styles.css": "./static/media/revicons.e8746a62.woff"
},
"entrypoints": [
"static/js/runtime-main.ce5efd86.js",
"static/css/2.4cfadce7.chunk.css",
"static/js/2.416fbd02.chunk.js",
"static/css/main.72d30049.chunk.css",
"static/js/main.2388372a.chunk.js"
]
}

View File

@@ -1,5 +0,0 @@
<svg width="93" height="92" viewBox="0 0 93 92" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6.5" y="6" width="80" height="80" rx="37.625" fill="#E1DBFF"/>
<path d="M39 49C39 49 40 48 43 48C46 48 48 50 51 50C54 50 55 49 55 49V37C55 37 54 38 51 38C48 38 46 36 43 36C40 36 39 37 39 37V49ZM39 49V56" stroke="#7B61FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="6.5" y="6" width="80" height="80" rx="37.625" stroke="#EDEAFF" stroke-width="12"/>
</svg>

Before

Width:  |  Height:  |  Size: 491 B

View File

@@ -1,5 +0,0 @@
<svg width="93" height="92" viewBox="0 0 93 92" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6.5" y="6" width="80" height="80" rx="37.625" fill="#F7D154"/>
<path d="M47.5 36L37.5 48H46.5L45.5 56L55.5 44H46.5L47.5 36Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="6.5" y="6" width="80" height="80" rx="37.625" stroke="#FFF1C4" stroke-width="12"/>
</svg>

Before

Width:  |  Height:  |  Size: 410 B

View File

@@ -1,5 +0,0 @@
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="4" width="48" height="48" rx="24" fill="#F7D154"/>
<path d="M29 18L19 30H28L27 38L37 26H28L29 18Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="4" y="4" width="48" height="48" rx="24" stroke="#FFF1C3" stroke-width="8"/>
</svg>

Before

Width:  |  Height:  |  Size: 383 B

View File

@@ -1,5 +0,0 @@
<svg width="91" height="92" viewBox="0 0 91 92" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="5.5" y="6" width="80" height="80" rx="37.625" fill="#D1FADF"/>
<path d="M58.5625 44.8887V46.125C58.5609 49.0227 57.6225 51.8422 55.8875 54.163C54.1525 56.4839 51.7138 58.1817 48.935 59.0033C46.1562 59.8249 43.1863 59.7263 40.4682 58.7221C37.7501 57.7179 35.4294 55.8619 33.8522 53.431C32.2751 51.0001 31.526 48.1246 31.7166 45.2331C31.9073 42.3417 33.0275 39.5894 34.9102 37.3867C36.7929 35.1839 39.3372 33.6488 42.1636 33.0102C44.9901 32.3716 47.9472 32.6637 50.5941 33.8431M58.5625 35.375L45.125 48.8259L41.0938 44.7947" stroke="#039855" stroke-width="2.6875" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="5.5" y="6" width="80" height="80" rx="37.625" stroke="#ECFDF3" stroke-width="10.75"/>
</svg>

Before

Width:  |  Height:  |  Size: 818 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1 +0,0 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="A place for creators to share theier lightning apps"/><link rel="manifest" href="./manifest.json"/><title>makers.bolt.fun</title><link href="./static/css/2.4cfadce7.chunk.css" rel="stylesheet"><link href="./static/css/main.72d30049.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,a,i=r[0],c=r[1],l=r[2],s=0,p=[];s<i.length;s++)a=i[s],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&p.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);p.length;)p.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var c=t[i];0!==o[c]&&(n=!1)}n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={1:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+"static/js/"+({}[e]||e)+"."+{3:"f6cea3fe"}[e]+".chunk.js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,function(r){return e[r]}.bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="./",a.oe=function(e){throw console.error(e),e};var i=this["webpackJsonpmakers.bolt.fun"]=this["webpackJsonpmakers.bolt.fun"]||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var l=0;l<i.length;l++)r(i[l]);var f=c;t()}([])</script><script src="./static/js/2.416fbd02.chunk.js"></script><script src="./static/js/main.2388372a.chunk.js"></script></body></html>

View File

@@ -1,25 +0,0 @@
{
"short_name": "makers.bolt.fun",
"name": "Makers.Bolt.Fun",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,96 +0,0 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/**
* React Router DOM v6.0.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router v6.0.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/** @license React v0.20.2
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
(this["webpackJsonpmakers.bolt.fun"]=this["webpackJsonpmakers.bolt.fun"]||[]).push([[3],{201:function(t,e,n){"use strict";n.r(e),n.d(e,"getCLS",(function(){return d})),n.d(e,"getFCP",(function(){return S})),n.d(e,"getFID",(function(){return k})),n.d(e,"getLCP",(function(){return F})),n.d(e,"getTTFB",(function(){return C}));var i,a,r,o,u=function(t,e){return{name:t,value:void 0===e?-1:e,delta:0,entries:[],id:"v1-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(t,e){try{if(PerformanceObserver.supportedEntryTypes.includes(t)){if("first-input"===t&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(t){return t.getEntries().map(e)}));return n.observe({type:t,buffered:!0}),n}}catch(t){}},f=function(t,e){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(t(i),e&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(t){addEventListener("pageshow",(function(e){e.persisted&&t(e)}),!0)},m="function"==typeof WeakSet?new WeakSet:new Set,p=function(t,e,n){var i;return function(){e.value>=0&&(n||m.has(e)||"hidden"===document.visibilityState)&&(e.delta=e.value-(i||0),(e.delta||void 0===i)&&(i=e.value,t(e)))}},d=function(t,e){var n,i=u("CLS",0),a=function(t){t.hadRecentInput||(i.value+=t.value,i.entries.push(t),n())},r=c("layout-shift",a);r&&(n=p(t,i,e),f((function(){r.takeRecords().map(a),n()})),s((function(){i=u("CLS",0),n=p(t,i,e)})))},v=-1,l=function(){return"hidden"===document.visibilityState?0:1/0},h=function(){f((function(t){var e=t.timeStamp;v=e}),!0)},g=function(){return v<0&&(v=l(),h(),s((function(){setTimeout((function(){v=l(),h()}),0)}))),{get timeStamp(){return v}}},S=function(t,e){var n,i=g(),a=u("FCP"),r=function(t){"first-contentful-paint"===t.name&&(f&&f.disconnect(),t.startTime<i.timeStamp&&(a.value=t.startTime,a.entries.push(t),m.add(a),n()))},o=performance.getEntriesByName("first-contentful-paint")[0],f=o?null:c("paint",r);(o||f)&&(n=p(t,a,e),o&&r(o),s((function(i){a=u("FCP"),n=p(t,a,e),requestAnimationFrame((function(){requestAnimationFrame((function(){a.value=performance.now()-i.timeStamp,m.add(a),n()}))}))})))},y={passive:!0,capture:!0},E=new Date,w=function(t,e){i||(i=e,a=t,r=new Date,b(removeEventListener),L())},L=function(){if(a>=0&&a<r-E){var t={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+a};o.forEach((function(e){e(t)})),o=[]}},T=function(t){if(t.cancelable){var e=(t.timeStamp>1e12?new Date:performance.now())-t.timeStamp;"pointerdown"==t.type?function(t,e){var n=function(){w(t,e),a()},i=function(){a()},a=function(){removeEventListener("pointerup",n,y),removeEventListener("pointercancel",i,y)};addEventListener("pointerup",n,y),addEventListener("pointercancel",i,y)}(e,t):w(e,t)}},b=function(t){["mousedown","keydown","touchstart","pointerdown"].forEach((function(e){return t(e,T,y)}))},k=function(t,e){var n,r=g(),d=u("FID"),v=function(t){t.startTime<r.timeStamp&&(d.value=t.processingStart-t.startTime,d.entries.push(t),m.add(d),n())},l=c("first-input",v);n=p(t,d,e),l&&f((function(){l.takeRecords().map(v),l.disconnect()}),!0),l&&s((function(){var r;d=u("FID"),n=p(t,d,e),o=[],a=-1,i=null,b(addEventListener),r=v,o.push(r),L()}))},F=function(t,e){var n,i=g(),a=u("LCP"),r=function(t){var e=t.startTime;e<i.timeStamp&&(a.value=e,a.entries.push(t)),n()},o=c("largest-contentful-paint",r);if(o){n=p(t,a,e);var d=function(){m.has(a)||(o.takeRecords().map(r),o.disconnect(),m.add(a),n())};["keydown","click"].forEach((function(t){addEventListener(t,d,{once:!0,capture:!0})})),f(d,!0),s((function(i){a=u("LCP"),n=p(t,a,e),requestAnimationFrame((function(){requestAnimationFrame((function(){a.value=performance.now()-i.timeStamp,m.add(a),n()}))}))}))}},C=function(t){var e,n=u("TTFB");e=function(){try{var e=performance.getEntriesByType("navigation")[0]||function(){var t=performance.timing,e={entryType:"navigation",startTime:0};for(var n in t)"navigationStart"!==n&&"toJSON"!==n&&(e[n]=Math.max(t[n]-t.navigationStart,0));return e}();if(n.value=n.delta=e.responseStart,n.value<0)return;n.entries=[e],t(n)}catch(t){}},"complete"===document.readyState?setTimeout(e,0):addEventListener("pageshow",e)}}}]);
//# sourceMappingURL=3.f6cea3fe.chunk.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],l=r[2],s=0,p=[];s<i.length;s++)a=i[s],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&p.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);p.length;)p.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var c=t[i];0!==o[c]&&(n=!1)}n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={1:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+"static/js/"+({}[e]||e)+"."+{3:"f6cea3fe"}[e]+".chunk.js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"===typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,function(r){return e[r]}.bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="./",a.oe=function(e){throw console.error(e),e};var i=this["webpackJsonpmakers.bolt.fun"]=this["webpackJsonpmakers.bolt.fun"]||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var l=0;l<i.length;l++)r(i[l]);var f=c;t()}([]);
//# sourceMappingURL=runtime-main.ce5efd86.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,37 @@
const { ApolloServer } = require("apollo-server-lambda");
const { PrismaClient } = require("@prisma/client");
const resolvers = require("./resolvers");
const typeDefs = require("./typeDefs");
const prisma = new PrismaClient();
const server = new ApolloServer({
resolvers,
typeDefs,
context: () => {
return {
prisma,
};
},
});
const apolloHandler = server.createHandler({
cors: {
origin: "*",
credentials: true,
},
});
// https://github.com/vendia/serverless-express/issues/427#issuecomment-924580007
const handler = (event, context, ...args) => {
return apolloHandler(
{
...event,
requestContext: context,
},
context,
...args
);
};
exports.handler = handler;

View File

@@ -0,0 +1,225 @@
const { parsePaymentRequest } = require("invoices");
const axios = require("axios");
const { createHash } = require("crypto");
function hexToUint8Array(hexString) {
const match = hexString.match(/.{1,2}/g);
if (match) {
return new Uint8Array(match.map((byte) => parseInt(byte, 16)));
}
}
// TODO: generaly validate LNURL responses
// get lnurl params
function getLnurlDetails(lnurl) {
return axios.get(lnurl);
}
// parse lightning address and return a url that can be
// used in a request
function lightningAddressToLnurl(lightning_address) {
const [name, domain] = lightning_address.split("@");
return `https://${domain}/.well-known/lnurlp/${name}`;
}
// when pressing tip or selecting an amount.
// this is used for caching so the frontend doesnt
// have to make an additional http request to get
// the callback url for future visits
async function getLnurlCallbackUrl(lightning_address) {
return getLnurlDetails(lightningAddressToLnurl(lightning_address)).then(
(response) => {
return response.data.callback;
}
);
}
async function getPaymetRequestForProject(project, amount_in_sat) {
// # NOTE: CACHING LNURL CALLBACK URLS + PARAMETERS
// LNURL flows have a lot of back and forth and can impact
// the load time for your application users.
// You may consider caching the callback url, or resolved
// parameters but be mindful of this.
// The LNURL service provider can change the callback url
// details or the paramters that is returned we must be
// careful when trying to optimise the amount of
// requests so be mindful of this when you are storing
// these items.
let lnurlCallbackUrl = project.lnurl_callback_url;
const amount = amount_in_sat * 1000; // msats
if (!lnurlCallbackUrl) {
lnurlCallbackUrl = await getLnurlCallbackUrl(project.lightning_address);
}
return axios
.get(lnurlCallbackUrl, { params: { amount } })
.then((prResponse) => {
console.log(prResponse.data);
return prResponse.data.pr;
});
}
module.exports = {
Query: {
allCategories: async (_source, args, context) => {
return context.prisma.category.findMany({
orderBy: { title: "desc" },
include: {
project: {
take: 5,
orderBy: { votes_count: "desc" },
},
},
});
},
getCategory: async (_source, args, context) => {
return context.prisma.category.findUnique({
where: { id: args.id },
include: {
project: {
take: 5,
orderBy: { votes_count: "desc" },
},
},
});
},
newProjects: async (_source, args, context) => {
const take = args.take || 50;
const skip = args.skip || 0;
return context.prisma.project.findMany({
orderBy: { created_at: "desc" },
include: { category: true },
skip,
take,
});
},
allProjects: async (_source, args, context) => {
const take = args.take || 50;
const skip = args.skip || 0;
return context.prisma.project.findMany({
orderBy: { votes_count: "desc" },
include: { category: true },
skip,
take,
});
},
projectsByCategory: async (_source, args, context) => {
const take = args.take || 50;
const skip = args.skip || 0;
const categoryId = args.category_id;
return context.prisma.project.findMany({
where: { category_id: categoryId },
orderBy: { votes_count: "desc" },
include: { category: true },
skip,
take,
});
},
getProject: async (_source, args, context) => {
return context.prisma.project.findUnique({
where: {
id: args.id,
},
include: { category: true },
});
},
getLnurlDetailsForProject: async (_source, args, context) => {
const project = await context.prisma.project.findUnique({
where: {
id: args.project_id,
},
});
const lnurlDetails = await getLnurlDetails(
lightningAddressToLnurl(project.lightning_address)
);
if (
!lnurlDetails.data ||
lnurlDetails.data.status.toLowerCase() !== "ok"
) {
console.error(lnurlDetails.data);
throw new Error("Recipient not available");
}
// cache the callback URL
await context.prisma.project.update({
where: { id: project.id },
data: {
lnurl_callback_url: lnurlDetails.data.callback,
},
});
// # SENDING MESSAGES TO THE PROJECT OWNER USING LNURL-PAY COMMENTS
// comments in lnurl pay can be used to send a private message or
// post on the projcet owners site. could even be used for advertising
// or tip messages. or can even be a pay to respond / paid advise
return {
minSendable: parseInt(lnurlDetails.data.minSendable) / 1000,
maxSendable: parseInt(lnurlDetails.data.maxSendable) / 1000,
metadata: lnurlDetails.data.metadata,
commentAllowed: lnurlDetails.data.commentAllowed,
};
},
},
Mutation: {
// votes are like BTC Pay Server / ecommerce store "orders"
// the amount that needs to be paid is recorded, and the service
// awaits the payment. once the payment is made then a verification
// is necessary (confirmVote) since lnurl does not give a response
// for a successful payment. the payment is asyncronmous so we
// dont know when its get paid, and lnurl does not provide a webhook
// setup, or something for us to poll or subscribe to so we determine
// if a invoice is paid.
// the way that we implemented this check is that the client needs
// to provide the preimage for their vote to be counted on the site.
//
vote: async (_source, args, context) => {
const project = await context.prisma.project.findUnique({
where: { id: args.project_id },
});
const pr = await getPaymetRequestForProject(project, args.amount_in_sat);
const invoice = parsePaymentRequest({ request: pr });
return context.prisma.vote.create({
data: {
project_id: project.id,
amount_in_sat: args.amount_in_sat,
payment_request: pr,
payment_hash: invoice.id,
},
});
},
confirmVote: async (_source, args, context) => {
const paymentHash = createHash("sha256")
.update(hexToUint8Array(args.preimage))
.digest("hex");
// look for a vote for the payment request and the calculated payment hash
const vote = await context.prisma.vote.findFirst({
where: {
payment_request: args.payment_request,
payment_hash: paymentHash,
},
});
// if we find a vote it means the preimage is correct and we update the vote and mark it as paid
// can we write this nicer?
if (vote) {
const project = await context.prisma.project.findUnique({
where: { id: vote.project_id },
});
// count up votes cache
await context.prisma.project.update({
where: { id: project.id },
data: {
votes_count: (project.votes_count = vote.amount_in_sat),
},
});
// return the current vote
return context.prisma.vote.update({
where: { id: vote.id },
data: {
paid: true,
preimage: args.preimage,
},
});
} else {
throw new Error("Invalid preimage");
}
},
},
};

View File

@@ -0,0 +1,50 @@
const { gql } = require("apollo-server-lambda");
module.exports = gql`
type Project {
id: Int!
cover_image: String!
thumbnail_image: String!
title: String!
website: String!
lightning_address: String!
votes_count: Int!
category: Category!
}
type Category {
id: Int!
title: String!
project: [Project]
}
type Vote {
id: Int!
project: Project!
amount_in_sat: Int!
payment_request: String!
payment_hash: String!
paid: Boolean!
}
type LnurlDetails {
minSendable: Int
maxSendable: Int
metadata: String
commentAllowed: Int
}
type Query {
allProjects(skip: Int, take: Int): [Project]!
newProjects(skip: Int, take: Int): [Project]!
projectsByCategory(category_id: Int!, skip: Int, take: Int): [Project]!
getProject(id: Int!): Project!
allCategories: [Category]!
getCategory(id: Int!): Category!
getLnurlDetailsForProject(project_id: Int!): LnurlDetails!
}
type Mutation {
vote(project_id: Int!, amount_in_sat: Int!): Vote!
confirmVote(payment_request: String!, preimage: String!): Vote!
}
`;

3
netlify.toml Normal file
View File

@@ -0,0 +1,3 @@
[build]
functions = "functions" # netlify-lambda builds to this folder AND Netlify reads functions from here
publish = "build" # create-react-app builds to this folder, Netlify should serve all these files statically

33142
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
"homepage": ".",
"dependencies": {
"@apollo/client": "^3.5.5",
"@prisma/client": "3.5.0",
"@reduxjs/toolkit": "^1.6.2",
"@testing-library/jest-dom": "^5.15.0",
"@testing-library/react": "^11.2.7",
@@ -13,10 +14,16 @@
"@types/node": "^12.20.36",
"@types/react": "^17.0.33",
"@types/react-dom": "^17.0.10",
"apollo-server": "^3.5.0",
"apollo-server-lambda": "^3.5.0",
"axios": "^0.24.0",
"framer-motion": "^5.3.0",
"graphql": "^16.0.1",
"invoices": "^2.0.2",
"lodash.throttle": "^4.1.1",
"prisma": "3.5.0",
"react": "^17.0.2",
"react-confetti": "^6.0.1",
"react-copy-to-clipboard": "^5.0.4",
"react-dom": "^17.0.2",
"react-icons": "^4.3.1",
@@ -27,8 +34,10 @@
"react-responsive-carousel": "^3.2.22",
"react-router-dom": "^6.0.2",
"react-scripts": "4.0.3",
"react-use": "^17.3.1",
"typescript": "^4.4.4",
"web-vitals": "^1.1.2"
"web-vitals": "^1.1.2",
"webln": "^0.2.2"
},
"scripts": {
"start": "craco start",
@@ -40,7 +49,14 @@
"only-deploy": "gh-pages -d build",
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook -s public",
"codegen": "graphql-codegen --config codegen.yml"
"db:init": "prisma migrate dev",
"db:reset": "prisma migrate reset",
"db:seed": "prisma db seed",
"db:gui": "prisma studio",
"netlify:start": "netlify dev"
},
"prisma": {
"seed": "node prisma/seed.js"
},
"eslintConfig": {
"extends": [
@@ -84,6 +100,7 @@
"@types/react-copy-to-clipboard": "^5.0.2",
"autoprefixer": "^9.8.8",
"gh-pages": "^3.2.3",
"netlify-cli": "^8.0.3",
"postcss": "^7.0.39",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.17",
"@graphql-codegen/typescript": "2.4.1",

View File

@@ -0,0 +1,41 @@
-- CreateTable
CREATE TABLE "Category" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Project" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"website" TEXT NOT NULL,
"thumbnail_image" TEXT,
"cover_image" TEXT,
"category_id" INTEGER NOT NULL,
"votes_count" INTEGER NOT NULL DEFAULT 0,
"lightning_address" TEXT,
CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Vote" (
"id" SERIAL NOT NULL,
"project_id" INTEGER NOT NULL,
"amount_in_sat" INTEGER NOT NULL,
"payment_request" TEXT,
"payment_hash" TEXT,
"preimage" TEXT,
"paid" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "Vote_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Project" ADD CONSTRAINT "Project_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Vote" ADD CONSTRAINT "Vote_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "lnurl_callback_url" TEXT;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

41
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,41 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Category {
id Int @id @default(autoincrement())
title String
project Project[]
}
model Project {
id Int @id @default(autoincrement())
title String
description String
website String
thumbnail_image String?
cover_image String?
lightning_address String?
lnurl_callback_url String?
category Category @relation(fields: [category_id], references: [id])
category_id Int
votes_count Int @default(0)
vote Vote[]
created_at DateTime @default(now())
}
model Vote {
id Int @id @default(autoincrement())
project Project @relation(fields: [project_id], references: [id])
project_id Int
amount_in_sat Int
payment_request String?
payment_hash String?
preimage String?
paid Boolean @default(false)
}

29
prisma/seed.js Normal file
View File

@@ -0,0 +1,29 @@
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
async function main() {
const category = await prisma.category.create({
data: {
title: 'El Salvador',
},
});
const project = await prisma.project.create({
data: {
title: "Captain Morgan",
description: "HQ on a VULCANO lake",
website: "https://github.com/peakshift",
category_id: category.id,
lightning_address: "johns@getalby.com",
}
});
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})

View File

@@ -1,9 +1,29 @@
import { useEffect } from "react";
import Navbar from "./Components/Shared/Navbar/Navbar";
import ExplorePage from "./Components/ExplorePage/ExplorePage";
import ModalsContainer from "./Components/Shared/ModalsContainer/ModalsContainer";
import { useAppDispatch, useAppSelector } from './utils/hooks';
import { connectWallet } from './redux/features/wallet.slice';
function App() {
const { isWalletConnected, webln } = useAppSelector(state => ({
isWalletConnected: state.wallet.isConnected,
webln: state.wallet.provider,
}));
const dispatch = useAppDispatch();
useEffect(() => {
if(typeof window.webln != "undefined") {
window.webln.enable().then((res: any) => {
dispatch(connectWallet(window.webln));
console.log("called:webln.enable()", res);
}).catch((err: any) => {
console.log("error:webln.enable()", err);
});
}
}, []);
return <div id="app" className='w-screen overflow-hidden'>
<Navbar />
<ExplorePage />

View File

@@ -20,10 +20,6 @@ export default function Claim_CopySignatureCard({ onClose, direction, ...props }
}))
}, [dispatch])
const onCopy = () => {
// Copy to Clipboard
}
return (
<motion.div
custom={direction}

View File

@@ -1,10 +1,10 @@
import { motion } from 'framer-motion'
import { useAppDispatch } from '../../utils/hooks';
// import { useAppDispatch } from '../../utils/hooks';
import { ModalCard, modalCardVariants } from '../Shared/ModalsContainer/ModalsContainer'
export default function Claim_FundWithdrawCard({ onClose, direction, ...props }: ModalCard) {
const dispatch = useAppDispatch();
//const dispatch = useAppDispatch();
return (

View File

@@ -25,10 +25,10 @@ export default function Claim_GenerateSignatureCard({ onClose, direction, ...pro
// return () => clearTimeout(timeout)
}, [handleNext])
const onCopy = () => {
// Copy to Clipboard
setTimeout(handleNext, 2000)
}
//const onCopy = () => {
// // Copy to Clipboard
// setTimeout(handleNext, 2000)
//}
return (
<motion.div

View File

@@ -1,14 +1,14 @@
import { useAllCategoriesQuery } from 'src/generated/graphql'
import { useQuery } from '@apollo/client';
import { ALL_CATEGORIES_QUERY, ALL_CATEGORIES_QUERY_RES } from './query';
export default function Categories() {
const { data, loading } = useAllCategoriesQuery();
const { data, loading } = useQuery<ALL_CATEGORIES_QUERY_RES>(ALL_CATEGORIES_QUERY);
const handleClick = (categoryId: number) => {
const handleClick = (categoryId: string) => {
}
if (loading)
return null;

View File

@@ -1,6 +1,7 @@
import { gql } from "@apollo/client";
import { ProjectCategory } from "src/utils/interfaces";
export const QUERY_ALL_CATEGORIES = gql`
export const ALL_CATEGORIES_QUERY = gql`
query AllCategories {
allCategories {
id
@@ -8,3 +9,7 @@ export const QUERY_ALL_CATEGORIES = gql`
}
}
`;
export type ALL_CATEGORIES_QUERY_RES = {
allCategories: ProjectCategory[];
};

View File

@@ -4,13 +4,13 @@ import { ProjectCard } from "../../../utils/interfaces";
interface Props {
project: ProjectCard
onClick: (projectId: number) => void
onClick: (projectId: string) => void
}
export default function ProjectCardMini({ project, onClick }: Props) {
return (
<div className="bg-gray-25 select-none px-16 py-16 flex w-[296px] gap-16 border border-gray-200 rounded-10 hover:cursor-pointer hover:bg-gray-100" onClick={() => onClick(project.id)}>
<img src={project.thumbnail_image} draggable="false" className="flex-shrink-0 w-80 h-80 bg-gray-200 border-0 rounded-8"></img>
<img src={project.thumbnail_image} alt={project.title} draggable="false" className="flex-shrink-0 w-80 h-80 bg-gray-200 border-0 rounded-8"></img>
<div className="justify-around items-start min-w-0">
<p className="text-body4 w-full font-bold overflow-ellipsis overflow-hidden whitespace-nowrap">{project.title}</p>
<p className="text-body5 text-gray-600 font-light my-[5px]">{project.category.title}</p>

View File

@@ -20,14 +20,14 @@ Hottest.args = {
<MdLocalFireDepartment
className='inline-block text-fire align-bottom scale-125 ml-4 origin-bottom'
/></>,
categoryId: 2,
categoryId: '2',
projects: mockData.projectsCards
}
export const Defi = Template.bind({});
Defi.args = {
title: 'DeFi',
categoryId: 33,
categoryId: '33',
projects: mockData.projectsCards
}

View File

@@ -19,12 +19,12 @@ const calcNumItems = () => {
return items;
}
interface Props { title: string | ReactElement, categoryId: number, projects: ProjectCard[] }
interface Props { title: string | ReactElement, categoryId: string, projects: ProjectCard[] }
export default function ProjectsRow({ title, categoryId, projects }: Props) {
const dispatch = useAppDispatch()
const [carouselItmsCnt, setCarouselItmsCnt] = useState(calcNumItems);
const dispatch = useAppDispatch()
responsive.all.items = carouselItmsCnt
@@ -33,7 +33,7 @@ export default function ProjectsRow({ title, categoryId, projects }: Props) {
document.addEventListener('mousedown', () => drag.current = false);
document.addEventListener('mousemove', () => drag.current = true);
const handleClick = (projectId: number) => {
const handleClick = (projectId: string) => {
if (!drag.current)
dispatch(openModal({ modalId: ModalId.Project, propsToPass: { projectId } }))
}

View File

@@ -1,19 +1,20 @@
import ProjectsRow from "../ProjectsRow/ProjectsRow";
import { MdLocalFireDepartment } from "react-icons/md";
import { useAllCategoriesProjectsQuery } from "src/generated/graphql";
import { useQuery } from "@apollo/client";
import { ALL_CATEGORIES_PROJECTS_QUERY, ALL_CATEGORIES_PROJECTS_RES } from "./query";
export default function ProjectsSection() {
const { data, loading } = useAllCategoriesProjectsQuery()
const { data, loading } = useQuery<ALL_CATEGORIES_PROJECTS_RES>(ALL_CATEGORIES_PROJECTS_QUERY);
if (loading || !data) return null;
return (
<div className='mt-32 lg:mt-48'>
<ProjectsRow title={<>Hottest <MdLocalFireDepartment className='inline-block text-fire align-bottom scale-125 origin-bottom' /></>}
categoryId={10101}
categoryId="133123"
projects={data.newProjects} />
{data.allCategories.map(({ id, title, project, }) => {
if (project)

View File

@@ -1,6 +1,7 @@
import { gql } from "@apollo/client";
import { ProjectCard } from "src/utils/interfaces";
export const QUERY_ALL_CATEGORIES_PROJECTS = gql`
export const ALL_CATEGORIES_PROJECTS_QUERY = gql`
query AllCategoriesProjects {
allCategories {
id
@@ -24,3 +25,12 @@ export const QUERY_ALL_CATEGORIES_PROJECTS = gql`
}
}
`;
export type ALL_CATEGORIES_PROJECTS_RES = {
newProjects: ProjectCard[];
allCategories: {
id: string;
title: string;
project: ProjectCard[];
}[];
};

View File

@@ -4,7 +4,7 @@ import { useAppDispatch } from '../../utils/hooks';
import { ModalCard, modalCardVariants } from '../Shared/ModalsContainer/ModalsContainer'
import { AiFillThunderbolt } from 'react-icons/ai';
import CopyToClipboard from 'src/Components/Shared/CopyToClipboard/CopyToClipboard';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect } from 'react';
import { IoClose } from 'react-icons/io5';
export default function Login_ExternalWalletCard({ onClose, direction, ...props }: ModalCard) {

View File

@@ -16,7 +16,7 @@ export default function Login_SuccessCard({ onClose, direction, ...props }: Moda
useEffect(() => {
dispatch(connectWallet());
//dispatch(connectWallet());
const timeout = setTimeout(handleNext, 3000)
return () => clearTimeout(timeout)
}, [handleNext, dispatch])

View File

@@ -1,30 +1,55 @@
import { motion } from 'framer-motion'
import { BiArrowBack, BiWindowClose } from 'react-icons/bi'
import { BsJoystick } from 'react-icons/bs'
import { MdClose, MdLocalFireDepartment } from 'react-icons/md';
import { ModalCard, modalCardVariants } from '../Shared/ModalsContainer/ModalsContainer';
import { useQuery } from "@apollo/client";
import { useAppDispatch, useAppSelector } from '../../utils/hooks';
import { ModalId, openModal, scheduleModal } from '../../redux/features/modals.slice';
import { setProject } from '../../redux/features/project.slice';
import { connectWallet } from '../../redux/features/wallet.slice';
import Button from 'src/Components/Shared/Button/Button';
import { useGetProjectQuery } from 'src/generated/graphql';
import { requestProvider } from 'webln';
import { PROJECT_BY_ID_QUERY, PROJECT_BY_ID_RES, PROJECT_BY_ID_VARS } from './query'
export default function ProjectCard({ onClose, direction, ...props }: ModalCard) {
const { data, loading } = useGetProjectQuery({
variables: {
getProjectId: props.projectId
}
})
const { isWalletConnected } = useAppSelector(state => ({ isWalletConnected: state.wallet.isConnected }))
const dispatch = useAppDispatch();
const project = data?.getProject;
const { loading } = useQuery<PROJECT_BY_ID_RES, PROJECT_BY_ID_VARS>(
PROJECT_BY_ID_QUERY,
{
variables: { projectId: parseInt(props.projectId) },
onCompleted: data => {
dispatch(setProject(data.getProject))
},
}
);
const { isWalletConnected, webln, project } = useAppSelector(state => ({
isWalletConnected: state.wallet.isConnected,
webln: state.wallet.provider,
project: state.project.project,
}));
if (loading || !project) return <></>;
const onConnectWallet = async () => {
try {
const webln = await requestProvider();
if (webln) {
dispatch(connectWallet(webln));
alert("wallet connected!");
}
// Now you can call all of the webln.* methods
}
catch (err: any) {
// Tell the user what went wrong
alert(err.message);
}
}
const onTip = () => {

View File

@@ -1,8 +1,9 @@
import { gql } from "@apollo/client";
import { Project } from "src/utils/interfaces";
export const QUERY_PROJECT_BY_ID = gql`
query GetProject($getProjectId: Int!) {
getProject(id: $getProjectId) {
export const PROJECT_BY_ID_QUERY = gql`
query Project($projectId: Int!) {
getProject(id: $projectId) {
id
cover_image
thumbnail_image
@@ -10,9 +11,17 @@ export const QUERY_PROJECT_BY_ID = gql`
website
votes_count
category {
id
title
id
}
}
}
`;
export interface PROJECT_BY_ID_RES {
getProject: Project;
}
export interface PROJECT_BY_ID_VARS {
projectId: number;
}

View File

@@ -25,7 +25,11 @@ export default function Navbar() {
const [searchInput, setSearchInput] = useState("");
const inputRef = useRef<HTMLInputElement>(null)
const dispatch = useAppDispatch()
const { isWalletConnected } = useAppSelector(state => ({ isWalletConnected: state.wallet.isConnected }))
const { isWalletConnected, webln } = useAppSelector(state => ({
isWalletConnected: state.wallet.isConnected,
webln: state.wallet.provider,
}));
const toggleSearch = () => {
if (!searchOpen) {
@@ -78,7 +82,7 @@ export default function Navbar() {
className="flex">
<Button color='primary' size='md' className="lg:px-40">Submit App</Button>
{isWalletConnected ?
<Button className="ml-16 py-12 px-16 lg:px-20" onClick={onWithdraw}>2.2k Sats <AiFillThunderbolt className='inline-block text-thunder transform scale-125' /></Button>
<Button className="ml-16 py-12 px-16 lg:px-20">Connected <AiFillThunderbolt className='inline-block text-thunder transform scale-125' /></Button>
: <Button className="ml-16 py-12 px-16 lg:px-20" onClick={onConnectWallet}><AiFillThunderbolt className='inline-block text-thunder transform scale-125' /> Connect Wallet </Button>
}
</motion.div>

View File

@@ -3,6 +3,10 @@ import React, { useState } from 'react';
import { AiFillThunderbolt } from 'react-icons/ai'
import { IoClose } from 'react-icons/io5'
import { ModalCard, modalCardVariants } from '../Shared/ModalsContainer/ModalsContainer';
import { useAppDispatch, useAppSelector } from '../../utils/hooks';
import { gql, useQuery, useMutation } from "@apollo/client";
import useWindowSize from "react-use/lib/useWindowSize";
import Confetti from "react-confetti";
const defaultOptions = [
{ text: '10 sat', value: 10 },
@@ -10,19 +14,88 @@ const defaultOptions = [
{ text: '1k sats', value: 1000 },
]
export default function TipCard({ onClose, direction, ...props }: ModalCard) {
enum PaymentStatus {
DEFAULT,
FETCHING_PAYMENT_DETAILS,
PAID,
AWAITING_PAYMENT,
PAYMENT_CONFIRMED,
NOT_PAID,
CANCELED
}
const [selectedOption, setSelectedOption] = useState(0);
const [input, setInput] = useState<number>();
const VOTE = gql`
mutation Mutation($projectId: Int!, $amountInSat: Int!) {
vote(project_id: $projectId, amount_in_sat: $amountInSat) {
id
amount_in_sat
payment_request
payment_hash
paid
}
}
`;
const CONFIRM_VOTE = gql`
mutation Mutation($paymentRequest: String!, $preimage: String!) {
confirmVote(payment_request: $paymentRequest, preimage: $preimage) {
id
amount_in_sat
payment_request
payment_hash
paid
}
}
`;
export default function TipCard({ onClose, direction, ...props }: ModalCard) {
const { width, height } = useWindowSize()
const { isWalletConnected, webln } = useAppSelector(state => ({
isWalletConnected: state.wallet.isConnected,
webln: state.wallet.provider,
}));
const dispatch = useAppDispatch();
const [selectedOption, setSelectedOption] = useState(10);
const [voteAmount, setVoteAmount] = useState<number>(10);
const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(PaymentStatus.DEFAULT);
const [vote, { data }] = useMutation(VOTE, {
onCompleted: (votingData) => {
setPaymentStatus(PaymentStatus.AWAITING_PAYMENT);
webln.sendPayment(votingData.vote.payment_request).then((res: any) => {
console.log("waiting for payment", res);
confirmVote({variables: { paymentRequest: votingData.vote.payment_request, preimage: res.preimage }});
setPaymentStatus(PaymentStatus.PAID);
})
.catch((err: any) => {
console.log(err);
setPaymentStatus(PaymentStatus.NOT_PAID);
});
}
});
const [confirmVote, { data: confirmedVoteData }] = useMutation(CONFIRM_VOTE, {
onCompleted: (votingData) => {
setPaymentStatus(PaymentStatus.PAYMENT_CONFIRMED);
}
});
const onChangeInput = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedOption(-1);
setInput(Number(event.target.value));
setVoteAmount(Number(event.target.value));
};
const onSelectOption = (idx: number) => {
setSelectedOption(idx);
setInput(defaultOptions[idx].value);
setVoteAmount(defaultOptions[idx].value);
}
const requestPayment = () => {
setPaymentStatus(PaymentStatus.FETCHING_PAYMENT_DETAILS);
vote({variables: { "amountInSat": voteAmount, "projectId": parseInt("1") }});
}
return (
@@ -43,7 +116,7 @@ export default function TipCard({ onClose, direction, ...props }: ModalCard) {
<div className="input-wrapper">
<input
className="input-field"
value={input} onChange={onChangeInput}
value={voteAmount} onChange={onChangeInput}
type="number"
placeholder="e.g 5 sats" />
{/* <IoCopy className='input-icon' /> */}
@@ -60,10 +133,16 @@ export default function TipCard({ onClose, direction, ...props }: ModalCard) {
)}
</div>
<p className="text-body6 mt-12 text-gray-500">1 sat = 1 vote</p>
<button className="btn btn-primary w-full mt-32" onClick={onClose}>
{paymentStatus === PaymentStatus.FETCHING_PAYMENT_DETAILS && <p className="text-body6 mt-12 text-gray-500">Please wait while we the fetch payment details.</p>}
{paymentStatus === PaymentStatus.NOT_PAID && <p className="text-body6 mt-12 text-red-500">You did not confirm the payment. Please try again.</p>}
{paymentStatus === PaymentStatus.PAID && <p className="text-body6 mt-12 text-green-500">The invoice was paid! Please wait while we confirm it.</p>}
{paymentStatus === PaymentStatus.AWAITING_PAYMENT && <p className="text-body6 mt-12 text-yellow-500">Please confirm the payment in the prompt...</p>}
{paymentStatus === PaymentStatus.PAYMENT_CONFIRMED && <p className="text-body6 mt-12 text-green-500">Imagine confetti here</p>}
<button className="btn btn-primary w-full mt-32" onClick={requestPayment}>
Upvote
</button>
</div>
{paymentStatus === PaymentStatus.PAYMENT_CONFIRMED && <Confetti width={width} height={height} />}
</motion.div>
)
}

View File

@@ -1,19 +1,53 @@
import { Project, ProjectCard, ProjectCategory } from "../utils/interfaces";
import { gql, useQuery } from "@apollo/client";
import data from "./mockData.json";
// export async function getAllCategories(): Promise<ProjectCategory[]> {
// return data.categories as any;
// }
export async function getAllCategories(): Promise<ProjectCategory[]> {
// let QUERY = gql`
// query GetCategories {
// allCategories {
// id
// title
// }
// }
// `;
return data.categories;
}
// export async function getHottestProjects(): Promise<ProjectCard[]> {
// return data.projectsCards as any;
// }
export async function getHottestProjects(): Promise<ProjectCard[]> {
// let QUERY = gql`
// query {
// allProject {
// id
// cover_image
// thumbnail_image
// title
// website
// votes_count
// }
// }
// `;
return data.projectsCards;
}
// export async function getProjectsByCategory(
// categoryId: string
// ): Promise<ProjectCard[]> {
// return data.projectsCards as any;
// }
export async function getProjectsByCategory(
categoryId: string
): Promise<ProjectCard[]> {
// let QUERY = gql`
// query Categories($categoryId: Int!){
// projectsByCategory(category_id: ${categoryId}) {
// id
// cover_image
// thumbnail_image
// title
// website
// lightning_address
// votes_count
// }
// }
// `;
return data.projectsCards;
}
// // returns the latest bunch of projects in each ( or some ) categories, and returns the hottest projects
// export async function getLatestProjects(): Promise<
@@ -26,5 +60,17 @@ import data from "./mockData.json";
// }
export async function getProjectById(projectId: string): Promise<Project> {
return data.project as any;
// let QUERY = gql`
// query Project(projectId: String!) {
// getProject(id: $projectId) {
// id
// cover_image
// thumbnail_image
// title
// website
// votes_count
// }
// }
// `;
return data.project;
}

View File

@@ -1,131 +1,131 @@
{
"categories": [
{
"id": 111,
"id": "111",
"title": "Hottest"
},
{
"id": 123,
"id": "123",
"title": "Art & Collectibles"
},
{
"id": 124,
"id": "124",
"title": "DeFi"
},
{
"id": 311,
"id": "311",
"title": "Entertainment"
},
{
"id": 333,
"id": "333",
"title": "Exchange"
},
{
"id": 223,
"id": "223",
"title": "News"
},
{
"id": 451,
"id": "451",
"title": "Shop"
},
{
"id": 2321,
"id": "232",
"title": "Social"
},
{
"id": 51231,
"id": "512",
"title": "Wallet"
},
{
"id": 1321,
"id": "132",
"title": "Other"
}
],
"projectsCards": [
{
"id": 123123,
"id": "123",
"title": "First App",
"thumbnail_image": "https://via.placeholder.com/150",
"category": {
"id": 51231,
"id": "512",
"title": "{app.category}"
},
"votes_count": 123
},
{
"id": 765454,
"id": "765",
"title": "Second App",
"thumbnail_image": "https://via.placeholder.com/150",
"category": {
"id": 51231,
"id": "512",
"title": "{app.category}"
},
"votes_count": 123
},
{
"id": 55,
"id": "55",
"title": "Third App",
"thumbnail_image": "https://via.placeholder.com/150",
"category": {
"id": 51231,
"id": "512",
"title": "{app.category}"
},
"votes_count": 123
},
{
"id": 12344123,
"id": "12344123",
"title": "Fourth App",
"thumbnail_image": "https://via.placeholder.com/150",
"category": {
"id": 51231,
"id": "512",
"title": "{app.category}"
},
"votes_count": 123
},
{
"id": 56745,
"id": "56745",
"title": "Fifth App",
"thumbnail_image": "https://via.placeholder.com/150",
"category": {
"id": 51231,
"id": "512",
"title": "{app.category}"
},
"votes_count": 123
},
{
"id": 3312431,
"id": "3312431",
"title": "Sixth App",
"thumbnail_image": "https://via.placeholder.com/150",
"category": {
"id": 51231,
"id": "512",
"title": "{app.category}"
},
"votes_count": 123
}
],
"project": {
"id": 123123123,
"cover_image": "https://s3-alpha-sig.figma.com/img/07b8/5d84/145942255afd215b3da26dbbf1dd03bd?Expires=1638144000&Signature=Cl1DUQJIUsrrFi48M~qU1r3Z0agGdy-uiNUao5g8-nu34QtoyWTFPXvaH3naSZBYqcPyKFq1jaXF6Mw1uj1hdWwGhXhMPLnKNJFFrGViVXhXu-3YeCPY9p4-IcieFJBZPVA~zDY8zxY5b06loWsINAVx4eMHRAhSWl~~Mca5PjlSXloiYrT00W-6c9m8gevfMMX~PsHQedzwYzg0j2DlnhPX8LbRkli1G2OxtCaFwo3~HGHXIlFGuGU1uXRvi1qBWrdjdsuWgIly1ekcFfJWAKmwYXk06EtCmfWRgGYbD7cBK~lwOkFofbf1LW0yqLv0hr4svwToH~3FiHenrCF-1g__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
"id": "1233",
"cover_image": "https://picsum.photos/id/10/1024/1024",
"thumbnail_image": "https://s3-alpha-sig.figma.com/img/be1b/cd75/1baa911b3875134c0889d6755c4ba2cb?Expires=1638748800&Signature=DOiLciAA95w8gOvAowjiiR-ZPbmV1oGSRRK8YpE4ALMoe47pL7DifQxZvL1LQn~NRa0aLMoMk61521fbbGJeDAwk~j6fIm~iZAlMzQn7DdWy0wFR0uLQagOgpIiIXO-w8CeC14VoW-hrjIX5mDmOonJzkfoftGqIF1WCOmP2EuswyJpIngFdLb15gCex4Necs3vH2cuD9iSgWG2za97KfdXZP79ROyk2EN9Q3~a7RT4FTBBIlgKDLuFGSVRxReXVNajn~XSxBJh2de9dFVa3tOXkwJXu3jb0G4x-wRCaG-KmBhUOemuGtu5Fumh6goktGh~bIDwoHeUBVKFHAzaYgw__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA",
"title": "{Project Name}",
"website": "https://www.project.com",
"description": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Distinctio nobis aliquam, minima assumenda earum sapiente pariatur cupiditate error nihil, eius corporis ratione unde perspiciatis tenetur ipsa aut ex consequatur maiores eligendi quidem exercitationem suscipit. Ex hic reprehenderit deleniti possimus culpa animi velit? Dolores, nemo quis minima sapiente sed laborum ipsam?",
"title": "█████████",
"website": "████████",
"description": "██████████████████",
"category": {
"id": 124,
"title": "DeFi"
"id": "333",
"title": "█████"
},
"tags": [
{
"id": 123,
"title": "lightning"
"id": "123",
"title": "█████"
},
{
"id": 333,
"title": "payments"
"id": "313",
"title": "██████"
},
{
"id": 444,
"title": "lnurl-auth"
"id": "451",
"title": "█████"
}
],
"screenShots": [
@@ -134,6 +134,6 @@
"",
""
],
"votes_count": 123
"votes_count": 0
}
}

3
src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare interface Window {
webln: any;
}

View File

@@ -1,15 +1,27 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {
ApolloClient,
InMemoryCache,
ApolloProvider,
} from "@apollo/client";
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import Wrapper from './utils/Wrapper';
const client = new ApolloClient({
uri: 'https://xenodochial-goldstine-d09942.netlify.app/.netlify/functions/graphql',
cache: new InMemoryCache()
});
ReactDOM.render(
<React.StrictMode>
<Wrapper>
<App />
</Wrapper>
<ApolloProvider client={client}>
<Wrapper>
<App />
</Wrapper>
</ApolloProvider>
</React.StrictMode>,
document.getElementById('root')
);

View File

@@ -5,10 +5,11 @@ import mockData from "../../api/mockData.json";
interface StoreState {
project: Project | null;
projectSet: boolean;
}
const initialState = {
project: mockData.project,
project: null,
} as StoreState;
export const projectSlice = createSlice({

View File

@@ -1,21 +1,39 @@
import { requestProvider } from "webln";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface StoreState {
isConnected: boolean;
isLoading: boolean;
provider: any;
}
const isWebLNConnected = () => {
// since webln spec expects webln.enable() to be called on each load
// and extensions like alby do not inject the webln object with true
// every refresh requires the user to re-enable, even if its been
// given premission previously.
//
// that is to say ... this function is quite useless
if (typeof window.webln === 'undefined') {
return false;
} else if (window.webln.enabled === true) {
return true;
}
}
const initialState = {
isConnected: false,
isLoading: false,
provider: null,
} as StoreState;
export const walletSlice = createSlice({
name: "wallet",
initialState,
reducers: {
connectWallet(state) {
state.isConnected = true;
connectWallet(state, action: PayloadAction<any>) {
state.isConnected = action.payload ? true : false;
state.provider = action.payload;
},
},
});

View File

@@ -1,10 +1,14 @@
export interface AllCategoriesData {
allCategories: ProjectCategory[];
}
export interface ProjectCategory {
id: number;
id: string;
title: string;
}
export interface ProjectCard {
id: number;
id: string;
title: string;
thumbnail_image: string;
category: ProjectCategory;
@@ -12,14 +16,14 @@ export interface ProjectCard {
}
export interface Tag {
id: number;
id: string;
title: string;
}
export type Image = string;
export interface Project {
id: number;
id: string;
title: string;
category: ProjectCategory;
website?: string;

21459
yarn.lock Normal file

File diff suppressed because it is too large Load Diff