74 Commits

Author SHA1 Message Date
51f418353d Merge branch 'monorepo-ts' of ssh://git.radner.ru:3036/anatolykopyl/warframe-center into monorepo-ts
Some checks failed
continuous-integration/drone/push Build is failing
2022-07-07 02:29:41 +03:00
57ebf51a0a ci workflow 2022-07-07 02:29:37 +03:00
9bfd3bdc01 ci workflow
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is failing
2022-07-07 02:23:41 +03:00
862cbd3af1 Migrate to yarn berry and fix all workspace errors 2022-07-07 01:12:21 +03:00
78fd1b5adf Migrate to yarn 2022-07-07 00:51:28 +03:00
1af1048670 Fix crash
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-06 19:43:44 +03:00
148a3693b9 Merge branch 'monorepo' of ssh://git.radner.ru:3036/anatolykopyl/warframe-center into monorepo
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-05 14:50:16 +03:00
efb1cba216 Remove metrika id from env 2022-07-05 14:50:12 +03:00
755cb06276 Remove metrika id from env
Some checks failed
continuous-integration/drone/push Build was killed
2022-07-05 14:49:12 +03:00
a40390a30e Add ym id to drone secrets
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-05 14:10:50 +03:00
9d5d49c9f2 Fix undefined scan results
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-05 13:31:08 +03:00
fe1cf2f171 Merge branch 'monorepo' of ssh://git.radner.ru:3036/anatolykopyl/warframe-center into monorepo
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build was killed
2022-07-05 12:03:29 +03:00
d3cb737972 Fix ya metrika hopefully 2022-07-05 12:03:08 +03:00
7a0480fa69 Fix ya metrika hopefully
Some checks failed
continuous-integration/drone/push Build is failing
2022-07-05 11:58:01 +03:00
a77fefc251 Switch ya metrika packages
Some checks failed
continuous-integration/drone/push Build is failing
2022-07-05 09:50:16 +03:00
ad10f35e19 Disable ya metrika
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-05 02:34:43 +03:00
0df5022493 Yandex metrica work
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-05 02:19:27 +03:00
ccf1aaba9b Fix empty orders in getSortedOrders
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-04 12:38:56 +03:00
5433c7a1d2 Fixed linting error
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-04 02:40:48 +03:00
0863a73625 Added a help modal
Some checks failed
continuous-integration/drone/push Build is failing
2022-07-04 02:23:09 +03:00
ba28925d3c Modified drone.yml
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-03 17:41:56 +03:00
b96feef044 Modified drone.yml
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-03 17:34:52 +03:00
9387994d6d Fixed no db on build error
Some checks failed
continuous-integration/drone/push Build is failing
2022-07-03 17:27:35 +03:00
e13d6a7f4a Translate table headers
Some checks failed
continuous-integration/drone/push Build is failing
2022-07-03 17:14:56 +03:00
b956bb525d Drone build update
Some checks failed
continuous-integration/drone/push Build is failing
2022-07-03 16:59:19 +03:00
3c888dc7f1 Added i18n
Some checks failed
continuous-integration/drone/push Build is failing
2022-07-03 16:31:31 +03:00
f7b78f6d0f Return empty orders on error
Some checks failed
continuous-integration/drone/push Build is failing
2022-07-02 18:58:00 +03:00
04a3ea5a23 Handle api errors
Some checks failed
continuous-integration/drone/push Build is failing
2022-07-02 17:50:25 +03:00
0b808328f4 Path to pm2
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-30 03:30:38 +03:00
55ecb3b12b Edited pipeline
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-30 03:22:45 +03:00
d366262636 Edited pipeline
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-30 03:21:54 +03:00
714ef9c87f Edited pipeline
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-30 03:20:43 +03:00
41116e6ef1 POSIX exec
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-30 03:11:14 +03:00
b56f1b0b62 Full path
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-30 03:06:34 +03:00
5aab4a955b Source interactive shell files
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-30 03:05:20 +03:00
3f60cba322 Edited pipeline
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-30 02:58:00 +03:00
f8e36fc519 Join lines 2022-03-30 02:54:40 +03:00
9937a407de Edited pipeline 2022-03-30 02:49:14 +03:00
92395f9db1 Replace ssh key with pass 2022-03-30 02:47:37 +03:00
f649cc2c14 Edited pipeline 2022-03-30 02:40:26 +03:00
3f3e851590 Source profioe
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-22 17:05:05 +03:00
dc54effc83 ssh client
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-22 17:02:23 +03:00
6951ce5db7 Remove dependency
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-22 17:00:45 +03:00
10516d84e6 Big step 2022-03-22 16:59:42 +03:00
307dfc2433 alpine-ssh 2022-03-22 16:53:53 +03:00
42c66ac8da All docker 2022-03-22 16:53:08 +03:00
7d70bec13b Костыль путь до npm
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-22 16:49:37 +03:00
fcff4f50c1 Twst pwd
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-22 16:42:29 +03:00
72a395e813 Specified port
Some checks failed
continuous-integration/drone/push Build encountered an error
continuous-integration/drone Build is failing
2022-03-22 16:21:58 +03:00
3877a37e7e Removed useless scp
Some checks failed
continuous-integration/drone/push Build encountered an error
2022-03-22 16:14:57 +03:00
cbffc25a78 Updated drone config
Some checks failed
continuous-integration/drone/push Build encountered an error
2022-03-22 16:12:58 +03:00
131a0b23f5 Disclamer
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-19 20:01:58 +03:00
4f54d08666 Got rid of scss files, fixet time format
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-19 15:41:32 +03:00
76bf04f8de Fix redirect and restart frontend on push
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-19 03:38:25 +03:00
12a482324c pm2 file and redirect fix
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-19 03:11:52 +03:00
e733ca9cc3 Renamed pm2 process
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-19 02:59:51 +03:00
66212c9a7d Sequential pipeline
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-19 01:49:17 +03:00
34c1e577f4 Absolute path
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-19 01:45:44 +03:00
3d3d145072 Install deps in pipeline
Some checks failed
continuous-integration/drone/push Build encountered an error
continuous-integration/drone Build is failing
2022-03-19 01:41:44 +03:00
7e31449bd2 added mongo url
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-18 23:23:33 +03:00
9d663510bc Install in workspace
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-18 22:46:35 +03:00
2c4cd8ca5d Docker deploy yml
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-18 22:24:25 +03:00
2198e134d6 Explicit logo size
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-14 23:57:27 +03:00
2df13c117a ci preparations
Some checks failed
continuous-integration/drone Build is failing
2022-03-14 22:46:08 +03:00
789fb6f38d Fixed mongo hanging script 2022-03-14 03:38:29 +03:00
946a9afeac Expanded mongo model 2022-03-14 01:41:52 +03:00
beb5ff052f Material UI 2022-03-14 00:29:29 +03:00
6ce512cac4 scss variables 2022-03-13 22:49:28 +03:00
773797757c Fixed critical mongoose hang error 2022-03-13 20:29:37 +03:00
9d6735bd40 eslint 2022-03-13 18:20:10 +03:00
648af4f060 Split table into component 2022-03-13 16:19:52 +03:00
9e216f821f Prefer update documents instead of inserting 2022-03-13 14:34:57 +03:00
ff2cc92b5f npm workspace integration 2022-03-13 14:06:16 +03:00
1e397af280 File structure 2022-03-12 03:35:35 +03:00
72 changed files with 8474 additions and 7413 deletions

View File

@@ -1,20 +1,65 @@
kind: pipeline kind: pipeline
type: ssh type: ssh
name: default name: install dependencies
server: server:
host: 192.168.1.54 host: warframe.center
user: pi user: webmaster
password: ssh_key:
from_secret: password from_secret: ssh_private_key
clone:
disable: true
steps: steps:
- name: pull - name: fetch remote
commands: commands:
- cd /home/pi/warframe-market-bot - cd /home/webmaster/warframe-center && git fetch --all && git reset --hard origin/monorepo-ts
- git fetch --all - name: install dependencies
- git reset --hard origin/master
- name: install
commands: commands:
- cd /home/pi/warframe-market-bot - export PATH=$PATH:/home/webmaster/.nvm/versions/node/v16.14.2/bin
- npm install - cd /home/webmaster/warframe-center && yarn install --immutable
trigger:
branch:
- monorepo-ts
---
kind: pipeline
type: docker
name: build
clone:
disable: true
steps:
- name: bulid frontend
image: node:16
environment:
SSH_PRIVATE_KEY:
from_secret: SSH_PRIVATE_KEY
MONGODB_URI:
from_secret: MONGODB_URI
commands:
- git clone -b monorepo-ts https://git.radner.ru/anatolykopyl/warframe-center.git
- cd warframe-center
- yarn install --immutable
- yarn workspace frontend run build
- mkdir ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 400 ~/.ssh/id_rsa
- scp -o StrictHostKeyChecking=no -r ./frontend/.next webmaster@warframe.center:~/warframe-center/frontend
- |
ssh -o StrictHostKeyChecking=no webmaster@warframe.center '
[ -s ~/.nvm/nvm.sh ] && source ~/.nvm/nvm.sh &&
nvm use default &&
pm2 restart warframe-center-app-3000
'
depends_on:
- install dependencies
trigger:
branch:
- monorepo-ts

View File

@@ -1,3 +0,0 @@
{
"extends": "standard"
}

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
node_modules node_modules
.yarn
.DS_Store .DS_Store
.vscode .vscode
.env .env
public/*.html

3
.yarnrc.yml Normal file
View File

@@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.2.1.cjs

View File

@@ -0,0 +1 @@
MONGODB_URI=

15
frontend/.eslintrc.json Normal file
View File

@@ -0,0 +1,15 @@
{
"extends": ["standard", "standard-jsx", "standard-react", "next/core-web-vitals"],
"rules": {
"import/order": [
"error",
{
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
]
}
}

30
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

View File

@@ -0,0 +1,33 @@
import { Box, Typography, Link } from '@mui/material'
import { Component } from 'react'
export default class Footer extends Component {
render () {
return (
<Box
sx={{
p: 12
}}
>
All data is collected from the&nbsp;
<Link
href='https://warframe.market/api_docs'
target='_blank'
rel='noreferrer'
>
Warframe Market API
</Link>
<Typography variant='h6'>DISCLAIMER</Typography>
<Box
sx={{
fontSize: 12,
lineHeight: 'normal'
}}
>
Digital Extremes Ltd, Warframe and the logo Warframe are registered trademarks. All rights are reserved worldwide. This site has no official link with Digital Extremes Ltd or Warframe. All artwork, screenshots, characters or other recognizable features of the intellectual property relating to these trademarks are likewise the intellectual property of Digital Extremes Ltd.
</Box>
</Box>
)
}
}

View File

@@ -0,0 +1,37 @@
import Router from 'next/router'
import React, { useCallback, useEffect } from 'react'
import ym, { YMInitializer } from 'react-yandex-metrika'
const WithYandexMetrika = (props) => {
const { children } = props
const enabled = process.env.NODE_ENV !== 'development'
const hit = useCallback((url) => {
if (enabled) {
ym('hit', url)
} else {
console.log('%c[YandexMetrika](HIT)', 'color: orange', url)
}
}, [enabled])
useEffect(() => {
hit(window.location.pathname + window.location.search)
Router.events.on('routeChangeComplete', hit)
}, [hit])
return (
<>
{enabled && (
<YMInitializer
accounts={[87671663]}
options={{ webvisor: true, defer: true }}
version='2'
/>
)}
{children}
</>
)
}
export default WithYandexMetrika

View File

@@ -0,0 +1,16 @@
import Head from 'next/head'
import Footer from './Footer'
export default function Layout ({ children }) {
return (
<>
<Head>
<title>Warframe Center</title>
<link rel='icon' href='/favicon.ico' />
</Head>
<main>{children}</main>
<Footer />
</>
)
}

View File

@@ -0,0 +1,7 @@
module.exports = {
apps: [{
name: 'warframe-center-app-3000',
script: 'npm run start',
watch: false
}]
}

43
frontend/lib/dbConnect.js Normal file
View File

@@ -0,0 +1,43 @@
import mongoose from 'mongoose'
const MONGODB_URI = process.env.MONGODB_URI
if (!MONGODB_URI) {
throw new Error(
'Please define the MONGODB_URI environment variable inside .env.local'
)
}
/**
* Global is used here to maintain a cached connection across hot reloads
* in development. This prevents connections growing exponentially
* during API Route usage.
*/
let cached = global.mongoose
if (!cached) {
cached = global.mongoose = { conn: null, promise: null }
}
async function dbConnect () {
if (cached.conn) {
return cached.conn
}
if (!cached.promise) {
const opts = {
useNewUrlParser: true,
useUnifiedTopology: true,
bufferCommands: false
}
cached.promise = mongoose.connect(MONGODB_URI, opts)
.then(mongoose => {
return mongoose
})
}
cached.conn = await cached.promise
return cached.conn
}
export default dbConnect

View File

@@ -0,0 +1,6 @@
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'ru']
}
}

31
frontend/next.config.js Normal file
View File

@@ -0,0 +1,31 @@
const withPlugins = require('next-compose-plugins')
const { i18n } = require('./next-i18next.config')
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
i18n
// webpack: (config, { webpack }) => {
// return config
// },
}
const redirects = {
async redirects () {
return [
{
source: '/',
destination: '/home',
permanent: true
}
]
}
}
module.exports = withPlugins(
[
[redirects]
],
nextConfig
)

42
frontend/package.json Normal file
View File

@@ -0,0 +1,42 @@
{
"name": "frontend",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@emotion/react": "^11.8.2",
"@emotion/styled": "^11.8.1",
"@mui/icons-material": "^5.5.0",
"@mui/material": "^5.5.0",
"@mui/x-data-grid": "^5.6.1",
"moment": "^2.29.4",
"mongoose": "^6.4.3",
"next": "latest",
"next-compose-plugins": "^2.2.1",
"next-i18next": "^11.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-moment": "^1.1.1",
"react-yandex-metrika": "^2.6.0",
"reset-css": "^5.0.1",
"sass": "^1.49.9",
"shared-stuff": "1.0.0",
"sharp": "^0.30.3"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"eslint": "^8.11.0",
"eslint-config-next": "^12.1.0",
"eslint-config-standard": "^16.0.3",
"eslint-config-standard-jsx": "^10.0.0",
"eslint-config-standard-react": "^11.0.1",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.29.3"
}
}

29
frontend/pages/_app.js Normal file
View File

@@ -0,0 +1,29 @@
import { CssBaseline } from '@mui/material'
import { createTheme, ThemeProvider } from '@mui/material/styles'
import { appWithTranslation } from 'next-i18next'
import 'reset-css'
import '../styles/global.scss'
import Layout from '../components/layout'
import WithYandexMetrika from '../components/WithYandexMetrika'
function App ({ Component, pageProps }) {
const theme = createTheme({
palette: {
mode: 'light'
}
})
return (
<WithYandexMetrika>
<ThemeProvider theme={theme}>
<CssBaseline />
<Layout>
<Component {...pageProps} />
</Layout>
</ThemeProvider>
</WithYandexMetrika>
)
}
export default appWithTranslation(App)

View File

@@ -0,0 +1,32 @@
import { Dialog, DialogTitle, Box, Typography } from '@mui/material'
import { useTranslation } from 'next-i18next'
export default function HelpModal ({ open, onClose }) {
const { t } = useTranslation('home')
return (
<Dialog
open={open}
onClose={onClose}
>
<DialogTitle>
{t('help_header')}
</DialogTitle>
<Box
sx={{
p: 3
}}
>
<Typography gutterBottom>
{t('help_body_1')}
</Typography>
<Typography gutterBottom>
{t('help_body_2')}
</Typography>
<Typography gutterBottom>
{t('help_body_3')}
</Typography>
</Box>
</Dialog>
)
}

View File

@@ -0,0 +1,73 @@
import { Box, Chip } from '@mui/material'
import { useTranslation } from 'next-i18next'
import Image from 'next/image'
import { useState } from 'react'
import logo from './assets/warframe_logo.png'
import HelpModal from './HelpModal'
const Hero = () => {
const { t } = useTranslation('home')
const [modalOpen, setModalOpen] = useState(false)
const openModal = () => {
setModalOpen(true)
}
const closeModal = () => {
setModalOpen(false)
}
return (
<>
<Box
sx={{
py: 12,
textAlign: 'center'
}}
>
<Chip
label={t('help_header')}
variant='outlined'
onClick={openModal}
sx={{
position: 'absolute',
top: '10px',
right: '10px'
}}
/>
<Box
sx={{
width: 400,
m: 'auto'
}}
>
<Image
src={logo}
alt='logo'
layout='responsive'
width={500}
height={300}
/>
</Box>
<Box
sx={{
width: 400,
m: 'auto'
}}
>
<h1>Market Gaps</h1>
<p>{t('description')}</p>
</Box>
</Box>
<HelpModal
open={modalOpen}
onClose={closeModal}
/>
</>
)
}
export default Hero

View File

@@ -0,0 +1,98 @@
import { Link, Typography, Box } from '@mui/material'
import { DataGrid } from '@mui/x-data-grid'
import { Component } from 'react'
import { withTranslation } from 'react-i18next'
import Moment from 'react-moment'
class Table extends Component {
constructor ({ scanResults, t }) {
super()
this.scanResults = scanResults
? scanResults.map(row => ({
...row,
id: row._id
}))
: []
this.columns = [
{
field: 'name',
headerName: t('name'),
flex: 2,
renderCell: (cellValues) => {
return (
<Box
component='span'
sx={{
display: 'block'
}}
>
<Link
target='_blank'
href={cellValues.row.url}
rel='noreferrer'
>
{cellValues.row.fullName}
</Link>
<Typography variant='caption' display='block'>
<Moment
format='DD.MM HH:mm'
>
{cellValues.row.updatedAt}
</Moment>
</Typography>
</Box>
)
}
},
{
field: 'partsPrice',
headerName: t('parts_price'),
flex: 1
},
{
field: 'setPrice',
headerName: t('set_price'),
flex: 1
},
{
field: 'difference',
headerName: t('difference'),
flex: 1
}
]
}
static getInitialProps () {
return {
props: { scanResults: [] }
}
}
render () {
return (
<Box
sx={{
height: '90vh',
width: '100%'
}}
>
<DataGrid
rows={this.scanResults}
columns={this.columns}
autoPageSize
initialState={{
sorting: {
sortModel: [{ field: 'difference', sort: 'desc' }]
}
}}
sx={{
borderLeft: 'none',
borderRight: 'none'
}}
/>
</Box>
)
}
}
export default withTranslation('home')(Table)

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 234 KiB

View File

@@ -0,0 +1,30 @@
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { models } from 'shared-stuff'
import dbConnect from '../../lib/dbConnect'
import Hero from './Hero'
import Table from './Table'
export default function Home ({ scanResults }) {
return (
<>
<Hero />
<Table
scanResults={scanResults}
/>
</>
)
}
export async function getServerSideProps ({ locale }) {
await dbConnect()
const scanResults = await models.ScanResult.find({})
return {
props: {
scanResults: JSON.parse(JSON.stringify(scanResults)),
...(await serverSideTranslations(locale, ['common', 'home']))
}
}
}

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

View File

@@ -0,0 +1,11 @@
{
"description": "Find a profitable difference between the price of the set and the price of the sum of it's parts.",
"help_header": "How to use this site",
"help_body_1": "The main page of the site has a table, that contains info about prime items, that is updated in real time. The column \"Name\" has a link to the item on warframe.market and the time of the last update.",
"help_body_2": "Knowing the difference between the price of the set and the price of the sum of it's parts lets you save platinum buying or even make a profit by purchasing the parts and selling them as a set.",
"help_body_3": "Have fun!",
"name": "Name",
"parts_price": "Parts Price",
"set_price": "Set Price",
"difference": "Difference"
}

View File

View File

@@ -0,0 +1,11 @@
{
"description": "Находи выгодную разницу между ценой набора и ценой его частей по отдельности.",
"help_header": "Как использовать этот сайт?",
"help_body_1": "На главной странице сайта находится таблица, которая содержит информацию о прайм предметах, обновляемую в реальном времени. Колонка \"Наименование\" содержит ссылку на страницу предмета на warframe.market и время последнего обновления информации.",
"help_body_2": "Зная разницу между ценой частей и ценой комплекта можно, например сэкономить на покупке или даже заработать, если купить части по отдельности, а продать как сет.",
"help_body_3": "Приятного использования!",
"name": "Наименование",
"parts_price": "Цена частей",
"set_price": "Цена набора",
"difference": "Разница"
}

View File

@@ -0,0 +1,22 @@
$text-font: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
sans-serif;
$font-h0: normal normal 300 56px/64px $text-font;
$font-h1: normal normal 300 48px/56px $text-font;
$font-h2: normal normal 300 40px/48px $text-font;
$font-h3: normal normal 300 36px/44px $text-font;
$font-h4: normal normal 300 32px/40px $text-font;
$font-h5: normal normal 300 28px/36px $text-font;
$font-h6: normal normal 300 24px/32px $text-font;
$font-h7: normal normal 300 20px/26px $text-font;
$font-h8: normal normal 300 18px/24px $text-font;
$font-h9: normal normal 300 16px/20px $text-font;
$font-r8: normal normal 300 24px/30px $text-font;
$font-r7: normal normal 300 22px/28px $text-font;
$font-r6: normal normal 300 20px/26px $text-font;
$font-r5: normal normal 300 18px/24px $text-font;
$font-r4: normal normal 300 16px/22px $text-font;
$font-r3: normal normal 300 14px/18px $text-font;
$font-r2: normal normal 300 12px/16px $text-font;

View File

@@ -0,0 +1 @@
@import './fonts';

View File

@@ -0,0 +1,31 @@
@import './variables';
html,
body {
padding: 0;
margin: 0;
font: $font-r4;
}
* {
box-sizing: border-box;
}
h1 {
font: $font-h1;
}
h2 {
font: $font-h2;
}
h3 {
font: $font-h3;
}
h4 {
font: $font-h4;
}
h5 {
font: $font-h5;
}
h6 {
font: $font-h6;
}

15
gather/.eslintrc.js Normal file
View File

@@ -0,0 +1,15 @@
export default {
parser: '@typescript-eslint/parser',
extends: 'standard-with-typescript',
parserOptions: {
project: './tsconfig.json'
},
rules: {
'@typescript-eslint/no-floating-promises': [
'error',
{
ignoreIIFE: true
}
]
}
}

1
gather/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist

View File

@@ -1,6 +1,6 @@
module.exports = { module.exports = {
apps: [{ apps: [{
name: 'warframe-market-bot', name: 'warframe-center-gather',
script: './src/index.js', script: './src/index.js',
watch: true, watch: true,
ignore_watch: ['node_modules', 'public'], ignore_watch: ['node_modules', 'public'],

22
gather/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "gather",
"private": true,
"version": "1.0.0",
"description": "The background job that collects data and stores it in a DB",
"scripts": {
"start": "node ./dist/src/index.js",
"build": "../node_modules/typescript/bin/tsc"
},
"dependencies": {
"axios": "^0.26.0",
"dotenv": "^16.0.0",
"limiter": "^2.1.0",
"mongoose": "^6.4.3",
"shared-stuff": "1.0.0",
"typescript": "^4.7.4"
},
"devDependencies": {
"eslint-config-standard-with-typescript": "^22.0.0",
"pm2": "^5.2.0"
}
}

59
gather/src/api.ts Normal file
View File

@@ -0,0 +1,59 @@
import axios from 'axios'
import type { AxiosError } from 'axios'
import { RateLimiter } from 'limiter'
type user = {
status: 'online' | 'offline'
}
type order = {
order_type: 'buy' | 'sell',
platinum: number,
user: user
}
class Api {
baseUrl: string
delay: number
limiter: RateLimiter
constructor () {
this.baseUrl = 'https://api.warframe.market/v1/'
this.delay = Number(process.env.API_DELAY) ?? 3000
this.limiter = new RateLimiter({ tokensPerInterval: 1, interval: Number(this.delay) })
}
async _get (url: string) {
await this.limiter.removeTokens(1)
return axios.get(url).catch((error: AxiosError) => {
console.error(error)
return {
data: {
payload: {
orders: []
}
}
}
})
}
async getOrders (url: string): Promise<order[]> {
const response = await this._get(this.baseUrl + 'items/' + url + '/orders')
return response.data.payload.orders.filter(function (order: order) {
return order.order_type === 'sell' && order.user.status !== 'offline'
})
}
async getSortedOrders (url: string): Promise<order[]> {
const orders = await this.getOrders(url)
if (orders.length === 0) {
return []
}
return orders.sort(function (a: order, b: order) {
return a.platinum - b.platinum
})
}
}
export default new Api()

40
gather/src/index.ts Normal file
View File

@@ -0,0 +1,40 @@
import mongoose from 'mongoose'
import items from './items'
import { models } from 'shared-stuff'
import 'dotenv/config'
async function initDB (): Promise<void> {
await mongoose.connect(String(process.env.MONGODB_URI))
}
(async () => {
await initDB()
for (const item of items) {
process.stdout.write(`Looking at ${item.name}: `)
let partsPrice = 0
for (const part of item.parts) {
process.stdout.write(`${part.part} `)
partsPrice += await part.getPrice()
}
const setPrice = await item.set.getPrice()
await models.ScanResult.findOneAndUpdate(
{ name: item.name },
{
fullName: item.fullName,
url: item.set.url,
timestamp: new Date(),
partsPrice,
setPrice,
difference: setPrice - partsPrice
},
{ upsert: true }
)
console.log('✅')
}
await mongoose.disconnect()
})()

View File

@@ -1,7 +1,7 @@
const Item = require('./Item') import Item from './Item'
module.exports = class Archwings extends Item { export default class Archwings extends Item {
constructor (name) { constructor (name: string) {
super(name) super(name)
this.addPart('harness') this.addPart('harness')

View File

@@ -1,7 +1,7 @@
const Item = require('./Item') import Item from './Item'
module.exports = class Companion extends Item { export default class Companion extends Item {
constructor (name) { constructor (name: string) {
super(name) super(name)
this.addPart('carapace') this.addPart('carapace')

25
gather/src/items/Item.ts Normal file
View File

@@ -0,0 +1,25 @@
import Part from './Part'
function capitalize (string: string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
export default class Item {
name: string
fullName: string
set: Part
parts: Part[]
constructor (name: string) {
this.name = name
this.fullName = capitalize(name) + ' Prime'
this.set = new Part(name, 'set')
this.parts = []
this.addPart('blueprint')
}
addPart (part: string, amount: number = 1) {
this.parts.push(new Part(this.name, part, amount))
}
}

24
gather/src/items/Part.ts Normal file
View File

@@ -0,0 +1,24 @@
import Api from '../api'
export default class Part {
part: string
urlPath: string
url: string
amount: number
constructor (set: string, part: string, amount: number = 1) {
this.part = part
this.urlPath = `${set}_prime_${part}`
this.url = `https://warframe.market/items/${set}_prime_${part}`
this.amount = amount ?? 1
}
async getPrice (): Promise<number> {
const orders = await Api.getSortedOrders(this.urlPath)
if (orders.length === 0) {
return 0
}
return Number(orders[0].platinum) * this.amount
}
}

View File

@@ -1,7 +1,7 @@
const Item = require('./Item') import Item from './Item'
module.exports = class Primary extends Item { export default class Primary extends Item {
constructor (name) { constructor (name: string) {
super(name) super(name)
this.addPart('barrel') this.addPart('barrel')

View File

@@ -1,7 +1,7 @@
const Item = require('./Item') import Item from './Item'
module.exports = class Secondary extends Item { export default class Secondary extends Item {
constructor (name) { constructor (name: string) {
super(name) super(name)
this.addPart('barrel', 2) this.addPart('barrel', 2)

View File

@@ -1,7 +1,7 @@
const Item = require('./Item') import Item from './Item'
module.exports = class Warframe extends Item { export default class Warframe extends Item {
constructor (name) { constructor (name: string) {
super(name) super(name)
this.addPart('systems') this.addPart('systems')

View File

@@ -1,11 +1,12 @@
const items = require('./items.json') import items from './items.json'
const Warframe = require('./Warframe') import Warframe from './Warframe'
// const Primary = require('./Primary') // const Primary = require('./Primary')
// const Secondary = require('./Secondary') // const Secondary = require('./Secondary')
const Companion = require('./Companion') import Companion from './Companion'
const Archwings = require('./Archwings') import Archwings from './Archwings'
import type Item from './Item'
const result = [] const result: Item[] = []
items.warframes.forEach((name) => { items.warframes.forEach((name) => {
result.push(new Warframe(name)) result.push(new Warframe(name))
@@ -23,4 +24,4 @@ items.archwings.forEach((name) => {
result.push(new Archwings(name)) result.push(new Archwings(name))
}) })
module.exports = result export default result

20
gather/tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"allowJs": true,
"checkJs": false,
"outDir": "dist",
"rootDir": ".",
"strict": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"esModuleInterop": true,
},
"exclude": ["node_modules", "dist"],
"include": [
"./src",
"./*",
]
}

1651
gather/yarn-error.log Normal file

File diff suppressed because it is too large Load Diff

6863
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,17 @@
{ {
"name": "warframe-market-bot", "name": "warframe-center",
"private": true,
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "Список Прайм предметов: https://warframe.fandom.com/wiki/Prime",
"main": "index.js", "repository": {
"scripts": { "type": "git",
"test": "echo \"Error: no test specified\" && exit 1" "url": "ssh://git@git.radner.ru:3036/anatolykopyl/warframe-center.git"
}, },
"author": "Anatoly Kopyl", "author": "Anatoly Kopyl",
"license": "ISC", "workspaces": [
"dependencies": { "./frontend",
"axios": "^0.26.0", "./gather",
"dotenv": "^16.0.0", "./shared-stuff"
"handlebars": "^4.7.7", ],
"limiter": "^2.1.0" "packageManager": "yarn@3.2.1"
},
"devDependencies": {
"eslint-config-standard": "^16.0.3",
"pm2": "^5.2.0"
}
} }

View File

@@ -1,25 +0,0 @@
const slider = document.getElementById('min-difference')
slider.addEventListener('input', function (event) {
const table = document.getElementById('items')
Array.from(table.children).forEach(row => {
if (row.dataset.difference < Number(event.target.value)) {
row.style.display = 'none'
} else {
row.style.display = 'table-row'
}
})
document.getElementById('filter-value').innerText = event.target.value
})
document.addEventListener('scroll', function () {
const gunner1 = document.getElementById('gunner1')
const gunner2 = document.getElementById('gunner2')
gunner1.style.transform = `translateX(${-window.scrollY}px)`
gunner2.style.transform = `translateX(${window.scrollY}px)`
gunner1.style.filter = `blur(${window.scrollY / 50}px)`
gunner2.style.filter = `blur(${window.scrollY / 50}px)`
})

View File

@@ -1,186 +0,0 @@
:root {
--bg-main: white;
--brand-clr: rgb(26, 34, 58);
--bg: rgb(16, 22, 25);
--text-clr: black;
--accent-clr: red;
}
* {
font-family: sans-serif;
font-weight: 100;
}
body {
position: relative;
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text-clr);
text-align: center;
}
a {
text-decoration: none;
color: var(--accent-clr);
}
.main-column {
width: 800px;
background: var(--bg-main);
padding: 32px 128px;
box-sizing: border-box;
margin: 128px auto;
border: 2px solid var(--accent-clr);
}
.main-column::before {
position: absolute;
left: 0px;
width: 50%;
border-top: 2px solid var(--accent-clr);
z-index: -1;
content: '';
}
.main-column::after {
position: absolute;
right: 0px;
width: 50%;
border-top: 2px solid var(--accent-clr);
z-index: -1;
content: '';
}
.hero {
width: 100%;
padding: 128px 0;
position: relative;
box-sizing: border-box;
background: white;
color: black;
text-align: center;
overflow: hidden;
}
.hero > .main {
position: relative;
margin: auto;
display: flex;
flex-direction: column;
z-index: 10;
color: var(--brand-clr);
}
.hero .logo, .hero .text {
margin: auto;
width: 400px;
}
#gunner1, #gunner2 {
position: absolute;
max-width: 500px;
height: 500px;
object-fit: contain;
transition: transform .1s;
top: calc(50% - 250px);
}
#gunner1 {
left: 0;
}
#gunner2 {
right: 0;
}
.languages {
position: absolute;
left: 0;
top: 0;
margin: 10px;
z-index: 20;
}
.languages > a {
position: relative;
display: inline-block;
padding: 5px;
height: 32px;
border-radius: 50%;
}
.languages img {
max-height: 100%;
}
.languages .selected {
background: rgb(233, 233, 233);
box-shadow: 0 0 5px rgba(0, 0, 0, 50%);
}
.controls {
margin: 32px auto;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-main);
padding: 8px;
width: max-content;
}
.controls > * {
margin: 0 4px;
}
table {
min-width: 500px;
margin: 32px auto;
border-spacing: 0;
background: var(--bg-main);
}
tr {
text-align: left;
}
td {
padding: 2px 16px;
}
th {
padding: 8px 16px;
font-weight: 400;
}
.name {
text-transform: capitalize;
}
.timestamp {
font-size: 14px;
padding-bottom: 16px;
position: sticky;
top: 100%;
color: white;
}
@media screen and (max-width: 500px) {
.hero .logo {
width: 100%;
}
#gunner1, #gunner2 {
display: none;
}
.main-column {
width: 100%;
border: none;
padding: 72px 0;
}
table {
min-width: 100%;
}
}

1
shared-stuff/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist

7
shared-stuff/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import ScanResult from './models/ScanResult'
const models = {
ScanResult
}
export { models }

View File

@@ -0,0 +1,15 @@
import mongoose from 'mongoose'
const scanResultSchema = new mongoose.Schema({
name: String,
fullName: String,
url: String,
partsPrice: Number,
setPrice: Number,
difference: Number
},
{
timestamps: true
})
export default mongoose.models.ScanResult || mongoose.model('ScanResult', scanResultSchema)

14
shared-stuff/package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "shared-stuff",
"private": true,
"version": "1.0.0",
"description": "The stuff that is shared between the background job and the Next app",
"main": "./dist/index.js",
"scripts": {
"build": "./node_modules/typescript/bin/tsc"
},
"dependencies": {
"mongoose": "^6.4.3",
"typescript": "^4.7.4"
}
}

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"checkJs": false,
"outDir": "dist",
"rootDir": ".",
"strict": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"esModuleInterop": true,
},
"exclude": ["node_modules", "dist"],
"include": [
"./*",
]
}

View File

@@ -1,31 +0,0 @@
const axios = require('axios')
const { RateLimiter } = require('limiter')
class Api {
constructor () {
this.baseUrl = 'https://api.warframe.market/v1/'
this.delay = process.env.API_DELAY ?? 3000
this.limiter = new RateLimiter({ tokensPerInterval: 1, interval: Number(this.delay) })
}
async _get (url) {
await this.limiter.removeTokens(1)
return axios.get(url)
}
async getOrders (url) {
const response = await this._get(this.baseUrl + 'items/' + url + '/orders')
return response.data.payload.orders.filter(function (order) {
return order.order_type === 'sell' && order.user.status !== 'offline'
})
}
async getSortedOrders (url) {
const orders = await this.getOrders(url)
return orders.sort(function (a, b) {
return a.platinum - b.platinum
})
}
}
module.exports = new Api()

View File

@@ -1,21 +0,0 @@
require('dotenv').config()
const items = require('./items')
const output = require('./output');
(async () => {
for (const item of items) {
console.log(`Looking at ${item.name}`)
let partsPrice = 0
for (const part of item.parts) {
partsPrice += await part.getPrice()
}
const setPrice = await item.set.getPrice()
if (partsPrice < setPrice) {
output.addItem(item.name, partsPrice, setPrice)
}
}
output.submit()
})()

View File

@@ -1,15 +0,0 @@
const Part = require('./Part')
module.exports = class Item {
constructor (name) {
this.name = name
this.set = new Part(name, 'set')
this.parts = []
this.addPart('blueprint')
}
addPart (part, amount) {
this.parts.push(new Part(this.name, part, amount))
}
}

View File

@@ -1,14 +0,0 @@
const Api = require('../api')
module.exports = class Part {
constructor (set, part, amount) {
this.part = part
this.url = `${set}_prime_${part}`
this.amount = amount ?? 1
}
async getPrice () {
const orders = await Api.getSortedOrders(this.url)
return Number(orders[0].platinum) * this.amount
}
}

View File

@@ -1,10 +0,0 @@
module.exports = class ListingSet {
constructor (name, parts, set) {
this.name = name + ' prime'
this.parts = parts
this.set = set
this.link = `https://warframe.market/items/${name}_prime_set`
this.difference = set - parts
}
}

View File

@@ -1,9 +0,0 @@
function ifEq (a, b, opts) {
if (a === b) {
return opts.fn(this)
} else {
return opts.inverse(this)
}
}
module.exports = { ifEq }

View File

@@ -1,35 +0,0 @@
{
"en": {
"link": "",
"title": "Market Gaps",
"description": "Find a profitable difference between the price of the set and the price of the sum of it's parts.",
"filter_by_difference": "Filter by difference:",
"name": "Name",
"parts_price": "Parts price",
"set_price": "Set price",
"difference": "Difference",
"generated_at": "Generated at"
},
"ru": {
"link": "ru",
"title": "Market Gaps",
"description": "Находите выгоду между покупкой сета по отдельности и продажей его в собранном виде.",
"filter_by_difference": "Фильтровать по разнице:",
"name": "Название",
"parts_price": "Стоимость частей",
"set_price": "Стоимость сета",
"difference": "Разница",
"generated_at": "Сгенерировано в"
},
"ua": {
"link": "ua",
"title": "Market Gaps",
"description": "Знаходьте вигоду між покупкою сета окремо та продажем його в зібраному вигляді.",
"filter_by_difference": "Фільтрувати за різницею:",
"name": "Назва",
"parts_price": "Вартість частин",
"set_price": "Вартість сета",
"difference": "Різниця",
"generated_at": "Згенеровано в"
}
}

View File

@@ -1,48 +0,0 @@
const fs = require('fs')
const Handlebars = require('handlebars')
const { ifEq } = require('./helpers')
const ListingSet = require('./ListingSet')
const i18n = require('./i18n.json')
Handlebars.registerHelper('if_eq', ifEq)
class Output {
constructor () {
this.items = []
this.timestamp = null
}
addItem (...p) {
this.items.push(new ListingSet(...p))
}
async _compileTemplate () {
const templateFile = await fs.readFileSync('./src/output/template.hbs', 'utf8')
return Handlebars.compile(templateFile)
}
async _writeToFile (filename, content) {
try {
await fs.unlinkSync(filename)
} catch {
console.log('File probably doesnt exist')
}
fs.writeFileSync(filename, content, 'utf8')
}
async submit () {
this.timestamp = new Date()
const template = await this._compileTemplate()
Object.keys(i18n).forEach(locale => {
const filename = i18n[locale].link || 'index'
this._writeToFile(`./public/${filename}.html`, template({
...this,
t: i18n[locale],
languages: i18n
}))
})
}
}
module.exports = new Output()

View File

@@ -1,102 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Find profits in the Warframe market">
<meta name="keywords" content="warframe,market,buy,sell,trade,mod,blueprint,syndicate,relic,order,auction,riven,wts,wtb,sales,worth,platinum,price,checking">
<link rel="canonical" href="https://warframe.center/">
<link rel="alternate" hreflang="en" href="https://warframe.center/">
<link rel="alternate" hreflang="ru" href="https://warframe.center/ru/">
<link rel="alternate" hreflang="uk" href="https://warframe.center/ua/">
<link rel="alternate" hreflang="x-default" href="https://warframe.center/">
<meta property="og:title" content="Warframe Center">
<meta property="og:description" content="Find profits in the Warframe market">
<meta property="og:url" content="https://warframe.center">
<meta property="og:image" content="https://warframe.center/assets/warframe_logo.png">
<meta property="og:site_name" content="Warframe Center">
<meta property="og:locale" content="en">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="style.css">
<title>Warframe Center</title>
<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(87671663, "init", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true
});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/87671663" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</head>
<body>
<div class="hero">
<img id="gunner1" src="./assets/gunner1.png">
<div class="main">
<img class="logo" src="./assets/warframe_logo.png">
<div class="text">
<h1>{{t.title}}</h1>
<p>{{t.description}}</p>
</div>
</div>
<img id="gunner2" src="./assets/gunner2.png">
<div class="languages">
{{#each languages}}
<a
href="/{{this.link}}"
class="{{#if_eq ../t.link this.link}}selected{{/if_eq}}"
>
<img src="./assets/languages/{{@key}}.png">
</a>
{{/each}}
</div>
</div>
<div class="main-column">
<div class="controls">
<label for="min-difference">{{t.filter_by_difference}}</label>
<input type="range" min="1" max="60" value="1" id="min-difference">
<span id="filter-value">1</span>
</div>
<table>
<thead>
<tr>
<th>{{t.name}}</th>
<th>{{t.parts_price}}</th>
<th>{{t.set_price}}</th>
<th>{{t.difference}}</th>
</tr>
</thead>
<tbody id="items">
{{#each items}}
<tr data-difference="{{this.difference}}">
<td class="name">
<a href="{{this.link}}" target="_blank">
{{this.name}}
</a>
</td>
<td>{{this.parts}}</td>
<td>{{this.set}}</td>
<td>{{this.difference}}</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
<div class="timestamp">
{{t.generated_at}} {{timestamp}}
</div>
<script src="index.js"></script>
</body>
</html>

5867
yarn.lock Normal file

File diff suppressed because it is too large Load Diff