first: commit

This commit is contained in:
Andrei O 2023-02-13 21:20:50 +02:00
commit 124a1d4e97
No known key found for this signature in database
GPG Key ID: B961E5B68389457E
65 changed files with 4773 additions and 0 deletions

22
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
- uses: c-hive/gha-yarn-cache@v2
- run: yarn install
- run: yarn build
- run: yarn run check

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
releases

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

8
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"json.schemas": [
{
"fileMatch": ["manifest.json"],
"url": "https://json.schemastore.org/chrome-manifest.json"
}
]
}

6
CHANGELOG.MD Normal file
View File

@ -0,0 +1,6 @@
# Change Log
## [Version 1.0.1]
- first release

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Andrei O.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

15
PRIVACY_POLICY.md Normal file
View File

@ -0,0 +1,15 @@
# 𝐏𝐫𝐢𝐯𝐚𝐜𝐲 𝐏𝐨𝐥𝐢𝐜𝐲:
## Privacy Points:
- This extension does not collect any data form your device.
- All storage uses chrome.storage.local
- This extension does not use external scripts, everything is packed into the extension.
- This extension uses the manifest V3 which does not allow any third party scripts to be injected.
- This extension is completely open source, the source is available on Github - [https://github.com/andrei0x309/yup-live-chrome-extension](https://github.com/andrei0x309/yup-live-chrome-extension).
- Extension has no production dependecies
- Only third party APIs used is YUP API and CoinGecko for the price of YUP.
### 𝐂𝐨𝐧𝐭𝐚𝐜𝐭:
Discord: andrei0x309#6562

47
README.md Normal file
View File

@ -0,0 +1,47 @@
# YUP Live Chrome Extension
> Svelte open source helper extension for YUP protocol, a decentralized social media platform.
## Features
- Extremely light-weight size less than 1080p picture.
- No external dependencies.
- No production dependencies.
- No external components.
- Made with Svelte & WindiCSS & TypeScript & Vite.
- Everything loaded asynchronously.
- Inhouse router system.
- Automatically detects and disables other YUP chrome extensions.
## Pages
- Main page - profile, balance, and website rateing system.
- Optional Notifications page - shows notifications.
- Settings page - change settings.
- Login page - login or create account using YUP Live website.
- Usage page - show how many likes you have left and a counter for the next refill.
- Info page - show info about the extension.
## Additional features
- Chrome notification at login.
- Light and dark theme.
- Optinally enable or disable notification system.
- Opt-in notification for reward.
- Opt-in notification for refill.
- Optinally enable a overlay for all websites to easely rate on mobile.
## Screenshots
![Screenshot 1](/misc/screen_1.png?raw=true "Screenshot 1")
![Screenshot 2](/misc/screen_2.png?raw=true "Screenshot 2")
## Possible ways to contribute
- Install and use the extension.
- Fork the repository.
- Open a pull request.
- Open an issue.
- Rate the extension.
- Star the repository.
- Share the extension.

58
manifest.json Normal file
View File

@ -0,0 +1,58 @@
{
"name": "yup live",
"description": "Light alternative extension for yup protocol",
"version": "1.0.1",
"manifest_version": 3,
"icons": {
"16": "src/assets/icons/yup_ext_16.png",
"32": "src/assets/icons/yup_ext_32.png",
"48": "src/assets/icons/yup_ext_48.png",
"64": "src/assets/icons/yup_ext_64.png",
"128": "src/assets/icons/yup_ext_128.png",
"258": "src/assets/icons/yup_ext_256.png"
},
"background": {
"service_worker": "src/background/index.ts"
},
"action": {
"default_popup": "src/popup/popup.html",
"default_icon": {
"16": "src/assets/icons/yup_ext_16.png",
"32": "src/assets/icons/yup_ext_32.png",
"48": "src/assets/icons/yup_ext_48.png",
"64": "src/assets/icons/yup_ext_64.png",
"128": "src/assets/icons/yup_ext_128.png",
"258": "src/assets/icons/yup_ext_256.png"
}
},
"content_scripts": [
{
"matches": [
"http://*/*",
"https://*/*"
],
"all_frames": true,
"run_at": "document_start",
"js": ["src/client/content.ts"]
}
],
"web_accessible_resources": [{
"resources": [
"src/client/inject.js",
"src/assets/icons/yup_ext_32.png",
"src/assets/icons/yup_ext_48.png",
"src/assets/icons/yup_ext_64.png",
"src/assets/icons/yup_ext_128.png",
"src/assets/res/reward_optimized.png"
],
"matches": ["<all_urls>"]
}],
"permissions": [
"storage",
"management",
"notifications",
"tabs",
"clipboardWrite",
"alarms"
]
}

BIN
misc/screen_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
misc/screen_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

39
package.json Normal file
View File

@ -0,0 +1,39 @@
{
"name": "yup-live-browser-extension",
"description": "Yup Live Browser Extension",
"version": "1.0.0",
"type": "module",
"author": "andrei0x309",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/nekitcorp/chrome-extension-svelte-typescript-boilerplate.git"
},
"scripts": {
"dev": "vite",
"inject": "tsc --out src/client/inject.js src/client/inject.ts",
"build": "yarn inject && vite build",
"upgrade": "yarn-upgrade-all",
"check": "svelte-check --tsconfig ./tsconfig.json",
"release": "yarn config set version-tag-prefix yup-live-browser-extension@v && yarn config set version-git-message 'yup-live-browser-extension@v%s' && yarn version --patch && yarn postversion",
"postversion": "git push",
"pub": "yarn build && yarn release && ts-node --esm ./scripts/create-release.ts"
},
"devDependencies": {
"@crxjs/vite-plugin": "^1.0.14",
"@sveltejs/vite-plugin-svelte": "^2.0.2",
"@tsconfig/svelte": "^3.0.0",
"@types/chrome": "^0.0.212",
"archiver": "^5.3.1",
"sass": "^1.58.0",
"svelte": "^3.55.1",
"svelte-check": "^3.0.3",
"svelte-preprocess": "^5.0.1",
"svelte-windicss-preprocess": "~4.1.0",
"ts-node": "^10.9.1",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"vite": "^4.1.1",
"yarn-upgrade-all": "^0.7.2"
}
}

69
scripts/create-release.ts Normal file
View File

@ -0,0 +1,69 @@
const pFs = import('fs')
const pCps = import ('child_process')
async function ghRelease(changes: string[]) {
const fs = (await pFs).default
if (!fs.existsSync('releases')){
fs.mkdirSync('releases');
}
const pkg = JSON.parse(fs.readFileSync('package.json').toString());
const archiver = (await import('archiver')).default
const archive = archiver('zip', { zlib: { level: 9 } });
const dirPipes = ['dist'];
const filePipes = ['LICENSE', 'README.md', 'PRIVACY_POLICY.md'];
const outputPath = `releases/${pkg.version}.zip`;
const outputZip = fs.createWriteStream(outputPath);
await new Promise((resolve, reject) => {
let arch = archive;
dirPipes.forEach((dir) => {
arch = arch.directory(dir, false);
});
filePipes.forEach((file) => {
arch = arch.file(file, { name: file });
});
arch.on('error', (err: unknown) => reject(err)).pipe(outputZip);
outputZip.on('close', () => resolve(true));
arch.finalize();
});
const changeLogPath = `releases/${pkg.version}.changelog.md`;
fs.writeFileSync(
changeLogPath,
`# ${pkg.version} \n
${changes.reduce((acc: string, change: string) => {
return acc + `- ${change}\n`;
}, '')}`,
);
const cps = (await pCps)
console.log(
await new Promise((resolve) => {
const p = cps.spawn('gh', ['release', 'create', `v${pkg.version}`, `./${outputPath}`, '-F', `./${changeLogPath}`], {
shell: true,
});
// const p = spawn('pwd');
let result = '';
p.stdout.on('data', (data) => (result += data.toString()));
p.stderr.on('data', (data) => (result += data.toString()));
p.on('close', () => {
resolve(result);
});
}),
);
}
(async () => {
if (!process.argv[2]) {
console.log('No changes provided');
return;
}
const changes = process.argv[2].split(',');
await ghRelease(changes);
console.log('Release created', changes);
})();

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

144
src/background/index.ts Normal file
View File

@ -0,0 +1,144 @@
import { SEND_AUTH_NOTIF } from '@/constants/messeges';
import { initStorage } from '@/utils/storage'
import { getStore, setProfile, setNotifStorageNotifs, setSettings, getNotifStorage, setNotifStorageLastRewardNotif } from '@/utils/storage'
import type { Notification } from '@/utils/types';
import { API_BASE } from '@/constants/config';
import { getNotifications } from '@/utils/notifications';
import { setBadge } from '@/utils/chrome-misc'
import { closeTo, getTimeRemaining } from '@/utils/time';
import { getActionUsage } from '@/utils/user';
// Disable conflict with yup extension
const yupExtensionId = 'nhmeoaahigiljjdkoagafdccikgojjoi'
console.info('Service worker started')
const alarmHandler = async () => {
const store = await getStore()
const requests = {} as Record<string, Promise<Response | Notification[]>>
if (store?.user?.auth?.authToken) {
requests.profile = fetch(`${API_BASE}/web3-profiles/` + store.user.auth.address)
if (store?.settings.notificationsEnabled) {
requests.notifications = getNotifications({
type: 'all',
limit: '15',
skip: '0',
userId: store.user.auth.userId
})
}
requests.coinGecko = fetch('https://api.coingecko.com/api/v3/simple/price?ids=yup&vs_currencies=usd')
try {
const profile = await requests.profile as Response
const profileJson = await profile.json()
setProfile(profileJson).catch(console.error)
} catch (error) {
console.error('Error fetching profile', error)
}
try {
const coinGecko = await requests.coinGecko as Response
const coinGeckoJson = await coinGecko.json()
const coinGeckoPrice = coinGeckoJson.yup.usd
const store = await getStore()
if (store.settings.coinGeckoPrice !== coinGeckoPrice) {
await chrome.storage.local.set({ store: { ...store, settings: { ...store.settings, coinGeckoPrice } } })
}
} catch (error) {
console.error('Error fetching coinGecko', error)
}
if (store?.settings.notificationsEnabled) {
try {
const notifications = await requests.notifications as Notification[]
const notSeen = notifications.reverse().filter(notif => !notif.seen)
const updateSettings = {} as Record<string, boolean>
if (notSeen.length > 0) {
setBadge(String(notSeen.length))
setNotifStorageNotifs(notSeen).catch(console.error)
updateSettings.hasNewNotifications = true
} else {
setBadge('')
updateSettings.hasNewNotifications = false
}
if (store.settings?.chromeNotifWhenReward && notSeen.some(notif => notif.action === 'reward')) {
const rewardNotif = notSeen.find(notif => notif.action === 'reward')
if (rewardNotif) {
const storeReward = (await getNotifStorage()).lastRewardNotif
if (!storeReward || (storeReward.id !== rewardNotif._id &&
!closeTo(new Date(storeReward.createdAt), new Date(rewardNotif.createdAt), 2e4))) {
{
setNotifStorageLastRewardNotif({ createdAt: rewardNotif.createdAt, id: rewardNotif._id }).then(() => {
chrome.notifications.create({
type: 'basic',
iconUrl: chrome.runtime.getURL('src/assets/icons/yup_ext_128.png'),
title: 'Yup Live Extension',
message: `You have been alocated a future reward of ${rewardNotif.quantity} YUP`,
})
}).catch(console.error)
}
}
}
}
if (store.settings?.chromeNotifWhenAbleToVote) {
const lastReset = (await getActionUsage(store?.user?.auth?.userId))?.data?.lastReset
if (lastReset) {
const isReset = getTimeRemaining(lastReset).total <= 0;
if (!closeTo(new Date(lastReset), new Date(store?.settings?.refilNotifTimestamp), 3.6e6) && isReset) {
updateSettings.refilNotifTimestamp = lastReset
chrome.notifications.create({
type: 'basic',
iconUrl: chrome.runtime.getURL('src/assets/icons/yup_ext_128.png'),
title: 'Yup Live Extension',
message: `You can curate now again`,
})
}
}
}
setSettings(updateSettings).catch(console.error)
} catch (error) {
console.error('Error fetching notifications', error)
}
}
}
}
chrome.alarms.create(
'alarm',
{
periodInMinutes: 1,
},
)
chrome.alarms.onAlarm.addListener(alarmHandler)
chrome.runtime.onInstalled.addListener(() => {
initStorage()
chrome.management.setEnabled(yupExtensionId, false)
});
chrome.runtime.onStartup.addListener(() => {
initStorage()
chrome.management.setEnabled(yupExtensionId, false)
})
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
try {
if (request.type === SEND_AUTH_NOTIF) {
chrome.notifications.create({
type: 'basic',
iconUrl: chrome.runtime.getURL('src/assets/icons/yup_ext_128.png'),
title: 'Yup Live Extension',
message: 'You have been logged in.',
})
}
} catch (error) {
console.error('Error in message listener', error)
sendResponse({ error })
}
return true
})

51
src/client/content.ts Normal file
View File

@ -0,0 +1,51 @@
(() =>{
try {
const container = document.documentElement;
const script = document.createElement('script');
script.setAttribute('async', "false")
script.setAttribute('fetchpriority', "high")
script.src = chrome.runtime.getURL('src/client/inject.js')
container.prepend(script)
script.addEventListener('load', () => { container.removeChild(script) } )
} catch (error) {
console.error('Yup Live Extension inject failed.', error);
}
})()
import { SEND_VOTE, SET_AUTH } from '@/constants/messeges'
import { setAuth } from '@/utils/storage'
const allowedEvents = [SEND_VOTE, SET_AUTH]
window.addEventListener("message", (event) => {
if (event.source != window)
return;
if(allowedEvents.includes(event?.data?.type ?? '')){
console.log('Yup Live Extension received message:', event.data);
switch (event.data.type) {
case SEND_VOTE:
console.log('SEND_VOTE', event.data.payload)
break;
case SET_AUTH:
console.log('SET_AUTH', event.data.payload)
setAuth(event.data.payload).catch(console.error)
break;
default:
break;
}
}
})
import Overlay from '@/overlay/Overlay.svelte'
import('@/utils/storage').then(({ getStore }) => {
getStore().then(async (store) => {
if (store.settings.injectEmbed) {
setTimeout(() => {
//@ts-ignore
new Overlay({ target: document.body });
}, 200)
}
});
});

42
src/client/inject.js Normal file
View File

@ -0,0 +1,42 @@
var SEND_VOTE = 'SEND_VOTE';
var SET_AUTH = 'SET_AUTH';
var WebCommunicator = /** @class */ (function () {
function WebCommunicator(injectAuthMethod) {
if (injectAuthMethod === void 0) { injectAuthMethod = false; }
var _this = this;
this._send = function (data) {
window.postMessage(data, "*");
};
this.submitVote = function (vote) {
return _this._send({
type: SEND_VOTE,
payload: vote
});
};
this.setAuth = function (authData) {
return _this._send({
type: SET_AUTH,
payload: authData
});
};
if (injectAuthMethod) {
;
window.yupSetAuth = this.setAuth;
}
else {
;
window.yupSetAuth = function () { return Promise.resolve(null); };
}
;
window.yupSubmitVote = this.submitVote;
}
return WebCommunicator;
}());
var allowRegex = /^((http:|https:))?([/][/])?(www.)?[a-zA-Z\-_0-9]{0,}\.?[a-zA-Z\-_0-9]{0,}(yup.info.gf|yup-live.pages.dev|.yup.io|yup-team.vercel.app|localhost\/|localhost:)(.*)/gm;
var isAllowed = allowRegex.test(window.location.href);
if (isAllowed) {
new WebCommunicator(true);
}
else {
new WebCommunicator();
}

43
src/client/inject.ts Normal file
View File

@ -0,0 +1,43 @@
const SEND_VOTE = 'SEND_VOTE'
const SET_AUTH = 'SET_AUTH'
class WebCommunicator {
_send = (data) => {
window.postMessage(data, "*")
}
submitVote = (vote) => {
return this._send({
type: SEND_VOTE,
payload: vote
})
}
setAuth = (authData) => {
return this._send({
type: SET_AUTH,
payload: authData
})
}
constructor (injectAuthMethod = false) {
if (injectAuthMethod) {
;(<any>window).yupSetAuth = this.setAuth
} else {
;(<any>window).yupSetAuth = () => Promise.resolve(null)
}
;(<any>window).yupSubmitVote = this.submitVote
}
}
const allowRegex = /^((http:|https:))?([/][/])?(www.)?[a-zA-Z\-_0-9]{0,}\.?[a-zA-Z\-_0-9]{0,}(yup.info.gf|yup-live.pages.dev|.yup.io|yup-team.vercel.app|localhost\/|localhost:)(.*)/gm
const isAllowed = allowRegex.test(window.location.href)
if(isAllowed) {
new WebCommunicator(true)
} else {
new WebCommunicator()
}

View File

@ -0,0 +1,91 @@
<script lang="ts">
import { fly } from 'svelte/transition'
import type { FlyParams } from 'svelte/transition'
let hidden = true
let timer: undefined | number | string = undefined
let alertTimeout = 5000
let alretType = 'success'
let alertMsg = ''
export const show = (message: string, type: string = 'success', timeout: number = 5000) => {
hidden = false
alertMsg = message
alretType = type
alertTimeout = timeout
timer = setTimeout(() => {
hidden = true
}, alertTimeout) as unknown as number
}
export const close = () => {
hidden = true
clearTimeout(timer)
}
const tryFly = (node: Element, params?: FlyParams) => {
try{
return fly(node, params)
}catch {
// ignore
}
}
</script>
{#if !hidden}
<div transition:tryFly="{{ y: 200, duration: 500 }}" class="shadow-md flex flex-row rounded-lg alertCmp">
<div
class="{`${
alretType === 'warning' ? 'yellow' : alretType === 'error' ? 'red' : 'green'
} inline-block rounded-lg p-1 mr-1`}"
></div>
<p class="p-1 flex items-center">{ alertMsg }</p>
<span class="h-5 w-5 text-gray-500 inline-block p-1" on:click={() => close()} aria-hidden="true">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 absolute"
style="right: 0.4rem; top: 0.4rem"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</span>
</div>
{/if}
<style lang="scss">
.alertCmp {
background-color: #0e0e0efc;
box-shadow: 0 1px 3px #0000001a, 0 1px 2px #0000000f;
position: fixed;
z-index: 100;
width: 230px;
max-width: 100%;
border-radius: 0.25rem;
border: 1px solid rgba(0,0,0,.1);
color: #fff;
font-size: .675rem;
font-weight: 400;
letter-spacing: .025em;
text-align: left;
padding: 0.2rem 0.6rem;
display: flex;
flex-direction: row;
align-items: center;
bottom: 0.8rem;
}
.green {
background-color: #48bb78;
}
.yellow {
background-color: #f6e05e;
}
.red {
background-color: #e53e3e;
}
</style>

View File

@ -0,0 +1,43 @@
<script lang="ts">
import { isUrlInvalid } from '@/utils/misc';
export let source
let loading = true
let loaded = false
let error = false
export const onLoad = () => {
loading = false
loaded = true
}
export const onError = () => {
loading = false
error = true
}
$: {
if(isUrlInvalid(source)) {
loading = false
error = true
} else {
loading = true
error = false
}
}
</script>
<div class="inline-flex {`${loading ? 'animate-pulse' :''}`}">
{#if loading || loaded}
<slot name="img">
</slot>
{:else if error}
<slot name="error">
</slot>
{/if}
<slot name="after">
</slot>
</div>

View File

@ -0,0 +1,93 @@
<script lang="ts">
import { timeSince } from '@/utils/time';
import ImgLoader from './ImgLoader.svelte';
import type { Notification } from '@/utils/types';
import { mainStore } from '@/utils/store';
import { chromeUrl } from '@/utils/chrome-misc';
import { extrenalNavigate } from "@/utils/chrome-misc";
let loader
export let notif: Notification
</script>
{#if notif.action === 'vote'}
{@const url = notif.post.url }
{@const length = url.length}
{@const shortUrl = url.slice(0, 10) + '...' + url.slice(length - 10, length) }
{@const finalUrl = length > 24 ? shortUrl : url }
<div class="flex flex-row notifBody">
<ImgLoader bind:this={loader} source="{notif.image} ">
<img class="notificationImage" on:load="{() => loader.onLoad()}" on:error={() => loader.onError()} style="{ $mainStore.settings.theme === 'light'? 'filter: invert(0.9);' : '' }" slot="img" src="{notif.image}" alt="preview">
<svg class="notificationImage" style="{ $mainStore.settings.theme === 'light'? 'filter: invert(0.9);' : '' }" slot="error" viewBox="0 0 512 512"><g><polygon points="40,38.999 40,468.998 215,293.998 270,348.998 360,258.998 470,358.998 470,38.999 " style="fill:#EFF3F6;"/>
<g><circle cx="150" cy="158.999" r="40" style="fill:#FCD884;"/></g><g><polygon points="470,358.998 470,468.998 385,468.998 385,463.998 270,348.998 360,258.998 " style="fill:#70993F;"/></g><g><polygon points="385,463.998 385,468.998 40,468.998 215,293.998 270,348.998" style="fill:#80AF52;"/></g></g><g/></svg>
</ImgLoader>
<div class="ml-4 text-left" style="width: 97%">
<p class="text-xs text-gray-200 my-0 mt-1">
{#if notif.like}
<svg class="w-4 like-dsilike" viewBox="0 0 24 24">
<path fill="#fff" d="M12,3.172L5.586,9.586c-0.781,0.781-0.781,2.047,0,2.828s2.047,0.781,2.828,0L10,10.828v7.242c0,1.104,0.895,2,2,2 c1.104,0,2-0.896,2-2v-7.242l1.586,1.586C15.977,12.805,16.488,13,17,13s1.023-0.195,1.414-0.586c0.781-0.781,0.781-2.047,0-2.828 L12,3.172z"/>
</svg>
{:else}
<svg class="w-4 down like-dsilike" viewBox="0 0 24 24">
<path fill="#fff" d="M12,3.172L5.586,9.586c-0.781,0.781-0.781,2.047,0,2.828s2.047,0.781,2.828,0L10,10.828v7.242c0,1.104,0.895,2,2,2 c1.104,0,2-0.896,2-2v-7.242l1.586,1.586C15.977,12.805,16.488,13,17,13s1.023-0.195,1.414-0.586c0.781-0.781,0.781-2.047,0-2.828 L12,3.172z"/>
</svg>
{/if}
by {notif.voter.length > 12 ? notif.voter.slice(0, 12) + '...' : notif.voter}
</p>
<p class="text-xs text-gray-200 my-0 mt-1">
<span on:click={() => extrenalNavigate(`https://yup-live.pages.dev/post/${notif.postid}`)}
aria-hidden
class="text-blue-200 interactive-svg">{finalUrl}</span>
</p>
<p class="text-xs text-gray-200 my-0 my-1 text-right mr-2">
<svg class="w-3 inline" viewBox="0 0 20 20" ><path fill="#fff" d="M10 20a10 10 0 1 1 0-20 10 10 0 0 1 0 20zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm-1-7.59V4h2v5.59l3.95 3.95-1.41 1.41L9 10.41z"/></svg>
{timeSince(new Date(notif.createdAt))}</p>
</div>
</div>
{:else if notif.action === 'reward'}
<div class="flex flex-row notifBody">
<img class="notificationImage" src="{chromeUrl('src/assets/res/reward_optimized.png')}" alt="reward">
<div class="ml-4 text-left" style="width: 97%">
<p class="text-xs text-gray-200 my-0 mt-1">You were alocated a future reward of {notif?.quantity ?? 'unknown'} amount of YUP.
</p>
<p class="text-xs text-gray-500 my-0 my-1 text-right mr-2">
<svg class="w-3 inline" viewBox="0 0 20 20" ><path fill="#fff" d="M10 20a10 10 0 1 1 0-20 10 10 0 0 1 0 20zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm-1-7.59V4h2v5.59l3.95 3.95-1.41 1.41L9 10.41z"/></svg>
{timeSince(new Date(notif.createdAt))}</p>
</div>
</div>
{:else }
<div class="flex flex-row items-center notifBody">
<div class="ml-4 text-left" style="width: 97%">
<p class="text-xs text-gray-200 my-0 mt-1">{notif?.message ?? 'unknown notification type'}</p>
<p class="text-xs text-gray-500 my-0 my-1 text-right mr-2">
<svg viewBox="0 0 20 20" ><path fill="#fff" d="M10 20a10 10 0 1 1 0-20 10 10 0 0 1 0 20zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm-1-7.59V4h2v5.59l3.95 3.95-1.41 1.41L9 10.41z"/></svg>
{timeSince(new Date(notif.createdAt))}</p>
</div>
</div>
{/if}
<style class="scss">
.notificationImage {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
border-radius: 6px;
margin-left: 0.5rem;
margin-top: 0.5rem;
}
.notifBody {
background: #00000061;
border: 1px solid rgba(0, 0, 0, 0.938);
}
.like-dsilike {
position: relative;
top: 0.2rem;
}
</style>

View File

@ -0,0 +1,63 @@
<div class="page-loader-wrapper">
<div class="page-loader"></div>
</div>
<style lang="scss">
.page-loader-wrapper {
position: fixed;
top: 3rem;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
overflow: hidden;
}
.page-loader {
display: block;
position: relative;
left: 50%;
top: 40%;
width: 150px;
height: 150px;
margin: -75px 0 0 -75px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #16a085;
animation: spin 1.7s linear infinite;
z-index: 11;
}
.page-loader:before {
content: "";
position: absolute;
top: 5px;
left: 5px;
right: 5px;
bottom: 5px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #e74c3c;
animation: spin-reverse .6s linear infinite;
}
.page-loader:after {
content: "";
position: absolute;
top: 15px;
left: 15px;
right: 15px;
bottom: 15px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #f9c922;
animation: spin 1s linear infinite;
}
</style>

View File

@ -0,0 +1,247 @@
<script lang="ts">
import { mainStore } from '@/utils/store';
import { hasVote, getPost } from '@/utils/votes';
import { onMount } from 'svelte'
import { formatNumber } from '@/utils/misc'
import { fetchWAuth } from '@/utils/auth'
import { alertStore } from '@/utils/store';
import { API_BASE } from '@/constants/config';
import { executeVote } from '@/utils/votes';
export let url = ''
export let disabled = false
let loading = true
const defaultUserVote = {
rating: 0,
like: null,
_id: null
}
let userVote = defaultUserVote
let saveUserVote = defaultUserVote
let positiveWeight = 0
let negativeWeight = 0
let savePositiveWeight = 0
let saveNegativeWeight = 0
let timer = 0
let doingVote = false
let post = null
let delLoading = false
const getPostIntial = async (onlyPost = false) => {
post = await getPost(url)
if(!post) {
loading = false
} else {
positiveWeight = Math.trunc(post.rawPositiveWeight ?? 0 as number)
negativeWeight = Math.trunc(post.rawNegativeWeight ?? 0 as number)
savePositiveWeight = positiveWeight
saveNegativeWeight = negativeWeight
if(!onlyPost) {
const has = (await hasVote(post._id.postid, $mainStore.user.auth.userId))?.[0] ?? null
if(has){
userVote = has
saveUserVote = has
}
} else {
userVote = defaultUserVote
saveUserVote = defaultUserVote
}
}
}
onMount(async () => {
if(disabled) {
loading = false
return
}
if(!url) {
loading = false
disabled = true
return
}
await getPostIntial()
loading = false
})
const doVote = (type = true) => {
if(disabled) {
$alertStore?.show('You shall not pass', 'error')
return
}
if(timer && !doingVote) {
clearTimeout(timer)
}
let noVoteAlert = false
if(!doingVote) {
const lastRating = userVote?.rating ?? 1
let sameType = true
if(type !== userVote?.like && userVote._id !== null) {
userVote.rating = 0
sameType = false
}
if(type && userVote?.rating < 3 || !type && userVote?.rating < 2) {
userVote.rating += 1
}
if(userVote?.rating === lastRating && userVote && sameType) {
$alertStore?.show('You already gave maximum rating.', 'warning')
noVoteAlert = true
} else {
if(userVote.like !== type && userVote._id !== null) {
if(type) {
negativeWeight -= $mainStore.user.profile.yup.weight * (userVote.rating + lastRating - 1)
userVote.rating = 1
positiveWeight += $mainStore.user.profile.yup.weight * userVote.rating
} else {
positiveWeight -= $mainStore.user.profile.yup.weight * (userVote.rating + lastRating - 1)
userVote.rating = 1
negativeWeight += $mainStore.user.profile.yup.weight * userVote.rating
}
} else {
if(type) {
positiveWeight += $mainStore.user.profile.yup.weight * (userVote.rating - lastRating)
} else {
negativeWeight += $mainStore.user.profile.yup.weight * (userVote.rating - lastRating)
}
}
userVote.like = type
}
timer = setTimeout(async () => {
doingVote = true
let reqVote
try {
reqVote = await executeVote({
post,
userVote,
$mainStore,
$alertStore,
url,
noVoteAlert
})
if(!reqVote) {
throw new Error('Vote not executed')
}
savePositiveWeight = positiveWeight
saveNegativeWeight = negativeWeight
saveUserVote = userVote
} catch(e) {
console.log(e)
positiveWeight = savePositiveWeight
negativeWeight = saveNegativeWeight
userVote = saveUserVote
}
if(reqVote) {
userVote = reqVote
}
doingVote = false
}, 500) as unknown as number
}
}
const deleteVote = async () => {
if(delLoading) {
return
}
try {
doingVote = true
delLoading = true
const reqVote = await fetch(`${API_BASE}/votes/post/${post._id.postid}/voter/${$mainStore.user.auth.userId}`)
const voteId = (await reqVote.json())[0]._id.voteid
const p1 = fetchWAuth($mainStore, `${API_BASE}/votes/${voteId}`, {
method: 'DELETE'
})
const [req] = await Promise.all([p1])
if (req.ok) {
await getPostIntial(true)
$alertStore.show('Vote deleted!')
} else {
$alertStore.show('Vote not deleted due to error try to re-login!', 'error')
positiveWeight = savePositiveWeight
negativeWeight = saveNegativeWeight
userVote = saveUserVote
}
doingVote = false
delLoading = false
} catch (error) {
positiveWeight = savePositiveWeight
negativeWeight = saveNegativeWeight
userVote = saveUserVote
console.log(error)
$alertStore.show('The vote could not be deleted!')
delLoading = false
doingVote = false
}
}
</script>
{#if userVote._id !== null}
<svg on:click={() => deleteVote()} aria-hidden="true" class="{`w-4 opacity-30 delete interactive-svg ${delLoading ? 'animate-ping' : ''}`}" viewBox="0 0 512 512"><title/><polygon points="337.46 240 312 214.54 256 270.54 200 214.54 174.54 240 230.54 296 174.54 352 200 377.46 256 321.46 312 377.46 337.46 352 281.46 296 337.46 240" style="fill:none"/><polygon points="337.46 240 312 214.54 256 270.54 200 214.54 174.54 240 230.54 296 174.54 352 200 377.46 256 321.46 312 377.46 337.46 352 281.46 296 337.46 240" style="fill:none"/><path d="M64,160,93.74,442.51A24,24,0,0,0,117.61,464H394.39a24,24,0,0,0,23.87-21.49L448,160ZM312,377.46l-56-56-56,56L174.54,352l56-56-56-56L200,214.54l56,56,56-56L337.46,240l-56,56,56,56Z"/><rect height="80" rx="12" ry="12" width="448" x="32" y="48"/></svg>
{/if}
<div class="{`flex justify-center main-section bt ${doingVote ? 'animate-pulse disabled' : ''}`}" class:disabled>
<div on:click={() => doVote(true)} aria-hidden="true" class="{`flex w-1/2 p-4 box h-6 bf mr-4 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}">
{#if userVote.rating && userVote.like}
<svg class="w-8" viewBox="0 0 24 24">
<path fill="#fff" d="M12,3.172L5.586,9.586c-0.781,0.781-0.781,2.047,0,2.828s2.047,0.781,2.828,0L10,10.828v7.242c0,1.104,0.895,2,2,2 c1.104,0,2-0.896,2-2v-7.242l1.586,1.586C15.977,12.805,16.488,13,17,13s1.023-0.195,1.414-0.586c0.781-0.781,0.781-2.047,0-2.828 L12,3.172z"/>
</svg>
{:else}
<svg class="w-8" viewBox="0 0 24 24"><g><path fill="#fff" d="M12,21c-1.654,0-3-1.346-3-3v-4.764c-1.143,1.024-3.025,0.979-4.121-0.115c-1.17-1.169-1.17-3.073,0-4.242L12,1.758 l7.121,7.121c1.17,1.169,1.17,3.073,0,4.242c-1.094,1.095-2.979,1.14-4.121,0.115V18C15,19.654,13.654,21,12,21z M11,8.414V18 c0,0.551,0.448,1,1,1s1-0.449,1-1V8.414l3.293,3.293c0.379,0.378,1.035,0.378,1.414,0c0.391-0.391,0.391-1.023,0-1.414L12,4.586 l-5.707,5.707c-0.391,0.391-0.391,1.023,0,1.414c0.379,0.378,1.035,0.378,1.414,0L11,8.414z"/></g>
</svg>
{/if}
{#key positiveWeight}
<span class="ml-4">{formatNumber(positiveWeight)}</span>
{/key}
</div>
<div on:click={() => doVote(false)} aria-hidden="true" class="{`flex w-1/2 p-4 box h-6 bf ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}">
{#if userVote.rating && !userVote.like}
<svg class="w-8 down" viewBox="0 0 24 24">
<path fill="#fff" d="M12,3.172L5.586,9.586c-0.781,0.781-0.781,2.047,0,2.828s2.047,0.781,2.828,0L10,10.828v7.242c0,1.104,0.895,2,2,2 c1.104,0,2-0.896,2-2v-7.242l1.586,1.586C15.977,12.805,16.488,13,17,13s1.023-0.195,1.414-0.586c0.781-0.781,0.781-2.047,0-2.828 L12,3.172z"/>
</svg>
{:else}
<svg class="w-8 down" viewBox="0 0 24 24"><g><path fill="#fff" d="M12,21c-1.654,0-3-1.346-3-3v-4.764c-1.143,1.024-3.025,0.979-4.121-0.115c-1.17-1.169-1.17-3.073,0-4.242L12,1.758 l7.121,7.121c1.17,1.169,1.17,3.073,0,4.242c-1.094,1.095-2.979,1.14-4.121,0.115V18C15,19.654,13.654,21,12,21z M11,8.414V18 c0,0.551,0.448,1,1,1s1-0.449,1-1V8.414l3.293,3.293c0.379,0.378,1.035,0.378,1.414,0c0.391-0.391,0.391-1.023,0-1.414L12,4.586 l-5.707,5.707c-0.391,0.391-0.391,1.023,0,1.414c0.379,0.378,1.035,0.378,1.414,0L11,8.414z"/></g>
</svg>
{/if}
{#key negativeWeight}
<span class="ml-4">{formatNumber(negativeWeight)}</span>
{/key}
</div>
</div>
<style lang="scss">
.cursor-block {
cursor: not-allowed;
}
.cursor-pointer {
cursor: pointer;
}
.box {
box-shadow: inset 3px -3px 6rem 11px #0000005e;
}
.bf {
border: 1px solid #d3d9dfc2;
border-radius: 6px;
transition: all 0.2s ease-in-out;
}
.bf:hover {
color: #fffee5;
background-color: #0000004f;
border: 1px solid #d3d9df73;
}
.down {
transform: rotate(180deg);
}
.delete {
position: absolute;
top: 225px;
right: 7px;
fill: #f0f8ff;
}
</style>

View File

@ -0,0 +1,69 @@
<script lang="ts">
import RateSingle from '@/components/RateSingle.svelte'
import { getCurrentTab } from '@/utils/chrome-misc'
import { onMount } from 'svelte'
import { mainStore } from '@/utils/store'
import ImgLoader from './ImgLoader.svelte';
import { isUrlInvalid } from '@/utils/misc';
let tab = null
let url
let isValid = true
let loading = true
let loader: ImgLoader
onMount(async () => {
tab = (await getCurrentTab())[0]
isValid = !isUrlInvalid(tab?.url)
try {
url = new URL(tab.url)
} catch (error) {
console.log(error)
}
console.log(url)
loading = false
})
$: {
if(loader && (!tab?.favIconUrl || !isValid) ) {
loader.onError()
}
}
</script>
{#if tab}
<div class="{`flex flex-col ${loading ? 'animate-pulse' :''}`}">
{#if url}
<div class="flex items-center flex-col text-[0.8rem] leading-4 mb-4">
<div class="mb relative">
<ImgLoader source={tab.favIconUrl} bind:this={loader}>
<img style="{ $mainStore.settings.theme === 'light'? 'filter: invert(0.9);' : '' }" slot="img" on:load={() => loader.onLoad()} on:error={() => loader.onError}
class="w-5 h-5 mt-2 rounded-full wicon" src="{tab.favIconUrl}" alt="favicon" />
<svg class="w-5 h-5 mt-2 rounded-full wicon" style="{ $mainStore.settings.theme === 'light'? 'filter: invert(0.9);' : '' }" slot="error" viewBox="0 0 512 512"><g><polygon points="40,38.999 40,468.998 215,293.998 270,348.998 360,258.998 470,358.998 470,38.999 " style="fill:#EFF3F6;"/>
<g><circle cx="150" cy="158.999" r="40" style="fill:#FCD884;"/></g><g><polygon points="470,358.998 470,468.998 385,468.998 385,463.998 270,348.998 360,258.998 " style="fill:#70993F;"/></g><g><polygon points="385,463.998 385,468.998 40,468.998 215,293.998 270,348.998" style="fill:#80AF52;"/></g></g><g/></svg>
</ImgLoader>
{#if isValid}
<h1 class="inline-flex text-[1.1rem] font-semibold mb-2">Rate Website</h1>
{:else}
<h1 class="inline-flex text-[1.1rem] font-semibold mb-2">Invalid URL</h1>
{/if}
</div>
<span>Hostname: {url.hostname.length > 18 ? url.hostname.slice(0,16) + '...': url.hostname }</span>
<span>URL: {url.href.length > 20 ? '...' + url.href.slice(-20) : url.href}</span>
</div>
{/if}
<RateSingle url={url.href.replace(/\/$/gms, '') ?? ''} disabled={!isValid} />
</div>
{/if}
<style>
.wicon {
position: absolute;
top: 2px;
left: -24px;
background-color: #7878788f;
padding: 0.13rem;
}
</style>

4
src/constants/config.ts Normal file
View File

@ -0,0 +1,4 @@
export const API_BASE = 'https://api.yup.io'
export const DEV_BASE = 'http://localhost:4566'
export const YUP_LIVE_BASE = 'https://yup-live.pages.dev'
export const APP_BASE = YUP_LIVE_BASE

View File

@ -0,0 +1,3 @@
export const SEND_VOTE = 'SEND_VOTE'
export const SET_AUTH = 'SET_AUTH'
export const SEND_AUTH_NOTIF = 'SEND_AUTH_NOTIF'

View File

@ -0,0 +1,50 @@
<script lang="ts">
import Alert from "@/components/Alert.svelte";
import '@/overlay/overlay.scss'
import { getStore } from "@/utils/storage";
import { executeVote } from "@/utils/votes";
let alert
let loading = false
const submitRate = async (type = true) => {
if(loading) return
loading = true
const store = await getStore()
const payload ={
userVote: {
like: type,
rating: 1
},
post: '',
url: document.location.href.replace(/\/$/gms, ''),
$mainStore: store,
$alertStore: null
}
const vote = await executeVote(payload)
console.log(vote)
if (vote?._id){
alert.show(type ? 'Liked' : 'Disliked', 'success')
} else {
alert.show('Error submitting', 'error')
}
loading = false
}
</script>
<div class="yup-overlay" >
<svg class="{`yup-logo ${loading ? 'yup-rotate': ''}`}" viewBox="0 0 366 366" fill="none" ><path d="M182.911 366C82.1487 366 0 283.851 0 182.911C0 81.9697 82.1487 0 182.911 0C283.672 0 365.821 82.1487 365.821 182.911C365.821 283.672 283.851 366 182.911 366ZM182.911 17.3604C91.6342 17.3604 17.1814 91.6342 17.1814 183.089C17.1814 274.545 91.4553 348.819 182.911 348.819C274.366 348.819 348.64 274.545 348.64 183.089C348.64 91.6342 274.366 17.3604 182.911 17.3604Z" fill="currentColor"></path><path d="M129.218 156.959C105.952 156.959 86.8018 139.241 84.4751 116.69C84.1172 114.185 86.2649 111.858 88.9495 111.858H97.0033C99.5089 111.858 101.657 113.827 102.014 116.332C104.162 129.576 115.616 139.599 129.397 139.599C143.178 139.599 154.633 129.576 156.78 116.332C157.138 113.827 159.286 111.858 161.792 111.858H169.845C172.53 111.858 174.499 114.185 174.32 116.69C171.635 139.241 152.306 156.959 129.218 156.959Z" fill="currentColor"></path><path d="M277.05 148.19H268.997C266.491 148.19 264.343 146.221 263.985 143.715C261.838 130.471 250.383 120.449 236.602 120.449C222.822 120.449 211.367 130.471 209.22 143.715C208.862 146.221 206.714 148.19 204.208 148.19H196.155C193.47 148.19 191.501 145.863 191.68 143.357C194.186 120.807 213.336 103.089 236.424 103.089C259.511 103.089 278.84 120.807 281.167 143.357C281.883 145.863 279.735 148.19 277.05 148.19Z" fill="currentColor"></path><path d="M185.058 306.939C115.616 308.192 58.8818 250.204 58.8818 180.763V178.615C58.8818 176.289 60.8505 174.32 63.1772 174.32H302.464C304.791 174.32 306.76 176.289 306.76 178.615V182.911C306.939 250.562 252.531 305.865 185.058 306.939ZM79.8217 191.68C78.2109 191.68 76.7791 193.112 76.9581 194.723C82.8642 248.057 128.144 289.579 182.91 289.579C237.676 289.579 282.956 248.057 288.862 194.723C289.041 193.112 287.788 191.68 285.999 191.68H79.8217Z" fill="currentColor"></path></svg>
<button class="{`yup-btn up ${loading? 'disabled' : ''}`}"
on:click={() => submitRate(true)}>
<svg class="yup-rate-svg" viewBox="0 0 24 24">
<path fill="#fff" d="M12,3.172L5.586,9.586c-0.781,0.781-0.781,2.047,0,2.828s2.047,0.781,2.828,0L10,10.828v7.242c0,1.104,0.895,2,2,2 c1.104,0,2-0.896,2-2v-7.242l1.586,1.586C15.977,12.805,16.488,13,17,13s1.023-0.195,1.414-0.586c0.781-0.781,0.781-2.047,0-2.828 L12,3.172z"/>
</svg></button>
<button class="{`yup-btn yup-btn-down down ${loading? 'disabled' : ''} `}" on:click={() => submitRate(true)}>
<svg class="yup-rate-svg" viewBox="0 0 24 24">
<path fill="#fff" d="M12,3.172L5.586,9.586c-0.781,0.781-0.781,2.047,0,2.828s2.047,0.781,2.828,0L10,10.828v7.242c0,1.104,0.895,2,2,2 c1.104,0,2-0.896,2-2v-7.242l1.586,1.586C15.977,12.805,16.488,13,17,13s1.023-0.195,1.414-0.586c0.781-0.781,0.781-2.047,0-2.828 L12,3.172z"/>
</svg></button>
<Alert bind:this={alert} />
</div>

95
src/overlay/overlay.scss Normal file
View File

@ -0,0 +1,95 @@
@keyframes yup-pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.2);
opacity: 0.5;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes yup-pulse-down {
0% {
transform: scale(1) rotate(180deg);
opacity: 1;
}
50% {
transform: scale(1.2) rotate(180deg);
opacity: 0.5;
}
100% {
transform: scale(1) rotate(180deg);
opacity: 1;
}
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.yup-rotate {
animation: rotate 1s linear infinite;
}
.yup-overlay {
position: fixed;
z-index: 2147483647;
top: 92vh;
left: 8px;
display: flex;
opacity: 0.7;
padding: 0.3rem;
gap: 0.4rem;
background: #121212d9;
.yup-logo {
opacity: 0.75;
width: 30px;
color: #eee;
}
.yup-btn {
width: 30px;
height: 30px;
background: transparent;
border: 1px solid #3344;
cursor: pointer;
}
.yup-btn:hover {
background: rgba(62, 62, 99, 0.267);
}
.yup-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.yup-btn.up:hover {
animation: yup-pulse 0.5s ease-in-out infinite;
}
.yup-btn.down:hover {
animation: yup-pulse-down 0.5s ease-in-out infinite;
}
.yup-rate-svg {
display: inline;
width: 16px;
}
.yup-btn-down {
transform: rotate(180deg);
}
}

138
src/pages/Entry.svelte Normal file
View File

@ -0,0 +1,138 @@
<script lang="ts">
// import { storage } from "@/storage";
import { extrenalNavigate } from "@/utils/chrome-misc";
import { getStore } from '@/utils/storage'
import { onMount } from "svelte";
import { navigate } from '@/utils/router'
import { mainStore } from '@/utils/store'
// https://yup-live.pages.dev
import { APP_BASE } from '@/constants/config'
import Alert from '@/components/Alert.svelte'
import { alertStore } from '@/utils/store'
import PageLoader from "@/components/PageLoader.svelte";
let store
let loading = true
let currentRoute = null
let auth = false
let alert: Alert
let navigating = false
const lNavigate = async (path: string) => {
if(navigating) return
navigating = true
await navigate(path)
currentRoute = path
navigating = false
}
onMount(async () => {
store = await getStore()
alertStore.set(alert)
auth = store?.user?.auth?.userId ?? false
if(auth){
await mainStore.set(store)
await navigate('/')
currentRoute = '/'
} else {
await navigate('/login')
currentRoute = '/login'
}
loading = false
})
</script>
<div class="entry" style="{ $mainStore.settings.theme === 'light'? 'filter: invert(1);' : '' }">
<a href="#app" on:click="{() => extrenalNavigate(APP_BASE)}">
<h1 aria-label="logo" class="logo inline-flex items-center text-[1.6rem] font-bold gap-2.5 pl-8">
<span class="gradient-text" style="{ $mainStore.settings.theme === 'light'? 'filter: invert(1.1);' : '' }" >YUP</span>
</h1>
</a>
{#if loading}
<PageLoader />
{:else if auth}
<div class="headerMenu flex">
{#if currentRoute !== '/info'}
<svg on:click={() => lNavigate('/info')} aria-hidden="true" viewBox="0 0 512 512" class="w-6 mx-1 interactive-svg"><path fill="#fff" d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM256 128c17.67 0 32 14.33 32 32c0 17.67-14.33 32-32 32S224 177.7 224 160C224 142.3 238.3 128 256 128zM296 384h-80C202.8 384 192 373.3 192 360s10.75-24 24-24h16v-64H224c-13.25 0-24-10.75-24-24S210.8 224 224 224h32c13.25 0 24 10.75 24 24v88h16c13.25 0 24 10.75 24 24S309.3 384 296 384z"/></svg>
{/if}
{#if currentRoute !== '/'}
<svg on:click={() => lNavigate('/')} aria-hidden="true" class="w-6 mx-1 interactive-svg" viewBox="0 0 24 24"><g id="info"/><g><path fill="#fff" d="M12,0C5.4,0,0,5.4,0,12c0,6.6,5.4,12,12,12s12-5.4,12-12C24,5.4,18.6,0,12,0z M12,4c2.2,0,4,2.2,4,5s-1.8,5-4,5 s-4-2.2-4-5S9.8,4,12,4z M18.6,19.5C16.9,21,14.5,22,12,22s-4.9-1-6.6-2.5c-0.4-0.4-0.5-1-0.1-1.4c1.1-1.3,2.6-2.2,4.2-2.7 c0.8,0.4,1.6,0.6,2.5,0.6s1.7-0.2,2.5-0.6c1.7,0.5,3.1,1.4,4.2,2.7C19.1,18.5,19.1,19.1,18.6,19.5z" id="user2"/></g></svg>
{/if}
{#if currentRoute !== '/usage' }
<svg on:click={() => lNavigate('/usage')} aria-hidden="true" class="w-6 mx-1 interactive-svg" viewBox="0 0 512 512"><title/><path fill="#fff" d="M256,48C141.12,48,48,141.12,48,256s93.12,208,208,208,208-93.12,208-208S370.88,48,256,48ZM173.67,162.34l105,71a32.5,32.5,0,0,1-37.25,53.26,33.21,33.21,0,0,1-8-8l-71-105a8.13,8.13,0,0,1,11.32-11.32ZM256,432C159,432,80,353.05,80,256a174.55,174.55,0,0,1,53.87-126.72,14.15,14.15,0,1,1,19.64,20.37A146.53,146.53,0,0,0,108.3,256c0,81.44,66.26,147.7,147.7,147.7S403.7,337.44,403.7,256c0-76.67-58.72-139.88-133.55-147V164a14.15,14.15,0,1,1-28.3,0V94.15A14.15,14.15,0,0,1,256,80C353.05,80,432,159,432,256S353.05,432,256,432Z"/></svg>
{/if}
{#if currentRoute !== '/settings' }
<svg on:click={() => lNavigate('/settings')} aria-hidden="true" class="w-6 mx-1 interactive-svg" viewBox="0 0 32 32" ><g><path clip-rule="evenodd" d="M8,6.021V3c0-1.654-1.346-3-3-3S2,1.346,2,3v3.021 C0.792,6.936,0,8.369,0,10s0.792,3.064,2,3.977V29c0,1.654,1.346,3,3,3s3-1.346,3-3V13.977c1.208-0.912,2-2.346,2-3.977 S9.208,6.936,8,6.021z M4,3c0-0.553,0.447-1,1-1s1,0.447,1,1v2.1C5.677,5.035,5.343,5,5,5S4.323,5.035,4,5.1V3z M6,29 c0,0.553-0.447,1-1,1s-1-0.447-1-1V14.898C4.323,14.965,4.657,15,5,15s0.677-0.035,1-0.102V29z M7.865,10.84 c-0.016,0.053-0.03,0.105-0.049,0.158c-0.095,0.264-0.217,0.514-0.378,0.736c-0.004,0.006-0.01,0.01-0.014,0.016 c-0.174,0.238-0.381,0.449-0.616,0.627c-0.004,0.004-0.007,0.006-0.01,0.008c-0.241,0.182-0.51,0.328-0.799,0.43 C5.686,12.928,5.353,13,5,13s-0.686-0.072-1-0.186c-0.289-0.102-0.558-0.248-0.799-0.43c-0.003-0.002-0.006-0.004-0.01-0.008 c-0.235-0.178-0.442-0.389-0.616-0.627c-0.004-0.006-0.01-0.01-0.014-0.016c-0.161-0.223-0.283-0.473-0.378-0.736 c-0.019-0.053-0.033-0.105-0.049-0.158C2.055,10.572,2,10.293,2,10c0-0.295,0.055-0.574,0.135-0.842 c0.016-0.053,0.03-0.105,0.049-0.156C2.278,8.738,2.4,8.488,2.562,8.264c0.004-0.006,0.01-0.01,0.014-0.016 c0.174-0.236,0.381-0.449,0.616-0.627c0.004-0.002,0.007-0.006,0.01-0.008C3.442,7.434,3.711,7.287,4,7.184 C4.314,7.072,4.647,7,5,7s0.686,0.072,1,0.184c0.289,0.104,0.558,0.25,0.799,0.43c0.003,0.002,0.006,0.006,0.01,0.008 c0.235,0.178,0.442,0.391,0.616,0.627c0.004,0.006,0.01,0.01,0.014,0.016C7.6,8.488,7.722,8.738,7.816,9.002 C7.835,9.053,7.85,9.105,7.865,9.158C7.945,9.426,8,9.705,8,10C8,10.293,7.945,10.572,7.865,10.84z" fill="#fff" fill-rule="evenodd"/><path clip-rule="evenodd" d="M30,6.021V3c0-1.654-1.346-3-3-3s-3,1.346-3,3v3.021 C22.791,6.936,22,8.369,22,10s0.791,3.064,2,3.977V29c0,1.654,1.346,3,3,3s3-1.346,3-3V13.977c1.207-0.912,2-2.346,2-3.977 S31.207,6.936,30,6.021z M26,3c0-0.553,0.447-1,1-1s1,0.447,1,1v2.1C27.676,5.035,27.342,5,27,5c-0.344,0-0.678,0.035-1,0.1V3z M28,29c0,0.553-0.447,1-1,1s-1-0.447-1-1V14.898C26.322,14.965,26.656,15,27,15c0.342,0,0.676-0.035,1-0.102V29z M29.865,10.84 c-0.016,0.053-0.031,0.105-0.049,0.158c-0.096,0.264-0.217,0.514-0.379,0.736c-0.004,0.006-0.01,0.01-0.014,0.016 c-0.174,0.238-0.381,0.449-0.615,0.627c-0.004,0.004-0.008,0.006-0.01,0.008c-0.242,0.182-0.51,0.328-0.799,0.43 C27.686,12.928,27.352,13,27,13c-0.354,0-0.686-0.072-1-0.186c-0.289-0.102-0.559-0.248-0.799-0.43 c-0.004-0.002-0.006-0.004-0.01-0.008c-0.236-0.178-0.443-0.389-0.617-0.627c-0.004-0.006-0.01-0.01-0.014-0.016 c-0.16-0.223-0.283-0.473-0.377-0.736c-0.02-0.053-0.033-0.105-0.049-0.158C24.055,10.572,24,10.293,24,10 c0-0.295,0.055-0.574,0.135-0.842c0.016-0.053,0.029-0.105,0.049-0.156c0.094-0.264,0.217-0.514,0.377-0.738 c0.004-0.006,0.01-0.01,0.014-0.016c0.174-0.236,0.381-0.449,0.617-0.627c0.004-0.002,0.006-0.006,0.01-0.008 c0.24-0.18,0.51-0.326,0.799-0.43C26.314,7.072,26.646,7,27,7c0.352,0,0.686,0.072,1,0.184c0.289,0.104,0.557,0.25,0.799,0.43 c0.002,0.002,0.006,0.006,0.01,0.008c0.234,0.178,0.441,0.391,0.615,0.627c0.004,0.006,0.01,0.01,0.014,0.016 c0.162,0.225,0.283,0.475,0.379,0.738c0.018,0.051,0.033,0.104,0.049,0.156C29.945,9.426,30,9.705,30,10 C30,10.293,29.945,10.572,29.865,10.84z" fill="#fff" fill-rule="evenodd"/><path clip-rule="evenodd" d="M19,18.021V3c0-1.654-1.346-3-3-3s-3,1.346-3,3v15.021 c-1.208,0.914-2,2.348-2,3.979s0.792,3.064,2,3.977V29c0,1.654,1.346,3,3,3s3-1.346,3-3v-3.023c1.207-0.912,2-2.346,2-3.977 S20.207,18.936,19,18.021z M15,3c0-0.553,0.447-1,1-1c0.553,0,1,0.447,1,1v14.1c-0.324-0.064-0.658-0.1-1-0.1 c-0.343,0-0.677,0.035-1,0.1V3z M17,29c0,0.553-0.447,1-1,1c-0.553,0-1-0.447-1-1v-2.102C15.323,26.965,15.657,27,16,27 c0.342,0,0.676-0.035,1-0.102V29z M18.865,22.84c-0.016,0.053-0.031,0.105-0.049,0.158c-0.096,0.264-0.217,0.514-0.379,0.736 c-0.004,0.006-0.01,0.01-0.014,0.016c-0.174,0.238-0.381,0.449-0.615,0.627c-0.004,0.004-0.008,0.006-0.01,0.008 c-0.242,0.182-0.51,0.328-0.799,0.43C16.686,24.928,16.352,25,16,25c-0.353,0-0.686-0.072-1-0.186 c-0.289-0.102-0.558-0.248-0.799-0.43c-0.003-0.002-0.006-0.004-0.01-0.008c-0.235-0.178-0.442-0.389-0.616-0.627 c-0.004-0.006-0.01-0.01-0.014-0.016c-0.161-0.223-0.283-0.473-0.378-0.736c-0.019-0.053-0.033-0.105-0.049-0.158 C13.055,22.572,13,22.293,13,22c0-0.295,0.055-0.574,0.135-0.842c0.016-0.053,0.03-0.105,0.049-0.156 c0.095-0.264,0.217-0.514,0.378-0.738c0.004-0.006,0.01-0.01,0.014-0.016c0.174-0.236,0.381-0.449,0.616-0.627 c0.004-0.002,0.007-0.006,0.01-0.008c0.241-0.18,0.51-0.326,0.799-0.43C15.314,19.072,15.647,19,16,19c0.352,0,0.686,0.072,1,0.184 c0.289,0.104,0.557,0.25,0.799,0.43c0.002,0.002,0.006,0.006,0.01,0.008c0.234,0.178,0.441,0.391,0.615,0.627 c0.004,0.006,0.01,0.01,0.014,0.016c0.162,0.225,0.283,0.475,0.379,0.738c0.018,0.051,0.033,0.104,0.049,0.156 C18.945,21.426,19,21.705,19,22C19,22.293,18.945,22.572,18.865,22.84z" fill="#fff" fill-rule="evenodd"/></g></svg>
{/if}
{#if currentRoute !== '/notifications' && $mainStore.settings.notificationsEnabled }
{@const hasNew = $mainStore.settings.hasNewNotifications}
<svg class="{`w-6 mx-1 interactive-svg ${hasNew ? 'animate-pulse red' : 'white'}`}"
style="{ $mainStore.settings.theme === 'light' && hasNew ? 'filter: invert(1.1);' : '' }"
on:click={() => lNavigate('/notifications')} aria-hidden="true" viewBox="0 0 512 512">
<path fill="currentColor" d="M470.346 403.26C468.655 400.18 466.169 397.611 463.146 395.82L441.487 382.82C433.033 377.737 426.038 370.553 421.183 361.967C416.327 353.381 413.776 343.684 413.777 333.82V216.66C413.756 182.674 402.73 149.609 382.347 122.414C361.964 95.2185 333.321 75.3562 300.707 65.8C299.296 65.3984 298.054 64.5512 297.165 63.3849C296.277 62.2187 295.79 60.7962 295.777 59.33C295.777 48.899 291.633 38.8953 284.257 31.5195C276.881 24.1437 266.877 20 256.447 20C246.016 20 236.012 24.1437 228.636 31.5195C221.26 38.8953 217.116 48.899 217.116 59.33C217.102 60.7957 216.614 62.2175 215.726 63.3834C214.837 64.5493 213.596 65.397 212.186 65.8C179.572 75.3562 150.929 95.2185 130.546 122.414C110.163 149.609 99.1367 182.674 99.1164 216.66V333.8C99.1173 343.664 96.566 353.361 91.7104 361.947C86.8548 370.533 79.8601 377.717 71.4065 382.8L49.7465 395.8C46.0067 398.033 43.0981 401.426 41.4629 405.463C39.8276 409.501 39.5551 413.961 40.6867 418.168C41.8184 422.374 44.2922 426.096 47.7324 428.767C51.1726 431.439 55.3911 432.915 59.7465 432.97H453.086C456.534 432.888 459.903 431.923 462.871 430.165C465.839 428.408 468.305 425.918 470.034 422.934C471.764 419.95 472.698 416.572 472.747 413.124C472.797 409.676 471.96 406.272 470.317 403.24L470.346 403.26Z"/>
<path fill="currentColor" d="M222.447 481.12C229.872 486.373 238.414 489.832 247.402 491.225C256.39 492.619 265.579 491.908 274.246 489.15C282.913 486.392 290.822 481.661 297.351 475.329C303.881 468.998 308.853 461.238 311.876 452.66H200.987C205.035 464.132 212.53 474.073 222.447 481.12Z"/>
</svg>
{/if}
</div>
{/if}
<div id="router"></div>
<Alert bind:this={alert} />
</div>
<style lang="scss">
.entry {
display: flex;
align-items: center;
height: 350px;
width: 250px;
background-color: #242424;
color: aliceblue;
font-family: 'Roboto', sans-serif;
font-size: 1.2rem;
font-weight: 300;
text-align: center;
letter-spacing: 0.1rem;
overflow: hidden;
flex-direction: column;
padding: 0.81rem;
box-shadow: inset 0 0px 1px 4px #161616;
}
.white {
color: white;
}
.red {
color: rgba(156, 39, 39, 0.733);
}
#router {
overflow-y: auto;
overflow-x: hidden;
margin-top: 2.2rem;
height: 100%;
width: 270px;
}
.headerMenu, .logo {
padding: 0.4rem 0.4rem;
position: absolute;
}
.headerMenu {
right: 0.3rem;
top: 0.2rem;
}
.logo {
left: 0rem;
top: -0.8rem;
width: 100%;
box-shadow: inset 3px -3px 6rem 11px #0000005e;
border-bottom: 1px solid #d3d9df63;
}
</style>

79
src/pages/Info.svelte Normal file
View File

@ -0,0 +1,79 @@
<script lang="ts">
import { onMount } from 'svelte';
import PageLoader from '@/components/PageLoader.svelte';
import { getExtensionVersion, extrenalNavigate } from '@/utils/chrome-misc';
let loading = true;
let version = '';
const extensionSourceLink = 'https://github.com/andrei0x309/yup-live-chrome-extension'
const yupLiveSourceLink = 'https://github.com/andrei0x309/yup-live'
const yupLiveLink = 'https://yup-live.pages.dev'
const yupAppLink = 'https://app.yup.io'
const discordLink = 'https://discord.com/invite/HnaTAXK'
const yupForumLink = 'https://forum.yup.io'
const yupDocsLink = 'https://docs.yup.io'
onMount(async () => {
version = getExtensionVersion();
loading = false;
});
</script>
{#if loading}
<PageLoader />
{:else}
<p class="text-[1.15rem] mt-2 mb-1">About Extension</p>
<p class="text-[0.95rem] mt-1 mb-6">Version: <b>{version}</b></p>
<div class="text-[0.85rem] text-left">
<p class="text-[1.15rem] mt-1 mb-1 ml-4">External Links</p>
<ul class="list-none">
<li><svg class="w-5 falign" viewBox="0 0 512 512"><g><path fill="#fff" class="st0" d="M256,32C132.3,32,32,134.8,32,261.7c0,101.5,64.2,187.5,153.2,217.9c11.2,2.1,15.3-5,15.3-11.1 c0-5.5-0.2-19.9-0.3-39.1c-62.3,13.9-75.5-30.8-75.5-30.8c-10.2-26.5-24.9-33.6-24.9-33.6c-20.3-14.3,1.5-14,1.5-14 c22.5,1.6,34.3,23.7,34.3,23.7c20,35.1,52.4,25,65.2,19.1c2-14.8,7.8-25,14.2-30.7c-49.7-5.8-102-25.5-102-113.5 c0-25.1,8.7-45.6,23-61.6c-2.3-5.8-10-29.2,2.2-60.8c0,0,18.8-6.2,61.6,23.5c17.9-5.1,37-7.6,56.1-7.7c19,0.1,38.2,2.6,56.1,7.7 c42.8-29.7,61.5-23.5,61.5-23.5c12.2,31.6,4.5,55,2.2,60.8c14.3,16.1,23,36.6,23,61.6c0,88.2-52.4,107.6-102.3,113.3 c8,7.1,15.2,21.1,15.2,42.5c0,30.7-0.3,55.5-0.3,63c0,6.1,4,13.3,15.4,11C415.9,449.1,480,363.1,480,261.7 C480,134.8,379.7,32,256,32z"/></g></svg>
<span aria-hidden class="link" on:click={() => extrenalNavigate(extensionSourceLink)} >Extension Source code
</span></li>
<li><svg class="w-5 falign" viewBox="0 0 512 512"><g><path fill="#fff" class="st0" d="M256,32C132.3,32,32,134.8,32,261.7c0,101.5,64.2,187.5,153.2,217.9c11.2,2.1,15.3-5,15.3-11.1 c0-5.5-0.2-19.9-0.3-39.1c-62.3,13.9-75.5-30.8-75.5-30.8c-10.2-26.5-24.9-33.6-24.9-33.6c-20.3-14.3,1.5-14,1.5-14 c22.5,1.6,34.3,23.7,34.3,23.7c20,35.1,52.4,25,65.2,19.1c2-14.8,7.8-25,14.2-30.7c-49.7-5.8-102-25.5-102-113.5 c0-25.1,8.7-45.6,23-61.6c-2.3-5.8-10-29.2,2.2-60.8c0,0,18.8-6.2,61.6,23.5c17.9-5.1,37-7.6,56.1-7.7c19,0.1,38.2,2.6,56.1,7.7 c42.8-29.7,61.5-23.5,61.5-23.5c12.2,31.6,4.5,55,2.2,60.8c14.3,16.1,23,36.6,23,61.6c0,88.2-52.4,107.6-102.3,113.3 c8,7.1,15.2,21.1,15.2,42.5c0,30.7-0.3,55.5-0.3,63c0,6.1,4,13.3,15.4,11C415.9,449.1,480,363.1,480,261.7 C480,134.8,379.7,32,256,32z"/></g></svg>
<span aria-hidden class="link" on:click={() => extrenalNavigate(yupLiveSourceLink)} >Yup Live Source code
</span></li>
<li><svg class="w-5 falign" viewBox="0 0 24 24"><title/><path d="M17.3,13.35a1,1,0,0,1-.7-.29,1,1,0,0,1,0-1.41l2.12-2.12a2,2,0,0,0,0-2.83L17.3,5.28a2.06,2.06,0,0,0-2.83,0L12.35,7.4A1,1,0,0,1,10.94,6l2.12-2.12a4.1,4.1,0,0,1,5.66,0l1.41,1.41a4,4,0,0,1,0,5.66L18,13.06A1,1,0,0,1,17.3,13.35Z" fill="#fff"/><path d="M8.11,21.3a4,4,0,0,1-2.83-1.17L3.87,18.72a4,4,0,0,1,0-5.66L6,10.94A1,1,0,0,1,7.4,12.35L5.28,14.47a2,2,0,0,0,0,2.83L6.7,18.72a2.06,2.06,0,0,0,2.83,0l2.12-2.12A1,1,0,1,1,13.06,18l-2.12,2.12A4,4,0,0,1,8.11,21.3Z" fill="#fff"/><path d="M8.82,16.18a1,1,0,0,1-.71-.29,1,1,0,0,1,0-1.42l6.37-6.36a1,1,0,0,1,1.41,0,1,1,0,0,1,0,1.42L9.52,15.89A1,1,0,0,1,8.82,16.18Z" fill="#fff"/></svg>
<span aria-hidden class="link" on:click={() => extrenalNavigate(yupLiveLink)} >Yup Live Website
</span></li>
<li><svg class="w-5 falign" viewBox="0 0 640 512" ><path fill="#fff" d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"/></svg>
<span aria-hidden class="link" on:click={() => extrenalNavigate(discordLink)} >Discord community
</span></li>
<li><svg class="w-5 falign" viewBox="0 0 24 24"><title/><path d="M17.3,13.35a1,1,0,0,1-.7-.29,1,1,0,0,1,0-1.41l2.12-2.12a2,2,0,0,0,0-2.83L17.3,5.28a2.06,2.06,0,0,0-2.83,0L12.35,7.4A1,1,0,0,1,10.94,6l2.12-2.12a4.1,4.1,0,0,1,5.66,0l1.41,1.41a4,4,0,0,1,0,5.66L18,13.06A1,1,0,0,1,17.3,13.35Z" fill="#fff"/><path d="M8.11,21.3a4,4,0,0,1-2.83-1.17L3.87,18.72a4,4,0,0,1,0-5.66L6,10.94A1,1,0,0,1,7.4,12.35L5.28,14.47a2,2,0,0,0,0,2.83L6.7,18.72a2.06,2.06,0,0,0,2.83,0l2.12-2.12A1,1,0,1,1,13.06,18l-2.12,2.12A4,4,0,0,1,8.11,21.3Z" fill="#fff"/><path d="M8.82,16.18a1,1,0,0,1-.71-.29,1,1,0,0,1,0-1.42l6.37-6.36a1,1,0,0,1,1.41,0,1,1,0,0,1,0,1.42L9.52,15.89A1,1,0,0,1,8.82,16.18Z" fill="#fff"/></svg>
<span aria-hidden class="link" on:click={() => extrenalNavigate(yupForumLink)} >Yup Forum
</span></li>
<li><svg class="w-5 falign" viewBox="0 0 24 24"><title/><path d="M17.3,13.35a1,1,0,0,1-.7-.29,1,1,0,0,1,0-1.41l2.12-2.12a2,2,0,0,0,0-2.83L17.3,5.28a2.06,2.06,0,0,0-2.83,0L12.35,7.4A1,1,0,0,1,10.94,6l2.12-2.12a4.1,4.1,0,0,1,5.66,0l1.41,1.41a4,4,0,0,1,0,5.66L18,13.06A1,1,0,0,1,17.3,13.35Z" fill="#fff"/><path d="M8.11,21.3a4,4,0,0,1-2.83-1.17L3.87,18.72a4,4,0,0,1,0-5.66L6,10.94A1,1,0,0,1,7.4,12.35L5.28,14.47a2,2,0,0,0,0,2.83L6.7,18.72a2.06,2.06,0,0,0,2.83,0l2.12-2.12A1,1,0,1,1,13.06,18l-2.12,2.12A4,4,0,0,1,8.11,21.3Z" fill="#fff"/><path d="M8.82,16.18a1,1,0,0,1-.71-.29,1,1,0,0,1,0-1.42l6.37-6.36a1,1,0,0,1,1.41,0,1,1,0,0,1,0,1.42L9.52,15.89A1,1,0,0,1,8.82,16.18Z" fill="#fff"/></svg>
<span aria-hidden class="link" on:click={() => extrenalNavigate(yupAppLink)} >Yup App
</span></li>
<li><svg class="w-5 falign" viewBox="0 0 24 24"><title/><path d="M17.3,13.35a1,1,0,0,1-.7-.29,1,1,0,0,1,0-1.41l2.12-2.12a2,2,0,0,0,0-2.83L17.3,5.28a2.06,2.06,0,0,0-2.83,0L12.35,7.4A1,1,0,0,1,10.94,6l2.12-2.12a4.1,4.1,0,0,1,5.66,0l1.41,1.41a4,4,0,0,1,0,5.66L18,13.06A1,1,0,0,1,17.3,13.35Z" fill="#fff"/><path d="M8.11,21.3a4,4,0,0,1-2.83-1.17L3.87,18.72a4,4,0,0,1,0-5.66L6,10.94A1,1,0,0,1,7.4,12.35L5.28,14.47a2,2,0,0,0,0,2.83L6.7,18.72a2.06,2.06,0,0,0,2.83,0l2.12-2.12A1,1,0,1,1,13.06,18l-2.12,2.12A4,4,0,0,1,8.11,21.3Z" fill="#fff"/><path d="M8.82,16.18a1,1,0,0,1-.71-.29,1,1,0,0,1,0-1.42l6.37-6.36a1,1,0,0,1,1.41,0,1,1,0,0,1,0,1.42L9.52,15.89A1,1,0,0,1,8.82,16.18Z" fill="#fff"/></svg>
<span aria-hidden class="link" on:click={() => extrenalNavigate(yupDocsLink)} >Yup Docs
</span></li>
</ul>
</div>
{/if}
<style lang="scss">
.link {
color: #3b82f6;
cursor: pointer;
}
.link:hover {
transform: scale(1.05);
}
.link:active {
transform: scale(0.95);
}
.falign {
position: relative;
top: 5px;
}
</style>

24
src/pages/Login.svelte Normal file
View File

@ -0,0 +1,24 @@
<script lang="ts">
import { extrenalNavigate } from "@/utils/chrome-misc";
import { APP_BASE } from '@/constants/config'
</script>
<a href="#app" on:click={() => extrenalNavigate(`${APP_BASE}/login`)}>
<svg enable-background="new 0 0 32 32" viewBox="0 0 32 32" class="w-20 mb-10 mt-8 svg-fill">
<g id="lock"><path d="M25,13V9c0-4.971-4.029-9-9-9c-4.971,0-9,4.029-9,9v4c-1.657,0-3,1.343-3,3v3v1v2v1c0,4.971,4.029,9,9,9h6 c4.971,0,9-4.029,9-9v-1v-2v-1v-3C28,14.342,26.656,13,25,13z M9,9c0-3.866,3.134-7,7-7c3.866,0,7,3.134,7,7v4h-2V9.002 c0-2.762-2.238-5-5-5c-2.762,0-5,2.238-5,5V13H9V9z M20,9v0.003V13h-8V9.002V9c0-2.209,1.791-4,4-4C18.209,5,20,6.791,20,9z M26,19 v1v2v1c0,3.859-3.141,7-7,7h-6c-3.859,0-7-3.141-7-7v-1v-2v-1v-3c0-0.552,0.448-1,1-1c0.667,0,1.333,0,2,0h14c0.666,0,1.332,0,2,0 c0.551,0,1,0.448,1,1V19z"/>
<path d="M16,19c-1.104,0-2,0.895-2,2c0,0.607,0.333,1.76,0.667,2.672c0.272,0.742,0.614,1.326,1.333,1.326 c0.782,0,1.061-0.578,1.334-1.316C17.672,22.768,18,21.609,18,21C18,19.895,17.104,19,16,19z"/></g>
</svg>
</a>
<div class="text-[1.6rem] mb-4 uppercase">
<a href="#app" on:click="{() => extrenalNavigate(`${APP_BASE}/login`)}">
Login
</a>
/
<a href="#app" on:click="{() => extrenalNavigate(`${APP_BASE}/sign-up`)}" >
Signup
</a>
</div>
<a href="#app" on:click="{() => extrenalNavigate(`${APP_BASE}/login`)}">
<svg enable-background="new 0 0 48 48" viewBox="0 0 48 48" class="w-18 svg-fill my-4"><path d="M0.115,30.348c0-7.771,6.303-14.073,14.074-14.073h0.002h14.071V8.051l19.622,15.261l-19.622,15.26v-8.225 H10.458c-3.887,0-7.037,3.152-7.037,7.037c0,0.906,0.186,1.768,0.5,2.564C1.566,37.434,0.115,34.064,0.115,30.348z"/></svg>
</a>

160
src/pages/Main.svelte Normal file
View File

@ -0,0 +1,160 @@
<script lang="ts">
// import { storage } from "@/storage";
import { extrenalNavigate } from "@/utils/chrome-misc";
import ImgLoader from "@/components/ImgLoader.svelte";
import { onMount } from "svelte";
// https://yup-live.pages.dev
import { APP_BASE } from "@/constants/config";
import type { StorageType } from "@/utils/storage";
import RateWebsite from "@/components/RateWebsite.svelte";
import { mainStore } from "@/utils/store";
import { formatNumber, truncteEVMAddr } from "@/utils/misc";
import { alertStore } from "@/utils/store";
import { copy } from "@/utils/chrome-misc";
let avatar: string = "";
let loader: ImgLoader;
let handle = $mainStore?.user?.profile?.handle;
const copyAddress = () => {
copy($mainStore?.user?.auth?.address);
$alertStore?.show("Copied to clipboard", "success");
};
onMount(async () => {
console.log($mainStore);
if ($mainStore?.user?.profile?.yup?.avatar && !$mainStore?.user?.profile?.yup?.avatar.endsWith(".mp4")) {
avatar = $mainStore?.user?.profile?.yup?.avatar;
} else if ($mainStore?.user?.profile?.lens?.avatar && !$mainStore?.user?.profile?.lens?.avatar.endsWith(".mp4")) {
avatar = $mainStore?.user?.profile?.lens?.avatar;
} else if (
$mainStore?.user?.profile?.farcaster?.avatar &&
!$mainStore?.user?.profile?.farcaster?.avatar.endsWith(".mp4")
) {
avatar = $mainStore?.user?.profile?.farcaster?.avatar;
}
});
</script>
<div class="h-24 leading-6 main-section">
<div class="flex">
<div on:click={() => extrenalNavigate(`${APP_BASE}/score/${$mainStore.user.auth.address}`)} aria-hidden class="flex flex-col w-16 mt-1 px-2 py-3 mr-4 link">
<span class="text-[0.6rem] mb-2">Score</span><span class="text-[0.95rem] mb-2"
>{$mainStore?.user?.profile?.yupScore?.toFixed(0)}</span
><span class="text-[0.7rem]">100<br />MAX</span>
</div>
<div on:click={() => extrenalNavigate(`${APP_BASE}/profile/${$mainStore.user.auth.userId}`)} aria-hidden class="flex flex-col justify-center mb-2 w-16">
<ImgLoader source={avatar} bind:this={loader}>
<img
style="{ $mainStore.settings.theme === 'light'? 'filter: invert(1);' : '' }"
slot="img"
src={avatar}
alt="avatar"
class="h-14 w-14 avatar"
on:load={() => loader.onLoad()}
on:error={() => loader.onError()}
/>
<svg slot="error" class="h-14 w-14 avatar" viewBox="-27 24 100 100" style="{ $mainStore.settings.theme === 'light'? 'filter: invert(0.9);' : '' }"
><g
><g
><rect fill="#F5EEE5" x="-27" y="24" /><g
><defs
><path
d="M36,95.9c0,4,4.7,5.2,7.1,5.8c7.6,2,22.8,5.9,22.8,5.9c3.2,1.1,5.7,3.5,7.1,6.6v9.8H-27v-9.8 c1.3-3.1,3.9-5.5,7.1-6.6c0,0,15.2-3.9,22.8-5.9c2.4-0.6,7.1-1.8,7.1-5.8c0-4,0-10.9,0-10.9h26C36,85,36,91.9,36,95.9z"
id="shoulders"
/></defs
><use fill="#E6C19C" overflow="visible" xlink:href="#shoulders" /><clipPath id="shoulders_1_"
><use overflow="visible" xlink:href="#shoulders" /></clipPath
><path
clip-path="url(#shoulders_1_)"
d="M23.2,35c0.1,0,0.1,0,0.2,0c0,0,0,0,0,0 c3.3,0,8.2,0.2,11.4,2c3.3,1.9,7.3,5.6,8.5,12.1c2.4,13.7-2.1,35.4-6.3,42.4c-4,6.7-9.8,9.2-13.5,9.4c0,0-0.1,0-0.1,0 c-0.1,0-0.1,0-0.2,0c-0.1,0-0.1,0-0.2,0c0,0-0.1,0-0.1,0c-3.7-0.2-9.5-2.7-13.5-9.4c-4.2-7-8.7-28.7-6.3-42.4 c1.2-6.5,5.2-10.2,8.5-12.1c3.2-1.8,8.1-2,11.4-2c0,0,0,0,0,0C23.1,35,23.1,35,23.2,35L23.2,35z"
fill="#D4B08C"
id="head-shadow"
/></g
></g
><path
d="M22.6,40c19.1,0,20.7,13.8,20.8,15.1c1.1,11.9-3,28.1-6.8,33.7c-4,5.9-9.8,8.1-13.5,8.3 c-0.2,0-0.2,0-0.3,0c-0.1,0-0.1,0-0.2,0C18.8,96.8,13,94.6,9,88.7c-3.8-5.6-7.9-21.8-6.8-33.8C2.3,53.7,3.5,40,22.6,40z"
fill="#F2CEA5"
id="head"
/></g
></svg
>
</ImgLoader>
{#if handle}
<span class="text-[0.6rem] mt-4 -ml-1">{handle.length >= 12 ? handle.slice(0, 10) + "..." : handle}</span>
{/if}
</div>
<div on:click={() => extrenalNavigate(`${APP_BASE}/profile/${$mainStore.user.auth.userId}`)} aria-hidden class="flex flex-col w-16 mt-1 ml-4 px-2 py-3 link">
<span class="text-[0.6rem] mb-2">Influence</span><span class="text-[0.95rem] mb-2"
>{$mainStore?.user?.profile?.yup?.weight}</span
><span class="text-[0.7rem]">10<br />MAX</span>
</div>
</div>
</div>
<div class="address text-[0.8rem]">
<span
class="mb-2"
>Address: {truncteEVMAddr($mainStore?.user?.auth?.address)}
<svg on:click={() => copyAddress()} aria-hidden="true" class="w-4 interactive-svg" viewBox="0 0 24 24"
><path
fill="#eee"
d="M14 8H4c-1.103 0-2 .897-2 2v10c0 1.103.897 2 2 2h10c1.103 0 2-.897 2-2V10c0-1.103-.897-2-2-2z"
/><path fill="#eee" d="M20 2H10a2 2 0 0 0-2 2v2h8a2 2 0 0 1 2 2v8h2a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z" /></svg
></span
>
<span>Balance: {formatNumber($mainStore?.user?.profile.yup.balance, 2)}
{#if $mainStore?.user?.profile.yup.balance > 0 && $mainStore?.settings?.coinGeckoPrice > 0}
<span class="text-[0.6rem] opacity-70 ml-1">${formatNumber($mainStore?.user?.profile.yup.balance * $mainStore?.settings.coinGeckoPrice, 2)}</span>
{/if}
</span>
</div>
<RateWebsite />
<style lang="scss">
.address {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 0.5rem;
border: 1px solid #2c2c2c;
box-shadow: inset -1px 0px 18px 0px #0000004f;
background: #1a1a1a;
height: 3.4rem;
width: 130%;
overflow: hidden;
margin-left: -2rem;
margin-bottom: 0.2rem;
}
.avatar {
border: 2px solid #d3d9df1a;
box-shadow: 0px 0px 0px 4px #0000004f;
transition: all .4s ease-in-out;
border-radius: 8px;
cursor: pointer;
}
.link {
cursor: pointer;
transition: all 0.4s ease-in-out;
border-radius: 6px;
background-color: #00000027;
}
.avatar:hover,
.link:hover {
color: #fffee5;
background-color: #0000004f;
}
.link:hover {
border: 1px solid #d3d9df73;
}
.avatar:hover {
border: 2px solid #d3d9df73;
}
</style>

View File

@ -0,0 +1,89 @@
<script lang="ts">
import { onMount } from 'svelte';
import { getNotifStorage, setSettings } from '@/utils/storage'
import { mainStore } from '@/utils/store'
import PageLoader from '@/components/PageLoader.svelte';
import { getNotifications } from '@/utils/notifications';
import Notification from '@/components/Notification.svelte';
import { clearNotifications } from '@/utils/notifications';
import { clearBadge } from '@/utils/chrome-misc'
let loading = true;
let noNotifications = false;
let notifs = [];
let pastNotifsPromise
let type = 'all'
onMount(async () => {
notifs = await getNotifications({
userId: $mainStore.user.auth.userId,
type,
skip: '0',
limit: '15'
})
pastNotifsPromise = getNotifStorage()
console.log(notifs);
loading = false;
clearNotifications($mainStore).then(
() => {
clearBadge()
$mainStore.settings.hasNewNotifications = false
setSettings($mainStore.settings)
}
)
});
const changeNotifsType = async (t :string) => {
if(type === t) return;
if(!['all', 'rewards'].includes(t)){
console.error('Invalid type');
return;
}
type = t;
loading = true;
notifs = await getNotifications({
userId: $mainStore.user.auth.userId,
type: t,
skip: '0',
limit: '15'
})
loading = false;
}
</script>
{#if loading}
<PageLoader />
{:else if notifs.length === 0}
{#await pastNotifsPromise}
&nbsp;
{:then pastNotifs}
{#if (pastNotifs.notifs.reverse() ?? []).length > 0}
{noNotifications = true}
{:else}
{#each pastNotifs.notifs.reverse() as notif}
<Notification {notif} />
{/each}
{/if}
{/await}
{:else}
<div class="text-[0.75rem] py-1">
<span on:click={() => changeNotifsType('all')} aria-hidden class="inline-block mr-2 interactive-svg text-blue-200 interactive-svg" >All</span>
<span on:click={() => changeNotifsType('rewards')} aria-hidden class="text-blue-200 interactive-svg interactive-svg text-blue-200 interactive-svg">Rewards</span>
</div>
<div class="flex flex-col">
{#each notifs.reverse() as notif}
<Notification {notif} />
{/each}
</div>
{/if}
{#if noNotifications }
<div class="flex flex-col items-center justify-center h-full">
<p class="text-2xl font-semibold">No Notifications</p>
<p class="text-lg">You have no notifications</p>
</div>
{/if}

149
src/pages/Settings.svelte Normal file
View File

@ -0,0 +1,149 @@
<script lang="ts">
import '@/popup/scss/settings.scss';
import { wipeStorage, setSettings } from "@/utils/storage";
import { mainStore } from '@/utils/store'
import { navigate } from "@/utils/router";
import { storageDefault } from '@/utils/storage'
import { onMount } from "svelte";
import PageLoader from "@/components/PageLoader.svelte";
import { clearBadge, reloadExtension } from '@/utils/chrome-misc';
let settings: typeof $mainStore['settings'] = null;
let settingLoading = false;
const logout = () => {
wipeStorage().then(async () => {
await Promise.all([mainStore.set(storageDefault),
clearBadge(),
navigate("/login")]);
reloadExtension()
});
};
const setSettingsLocal = async (setting: string, value = '') => {
settingLoading = true;
if(value) $mainStore.settings[setting] = value;
else {
$mainStore.settings[setting] = !$mainStore.settings[setting];
}
settings = $mainStore.settings;
await setSettings($mainStore.settings);
settingLoading = false;
};
onMount(async () => {
settings = $mainStore.settings
console.log(settings)
});
</script>
{#if settings}
<button on:click={() => logout() } class="logout" >
<svg class="w-5" viewBox="0 0 24 24" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id=""/><g id=""><g>
<path fill="#fff" d="M20.9,11.6c-0.1-0.1-0.1-0.2-0.2-0.3l-3-3c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l1.3,1.3H13c-0.6,0-1,0.4-1,1s0.4,1,1,1h4.6 l-1.3,1.3c-0.4,0.4-0.4,1,0,1.4c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3l3-3c0.1-0.1,0.2-0.2,0.2-0.3C21,12.1,21,11.9,20.9,11.6z "/>
<path fill="#fff" d="M15.5,18.1C14.4,18.7,13.2,19,12,19c-3.9,0-7-3.1-7-7s3.1-7,7-7c1.2,0,2.4,0.3,3.5,0.9c0.5,0.3,1.1,0.1,1.4-0.4 c0.3-0.5,0.1-1.1-0.4-1.4C15.1,3.4,13.6,3,12,3c-5,0-9,4-9,9s4,9,9,9c1.6,0,3.1-0.4,4.5-1.2c0.5-0.3,0.6-0.9,0.4-1.4 C16.6,18,16,17.8,15.5,18.1z"/>
</g></g>
</svg> Logout
</button>
<section class="switches-settings flex flex-col w-full justify-center" data-theme="blue">
<h2 class="text-1xl font-bold">Settings</h2>
<div class="flex flex-col">
<div class="switch switch--4 text-[0.8rem] flex flex-col mb-4">
<span class="inline-block">Enable Notifications</span>
<label aria-hidden class="switch__label mt-2">
<input type="checkbox" class="switch__input"
on:click={() => setSettingsLocal('notificationsEnabled')}
checked={settings.notificationsEnabled}
>
<span class="switch__design"></span>
</label>
</div>
<div class="switch switch--4 text-[0.8rem] flex flex-col mb-4">
<span class="inline-block switch-txt">Light Theme</span>
<label class="switch__label mt-2">
<input type="checkbox" class="switch__input"
on:click={() => setSettingsLocal('theme', settings.theme === 'light' ? 'dark' : 'light' )}
checked={settings.theme === 'light'}
>
<span class="switch__design"></span>
</label>
</div>
<div class="switch switch--4 text-[0.8rem] flex flex-col mb-4">
<span class="inline-block switch-txt">Inject overlay for all websites</span>
<label class="switch__label mt-2">
<input type="checkbox" class="switch__input"
on:click={() => setSettingsLocal('injectEmbed')}
checked={settings.injectEmbed}
>
<span class="switch__design"></span>
</label>
</div>
<div class="switch switch--4 text-[0.8rem] flex flex-col mb-4">
<span class="inline-block">Browser notification at reward</span>
<label aria-hidden class="switch__label mt-2">
<input type="checkbox" class="switch__input"
on:click={() => setSettingsLocal('chromeNotifWhenReward')}
checked={settings.chromeNotifWhenReward}
>
<span class="switch__design"></span>
</label>
</div>
<div class="switch switch--4 text-[0.8rem] flex flex-col mb-4">
<span class="inline-block">Browser notification actions refill</span>
<label class="switch__label mt-2">
<input on:click={() => setSettingsLocal('chromeNotifWhenAbleToVote')} type="checkbox" class="switch__input"
checked={settings.chromeNotifWhenAbleToVote}
>
<span class="switch__design"></span>
</label>
</div>
</div>
</section>
{:else}
<PageLoader />
{/if}
<style lang="scss">
.switch-txt{
top: -0.4rem;
}
.logout {
background: none;
border: 1px solid #160e0e;
box-shadow: inset 1px 1px 16px 3px #004985d1;
font-size: 0.9rem;
font-weight: 600;
padding: 0;
margin: 0;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.2rem;
color: #f0f8ff;
text-transform: uppercase;
padding: 0.4rem 1rem;
margin: auto;
margin-top: 1rem;
border-radius: 0.3rem;
}
.logout:hover {
transform: scale(1.1);
}
.logout:active {
transform: scale(0.9);
}
</style>

View File

104
src/pages/Usage.svelte Normal file
View File

@ -0,0 +1,104 @@
<script lang="ts">
import { onMount } from 'svelte';
import { createActionUsage } from '@/utils/user';
import { mainStore } from '@/utils/store';
import { getTimeRemaining } from '@/utils/time';
import PageLoader from '@/components/PageLoader.svelte';
let isReset = false;
let hoursSpan = '0'
let minutesSpan = '0'
let secondsSpan = '0'
let data: Awaited<ReturnType<typeof createActionUsage>> = null
function initializeClock(endtime) {
function updateClock() {
var t = getTimeRemaining(endtime);
hoursSpan = ('0' + t.hours).slice(-2);
minutesSpan = ('0' + t.minutes).slice(-2);
secondsSpan = ('0' + t.seconds).slice(-2);
if (t.total <= 0) {
clearInterval(timeinterval);
isReset = true;
}
}
updateClock();
var timeinterval = setInterval(updateClock, 1000);
}
onMount(async () => {
data = await createActionUsage($mainStore.user.auth.userId, $mainStore.user.profile.yup.balance);
const resetDate = new Date(data.nextReset)
isReset = getTimeRemaining(resetDate).total <= 0;
if(!isReset) initializeClock(resetDate);
});
</script>
{#if data}
<div class="flex flex-col">
<p class="semi-bold text-[1.05rem]">Likes Remaining:</p>
<p class="semi-bold text-[0.95rem] my-1">{data.actionBars.vote}</p>
<p class="semi-bold text-[1.05rem]">Follows Remaining:</p>
<p class="semi-bold text-[0.95rem] my-1">{data.actionBars.follow}</p>
</div>
{#if !isReset}
<p class="semi-bold text-[1.15rem]">Refill Coutdown</p>
<div id="clockdiv">
<div>
<span class="hours">{hoursSpan}</span>
<div class="smalltext">Hours</div>
</div>
<div><span class="minutes">{minutesSpan}</span>
<div class="smalltext">Minutes</div>
</div>
<div><span class="seconds">{secondsSpan}</span>
<div class="smalltext">Seconds</div>
</div>
</div>
{:else}
<p></p>
<p class="semi-bold text-[1.05rem]">There have been more than 24h since your last reset.
<span class="mt-2 block">You can now reset your actions by doing a rating.</span></p>
{/if}
{:else}
<PageLoader />
{/if}
<style lang="scss">
#clockdiv {
color: aliceblue;
display: inline-block;
font-weight: 100;
text-align: center;
font-size: 1rem;
}
#clockdiv > div {
padding: 10px;
border-radius: 3px;
background: #00000057;
display: inline-block;
}
#clockdiv div > span {
padding: 15px;
border-radius: 3px;
background: #3b45917a;
display: inline-block;
}
.smalltext {
padding-top: 5px;
font-size: 0.77rem
}
</style>

13
src/popup/index.ts Normal file
View File

@ -0,0 +1,13 @@
import "./popup.scss"
import Entry from "@/pages/Entry.svelte";
// import { storage } from "src/storage";
const target = document.getElementById("app");
function render() {
// storage.get().then(({ count }) => {
new Entry({target});
// });
}
document.addEventListener("DOMContentLoaded", render);

11
src/popup/popup.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Popup</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./index.ts"></script>
</body>
</html>

158
src/popup/popup.scss Normal file
View File

@ -0,0 +1,158 @@
::-webkit-scrollbar{
height: 7px;
width: 7px;
background: #747474;
}
::-webkit-scrollbar-thumb {
background: rgb(34, 34, 34);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:horizontal{
background: rgb(34, 34, 34);
border-radius: 4px;
}
html{
scrollbar-width: thin;
scroll-behavior: smooth;
}
@font-face {
@font-face {
font-family: 'Crete Round';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../assets//fonts/55xoey1sJNPjPiv1ZZZrxK110b3wKg.woff2') format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
}
.main-section {
width: 252px;
margin: auto;
}
body {
margin: 0;
font-family: 'Crete Round', serif;
}
a {
color: #e4edff;
font-weight: 300;
letter-spacing: 0.1rem;
transition: color 0.5s ease-in-out;
text-decoration: none;
}
a:hover {
color: #eeb845;
}
.svg-fill {
fill: #e4edff;
transition: fill 0.5s ease-in-out;
}
.svg-fill:hover {
fill: #eeb845;
}
.gradient-text {
background-color: #ca4246;
background-image: linear-gradient(
45deg,
#ca4246 16.666%,
#e16541 16.666%,
#e16541 33.333%,
#f18f43 33.333%,
#f18f43 50%,
#8b9862 50%,
#8b9862 66.666%,
#476098 66.666%,
#476098 83.333%,
#a7489b 83.333%
);
background-size: 100%;
background-repeat: repeat;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: rainbow-text-simple-animation-rev 0.75s ease forwards;
}
.gradient-text:hover {
animation: rainbow-text-simple-animation 0.5s ease-in forwards;
}
@keyframes rainbow-text-simple-animation-rev {
0% {
background-size: 650%;
}
40% {
background-size: 650%;
}
100% {
background-size: 100%;
}
}
@keyframes rainbow-text-simple-animation {
0% {
background-size: 100%;
}
80% {
background-size: 650%;
}
100% {
background-size: 650%;
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes spin-reverse {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-360deg);
}
}
h1 {
font-weight: normal;
font-size: 1.8rem;
text-align: center;
margin-bottom: 0;
display: block;
margin-left: auto;
margin-right: auto;
cursor: pointer;
}
.interactive-svg {
cursor: pointer;
}
.interactive-svg:hover {
transform: scale(1.2);
transition: 0.2s;
}
.interactive-svg:active {
transform: scale(1.1);
transition: 0.2s;
}

View File

@ -0,0 +1,121 @@
.switches-settings {
&[data-theme="green"] {
--primary-light: hsl(160, 79%, 46%);
--primary-dark: hsl(160, 79%, 16%);
--ripple: hsla(160, 79%, 46%, .1);
--focus: hsl(160, 69%, 46%);
}
&[data-theme="blue"] {
--primary-light: hsl(200, 79%, 46%);
--primary-dark: hsl(200, 79%, 16%);
--ripple: hsla(200, 79%, 46%, .1);
--focus: var(--primary-light);
}
& {
display: flex;
align-items: center;
margin-top: 0.8rem;
}
}
// main
// ==========================================================
.switch {
&__label {
position: relative;
cursor: pointer;
}
&__input {
opacity: 0;
position: absolute;
top: 50%;
left: 50%;
z-index: -1;
transform: translate(-50%, -50%);
}
&__input:focus+&__design {
box-shadow: 0 0 0 .1rem var(--global-background), 0 0 0 .2rem var(--focus);
}
// variables
// ----------------------------------------------------
& {
--width: 2.6rem;
--height: 1.4rem;
--background: hsl(0, 0%, 30%);
--checked-background: var(--primary-dark);
--thumb-size: 0.9rem;
--thumb-ripple-color: var(--ripple);
--thumb-background: hsl(0, 0%, 65%);
--checked-thumb-background: var(--primary-light);
--thumb-space-between-edges: .6rem;
--thumb-out: var(--thumb-space-between-edges);
}
&--4 {
--border: 1px solid hsl(0, 0%, 60%);
--background: transparent;
}
// appearance
// ----------------------------------------------------
&__design {
display: inline-block;
width: var(--width);
height: var(--height);
border: var(--border);
border-radius: 100rem;
background: var(--background);
position: relative;
transition: .2s, box-shadow 0s;
}
&__design::before {
content: '';
position: absolute;
left: var(--thumb-out);
top: 50%;
transform: translateY(-50%);
width: var(--thumb-size);
height: var(--thumb-size);
border-radius: 100rem;
background: var(--thumb-background);
transition: inherit;
}
// states
// ----------------------------------------------------
&__input:checked+&__design {
border-color: transparent;
background: var(--checked-background);
}
&__input:checked+&__design::before {
left: calc(100% - (var(--thumb-size) + var(--thumb-out)));
background: var(--checked-thumb-background);
}
}

9
src/utils/auth.ts Normal file
View File

@ -0,0 +1,9 @@
export const fetchWAuth = async (store, endpoint: string, options?: any) => {
if (!options) options = {}
if (!options.headers) options.headers = {}
if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'application/json'
if (!options.headers['Authorization']) options.headers['Authorization'] = 'Bearer ' + store.user.auth.authToken
return fetch(endpoint, options)
}

37
src/utils/chrome-misc.ts Normal file
View File

@ -0,0 +1,37 @@
export const copy = async (text: string) => {
return await navigator.clipboard.writeText(text)
}
export const chromeUrl = (url: string) => {
return chrome.runtime.getURL(url)
}
export const extrenalNavigate = (url) => {
chrome.tabs.create({url})
}
export const setBadge = async (text: string, color = '#222') => {
chrome?.action?.setBadgeBackgroundColor({color})
chrome?.action?.setBadgeText({text})
}
export const clearBadge = async () => {
chrome?.action?.setBadgeBackgroundColor({color: '#00000000'})
chrome?.action?.setBadgeText({text: ''})
}
export const getCurrentTab = () => {
return chrome.tabs.query({
active: true,
lastFocusedWindow: true
})
}
export const getExtensionVersion = () => {
return chrome.runtime.getManifest().version
}
export const reloadExtension = () => {
chrome.runtime.reload()
}

12
src/utils/misc.ts Normal file
View File

@ -0,0 +1,12 @@
export const formatNumber = (num: number, digits = 0) => {
return Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: digits
}).format(num)
}
export const truncteEVMAddr = (addr: string) => ((addr ?? '').length > 4 ? addr.substring(0, 5) + '...' + addr.substring(addr.length - 3) : '')
export const isUrlInvalid = (url: string) => !url ||
!/^http(s)?/gms.test(url) || url.startsWith('http://localhost')
|| /^d+\.d+\.d+\.d+\./gms.test(url) || /^http(s)?:\/\/\[/gms.test(url)

View File

@ -0,0 +1,97 @@
import {API_BASE} from '@/constants/config'
import type { StorageType } from '@/utils/storage'
import { fetchWAuth } from '@/utils/auth'
import { wait } from '@/utils/time'
export const getNotifications = async (
{ type, limit, skip, userId } = { type: 'all', limit: '10', skip: '0' } as { userId: string, type: string; limit?: string; skip?: string }
) => {
let req
if (type === 'all') {
req = await fetch(`${API_BASE}/notifications/${userId}?skip=${skip}&limit=${limit}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
})
} else if (type === 'vote') {
req = await fetch(`${API_BASE}/notifications/${userId}?skip=${skip}&limit=${limit}&type=vote`, {
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
})
} else {
req = await fetch(`${API_BASE}/notifications/${userId}?skip=${skip}&limit=${limit}&type=reward`, {
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
})
}
if (!req.ok) {
return false
}
const data = await req.json()
return data
}
export const clearNotifications = async (store: StorageType) => {
let req
let failed = true
let retry = 0
do {
try {
failed = false
req = await fetchWAuth(store, `${API_BASE}/notifications/${store.user.auth.userId}`)
} catch {
failed = true
await console.log(
`[ Account: ${store.user.auth.userId} ] Failed fetch on 'notifications' (probably 💩 network) Recursive retry in 2.5s`
)
retry++
if (retry > 3) {
await console.log(`[ Account: ${store.user.auth.userId} ] Failed fetch on 'notifications' (probably 💩 network)`)
return
}
await wait(2500)
}
} while (failed)
retry = 0
if (req.ok) {
const notif = await req.json()
for (const n of notif) {
if (!n.seen) {
const data = {
id: n['_id']
}
failed = true
do {
try {
failed = false
await fetchWAuth(store, `${API_BASE}/notifications/seen`, {
method: 'POST',
body: JSON.stringify(data)
})
} catch {
failed = true
await console.log(
`[ Account: ${store.user.auth.userId} ] Failed to mark notification ${data.id} as seen (probably 💩 network) retry in 2.5 seconds.`
)
retry++
if (retry > 3) {
await console.log(`[ Account: ${store.user.auth.userId} ] Failed fetch on 'notifications seen set' (probably 💩 network)`)
return
}
await wait(2500)
}
} while (failed)
}
}
}
}

53
src/utils/router.ts Normal file
View File

@ -0,0 +1,53 @@
const routes = {
'/': {
component: () => import('@/pages/Main.svelte'),
title: 'Main'
},
'/login': {
component: () => import('@/pages/Login.svelte'),
title: 'Login'
},
'/usage' : {
component: () => import('@/pages/Usage.svelte'),
title: 'Usage'
},
'/settings' : {
component: () => import('@/pages/Settings.svelte'),
title: 'Settings'
},
'/notifications' : {
component: () => import('@/pages/Notifications.svelte'),
title: 'Notifications'
},
'/info': {
component: () => import('@/pages/Info.svelte'),
title: 'Info'
}
}
let lastMountedRoute = null
export const getCurentRouteComponent = () => {
const path = window.location.pathname
return routes[path]
}
export const getCurrentRoutePath = () => {
return window.location.pathname
}
export const navigate = (path: string, props: {} = {}) => {
new Promise<void>((resolve) => {
window.history.pushState({}, '', path)
const { component, title } = getCurentRouteComponent()
component().then(({ default: Component }) => {
console.log({ target: document.getElementById('router'), props })
if (lastMountedRoute) lastMountedRoute.$destroy()
lastMountedRoute = new Component({ target: document.getElementById('router'), props })
document.title = title
resolve()
})
})
}

137
src/utils/storage.ts Normal file
View File

@ -0,0 +1,137 @@
import { SEND_AUTH_NOTIF } from '@/constants/messeges'
export const storageDefault = {
user: {
auth: {
ethSignature: '',
userId: '',
address: '',
authToken: '',
username: '',
},
profile: {
_id: '',
handle: '',
yupScore: 0,
avatar: '',
yup: {
_id: '',
handle: '',
bio: '',
avatar: '',
weight: 0,
balance: 0,
},
lens: {
avatar: '',
profileId: '',
handle: '',
},
farcaster: {
fid: '',
handle: '',
avatar: '',
},
ens: {
handle: '',
},
}
},
settings: {
theme: 'dark',
notificationsEnabled: false,
injectEmbed: false,
chromeNotifWhenReward: false,
chromeNotifWhenAbleToVote: false,
coinGeckoPrice: 0,
hasNewNotifications: false,
refilNotifTimestamp: 0,
}
}
export const storageNotifsDefault = {
lastRewardNotif: {
createdAt: 0,
id: '',
},
notifs: []
}
export type StorageType = typeof storageDefault
export const wipeStorage = async () => {
await chrome.storage.local.clear()
}
export const initStorage = async () => {
const store = await chrome.storage.local.get('store')
const updateStore = { store: { ...storageDefault, ...store.store } }
const updateCondition = !store.store || !store.store.user || !store.store.user.auth ||
store.store.user.auth.authToken || !store.store.settings || !store.store.settings.coinGeckoPrice
|| !store.store.user.profile || !store.store.user.profile._id
if(updateCondition) {
await chrome.storage.local.set(updateStore)
console.info('Storage initialized')
}
const notifs = await chrome.storage.local.get('notifs')
const updateNotifs = { ...storageNotifsDefault, ...notifs }
const updateNotifsCondition = !notifs.notifs || !notifs.lastRewardNotif
if(updateNotifsCondition) {
await chrome.storage.local.set({ notifs: updateNotifs })
console.info('Notifs storage initialized')
}
}
export const getNotifStorage = async () => {
const notifs = await chrome.storage.local.get('notifs')
return notifs ? notifs as typeof storageNotifsDefault : storageNotifsDefault as typeof storageNotifsDefault
}
export const setNotifStorageNotifs = async (notifs: any[]) => {
const notifsStorage = await getNotifStorage()
await chrome.storage.local.set({ ...notifsStorage, notifs: notifs })
}
export const setNotifStorageLastRewardNotif = async (lastRewardNotif) => {
const notifsStorage = await getNotifStorage()
await chrome.storage.local.set({ ...notifsStorage, lastRewardNotif: lastRewardNotif })
}
export const setAuth = async (auth) => {
const storeAuth = await chrome.storage.local.get('store')
let profile
try {
const res = await fetch('https://api.yup.io/web3-profiles/' + auth.address)
profile = await res.json()
} catch (error) {
console.error('Error fetching profile', error)
}
const updateObj = { store: { ...storeAuth.store, user: { auth: { ...auth }, } } }
if(profile){
updateObj.store.user.profile = profile
}
await chrome.storage.local.set(updateObj)
chrome.runtime.sendMessage({ type: SEND_AUTH_NOTIF })
}
export const setProfile = async (profile) => {
const store = await chrome.storage.local.get('store')
await chrome.storage.local.set({ store: { ...store.store, user: { ...store.store.user, profile } } })
}
export const setSettings = async (settings) => {
const store = await chrome.storage.local.get('store')
await chrome.storage.local.set({ store: { ...store.store, settings: { ...store.store.settings, ...settings } } })
}
export const getStore = async () => {
const store = await chrome.storage.local.get('store')
return store ? store.store as StorageType : storageDefault as StorageType
}
export const getSettings = async () => {
const store = await chrome.storage.local.get('store')
return store ? store.store.settings as StorageType['settings'] : storageDefault.settings as StorageType['settings']
}

7
src/utils/store.ts Normal file
View File

@ -0,0 +1,7 @@
import { writable } from 'svelte/store';
import type { StorageType } from './storage';
import { storageDefault } from './storage';
import type Alert from '@/components/Alert.svelte';
export const mainStore = writable<StorageType>(storageDefault)
export const alertStore = writable<Alert>(null)

60
src/utils/time.ts Normal file
View File

@ -0,0 +1,60 @@
export const getTimeRemaining = (endtime) => {
const t = Date.parse(endtime) - Date.parse(new Date().toISOString())
return {
'total': t,
'days': Math.floor(t / (1000 * 60 * 60 * 24)),
'hours': Math.floor((t / (1000 * 60 * 60)) % 24),
'minutes': Math.floor((t / 1000 / 60) % 60),
'seconds': Math.floor((t / 1000) % 60)
};
}
export const timeSince = (date: Date) => {
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
let interval = seconds / 31536000;
let intervalType: string;
if (interval >= 1) {
intervalType = 'year';
} else {
interval = Math.floor(seconds / 2592000);
if (interval >= 1) {
intervalType = 'month';
} else {
interval = Math.floor(seconds / 86400);
if (interval >= 1) {
intervalType = 'day';
} else {
interval = Math.floor(seconds / 3600);
if (interval >= 1) {
intervalType = "hour";
} else {
interval = Math.floor(seconds / 60);
if (interval >= 1) {
intervalType = "minute";
} else {
interval = seconds;
intervalType = "second";
}
}
}
}
}
if (interval > 1 || interval === 0) {
intervalType += 's';
}
return interval + ' ' + intervalType;
}
export const wait = (ms: number) => {
return new Promise(resolve => setTimeout(resolve, ms));
}
export const closeTo = (date1: Date, date2: Date, tolerance: number) => {
const a = date1.getTime()
const b = date2.getTime()
return Math.abs(a - b) < tolerance
}

38
src/utils/types.ts Normal file
View File

@ -0,0 +1,38 @@
export interface Vote {
influence: number
like: boolean
postid: string
rating: number
lastUpdated: string
timestamp: string
voter: string
_id: {
voteid: string
}
}
export interface Notification {
_id: string
action: string
image: string
invoker: {
username: string
eosname: string
}
like: boolean
post: {
postid: string
url: string
title: string
tag: string
}
seen: boolean
postid: string
rating: number
recipient: string
voter: string
createdAt: string
quantity?: string
message?: string
}

43
src/utils/user.ts Normal file
View File

@ -0,0 +1,43 @@
import {API_BASE} from '@/constants/config'
export const getNormalizedValue = (val: number, min: number, max: number) => {
return Math.floor(((val - min) / (max - min)) * 100)
}
export const getMaxVote = (balance: number) => {
return balance >= 100 ? 250 : balance >= 0.5 ? 190 : 130
}
export const makePercentage = (val: number) => {
return `${val}%`
}
export const MAX_DELETE_VOTE = 600
export const MAX_FOLLOW_USAGE = 60
export const getActionUsage = async (userId: string) => {
try {
const req = await fetch(`${API_BASE}/accounts/actionusage/${userId}`)
if (req.ok) {
return { error: false, data: await req.json() }
}
return { error: true, msg: "API didn't return expected response." }
} catch {
return { error: true, msg: 'API is not available' }
}
}
export const createActionUsage = async (userId: string, balance: number) => {
const data = await getActionUsage(userId)
if (data.error) return { error: true, msg: 'API returned error' }
const MAX_VOTE = getMaxVote(balance)
return {
nextReset: new Date(data.data.lastReset + 864e5).toLocaleString(),
actionBars: {
deleteVote: makePercentage(getNormalizedValue(MAX_DELETE_VOTE - data.data.deleteVoteCount, 0, MAX_DELETE_VOTE)),
follow: makePercentage(getNormalizedValue(MAX_FOLLOW_USAGE - data.data.followCount, 0, MAX_FOLLOW_USAGE)),
vote: makePercentage(getNormalizedValue(MAX_VOTE - data.data.createVoteCount, 0, MAX_VOTE))
}
}
}

84
src/utils/votes.ts Normal file
View File

@ -0,0 +1,84 @@
import type { Vote } from './types'
import { fetchWAuth } from './auth'
const API_BASE = 'https://api.yup.io'
export const getPost = async (url: string): Promise<any | null> => {
try {
const res = await fetch('https://api.yup.io/posts/post/url', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
url
})
})
if(res.ok) {
return (await res.json())?.[0] ?? null
}
} catch {
return null
}
}
export const hasVote = (postId: string, account: string): Promise<Vote[]> => {
return new Promise((resolve) => {
fetch(`${API_BASE}/votes/post/${postId}/voter/${account}`).then((res) => {
if (res.ok) {
res.json().then((json) => {
resolve(json)
})
} else {
resolve([] as Vote[])
}
})
})
}
export const executeVote = async ({
userVote,
post,
url,
$mainStore,
$alertStore,
noVoteAlert = false
}) => {
const body = {} as Record<string, unknown>
let voteid = ''
if (userVote?._id) {
voteid = userVote._id.voteid
} else if(post) {
body.postid = post._id.postid
} else {
body.url = url
}
body.rating = userVote.rating
body.voter = $mainStore.user.auth.userId
console.log(body.voter)
if (userVote.like) {
body.like = true
} else {
body.like = false
}
const req = await fetchWAuth($mainStore, `${API_BASE}/votes${voteid ? '/' + voteid : ''}`, {
method: 'POST',
body: JSON.stringify(body)
})
if (req.ok) {
noVoteAlert || $alertStore?.show('Rating submited!')
return await req.json()
} else {
const err = await req.text()
if (err.includes('limit')) {
$alertStore?.show('Rating limit consumed!!!', 'warning')
} else if(err.includes('requests')) {
$alertStore?.show('You have made too many request try aagin after 24h', 'warning')
} else if(err.toLocaleLowerCase().includes('unauthorized')) {
$alertStore?.show('Seem your auth token is not valid anymore re-login!!', 'error')
} else {
$alertStore?.show('Vote not submited due to error try to re-login!', 'error')
}
return null
}
}

2
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

9
svelte.config.js Normal file
View File

@ -0,0 +1,9 @@
import sveltePreprocess from "svelte-preprocess";
import { windi } from 'svelte-windicss-preprocess';
export default {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [windi({}), sveltePreprocess()]
};

29
tsconfig.json Normal file
View File

@ -0,0 +1,29 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"resolveJsonModule": true,
"baseUrl": ".",
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": false,
"paths": {
"@/*": ["./src/*", "./dist/*"]
}
},
"include": [
"src/**/*.d.ts",
"src/**/*.ts",
"src/**/*.js",
"src/**/*.svelte"
],
"references": [{ "path": "./tsconfig.node.json" }]
}

11
tsconfig.node.json Normal file
View File

@ -0,0 +1,11 @@
// vite tsconfig
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts", "manifest.json"]
}

18
vite.config.ts Normal file
View File

@ -0,0 +1,18 @@
import { crx } from "@crxjs/vite-plugin";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { resolve } from "path";
import { defineConfig } from "vite";
import manifest from "./manifest.json";
const srcDir = resolve(__dirname, "src");
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte(), crx({ manifest })],
resolve: {
alias: {
src: srcDir,
'@': srcDir
},
},
});

1729
yarn.lock Normal file

File diff suppressed because it is too large Load Diff