Compare commits
64 Commits
6ce512cac4
...
monorepo
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a50b2c6cf | |||
| db9180721a | |||
| 1af1048670 | |||
| 148a3693b9 | |||
| efb1cba216 | |||
| 755cb06276 | |||
| a40390a30e | |||
| 9d5d49c9f2 | |||
| fe1cf2f171 | |||
| d3cb737972 | |||
| 7a0480fa69 | |||
| a77fefc251 | |||
| ad10f35e19 | |||
| 0df5022493 | |||
| ccf1aaba9b | |||
| 5433c7a1d2 | |||
| 0863a73625 | |||
| ba28925d3c | |||
| b96feef044 | |||
| 9387994d6d | |||
| e13d6a7f4a | |||
| b956bb525d | |||
| 3c888dc7f1 | |||
| f7b78f6d0f | |||
| 04a3ea5a23 | |||
| 0b808328f4 | |||
| 55ecb3b12b | |||
| d366262636 | |||
| 714ef9c87f | |||
| 41116e6ef1 | |||
| b56f1b0b62 | |||
| 5aab4a955b | |||
| 3f60cba322 | |||
| f8e36fc519 | |||
| 9937a407de | |||
| 92395f9db1 | |||
| f649cc2c14 | |||
| 3f3e851590 | |||
| dc54effc83 | |||
| 6951ce5db7 | |||
| 10516d84e6 | |||
| 307dfc2433 | |||
| 42c66ac8da | |||
| 7d70bec13b | |||
| fcff4f50c1 | |||
| 72a395e813 | |||
| 3877a37e7e | |||
| cbffc25a78 | |||
| 131a0b23f5 | |||
| 4f54d08666 | |||
| 76bf04f8de | |||
| 12a482324c | |||
| e733ca9cc3 | |||
| 66212c9a7d | |||
| 34c1e577f4 | |||
| 3d3d145072 | |||
| 7e31449bd2 | |||
| 9d663510bc | |||
| 2c4cd8ca5d | |||
| 2198e134d6 | |||
| 2df13c117a | |||
| 789fb6f38d | |||
| 946a9afeac | |||
| beb5ff052f |
@@ -1,9 +0,0 @@
|
||||
kind: pipeline
|
||||
type: ssh
|
||||
name: default
|
||||
|
||||
server:
|
||||
host: 192.168.1.54
|
||||
user: pi
|
||||
password:
|
||||
from_secret: password
|
||||
2
.gitignore
vendored
@@ -2,3 +2,5 @@ node_modules
|
||||
.DS_Store
|
||||
.vscode
|
||||
.env
|
||||
.idea
|
||||
.yarn
|
||||
|
||||
@@ -1 +1 @@
|
||||
MONGODB_URI=
|
||||
MONGODB_URI=
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
{
|
||||
"extends": ["standard", "standard-jsx", "standard-react", "next/core-web-vitals"]
|
||||
"extends": ["standard", "standard-jsx", "standard-react", "next/core-web-vitals"],
|
||||
"rules": {
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"newlines-between": "always",
|
||||
"alphabetize": {
|
||||
"order": "asc",
|
||||
"caseInsensitive": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
33
app/components/Footer.js
Normal 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
|
||||
<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>
|
||||
)
|
||||
}
|
||||
}
|
||||
37
app/components/WithYandexMetrika.js
Normal 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
|
||||
@@ -1,5 +1,7 @@
|
||||
import Head from 'next/head'
|
||||
|
||||
import Footer from './Footer'
|
||||
|
||||
export default function Layout ({ children }) {
|
||||
return (
|
||||
<>
|
||||
@@ -8,6 +10,7 @@ export default function Layout ({ children }) {
|
||||
<link rel='icon' href='/favicon.ico' />
|
||||
</Head>
|
||||
<main>{children}</main>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
6
app/next-i18next.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'ru']
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,31 @@
|
||||
const withPlugins = require('next-compose-plugins')
|
||||
|
||||
const { i18n } = require('./next-i18next.config')
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
redirects: [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/home',
|
||||
permanent: true
|
||||
}
|
||||
]
|
||||
i18n
|
||||
// webpack: (config, { webpack }) => {
|
||||
// return config
|
||||
// },
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
const redirects = {
|
||||
async redirects () {
|
||||
return [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/home',
|
||||
permanent: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = withPlugins(
|
||||
[
|
||||
[redirects]
|
||||
],
|
||||
nextConfig
|
||||
)
|
||||
|
||||
5363
app/package-lock.json
generated
@@ -6,12 +6,22 @@
|
||||
"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",
|
||||
"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": "file:../shared-stuff"
|
||||
"shared-stuff": "file:../shared-stuff",
|
||||
"sharp": "^0.30.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.1.0",
|
||||
|
||||
@@ -1,13 +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'
|
||||
}
|
||||
})
|
||||
|
||||
function MyApp ({ Component, pageProps }) {
|
||||
return (
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
<WithYandexMetrika>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</ThemeProvider>
|
||||
</WithYandexMetrika>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyApp
|
||||
export default appWithTranslation(App)
|
||||
|
||||
32
app/pages/home/HelpModal.js
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,73 @@
|
||||
import { Component } from 'react'
|
||||
import { Box, Chip } from '@mui/material'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import Image from 'next/image'
|
||||
import styles from './Hero.module.scss'
|
||||
import logo from './assets/warframe_logo.png'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default class Hero extends Component {
|
||||
render () {
|
||||
return (
|
||||
<div className={styles.hero}>
|
||||
<div className={styles.logo}>
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.text}>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: 400,
|
||||
m: 'auto'
|
||||
}}
|
||||
>
|
||||
<h1>Market Gaps</h1>
|
||||
<p>Find a profitable difference between the price of the set and the price of the sum of it's parts.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<p>{t('description')}</p>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<HelpModal
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Hero
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
.hero {
|
||||
width: 100%;
|
||||
padding: 128px 0;
|
||||
position: relative;
|
||||
background: white;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
|
||||
.text, .logo {
|
||||
width: 400px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.text {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Component } from 'react'
|
||||
|
||||
export default class ItemRow extends Component {
|
||||
constructor ({ scanResult }) {
|
||||
super()
|
||||
this.scanResult = scanResult
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<tr>
|
||||
<td>{this.scanResult.name}</td>
|
||||
<td>{this.scanResult.partsPrice}</td>
|
||||
<td>{this.scanResult.setPrice}</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,98 @@
|
||||
import React from 'react'
|
||||
import ItemRow from './ItemRow'
|
||||
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'
|
||||
|
||||
export default class Table extends React.Component {
|
||||
constructor ({ scanResults }) {
|
||||
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 (
|
||||
<table>
|
||||
<thead />
|
||||
<tbody>
|
||||
{this.scanResults.map((scanResult) =>
|
||||
<ItemRow
|
||||
key={scanResult._id}
|
||||
scanResult={scanResult}
|
||||
/>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<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)
|
||||
|
||||
@@ -1,41 +1,30 @@
|
||||
import { Component } from 'react'
|
||||
import dbConnect from '../../lib/dbConnect'
|
||||
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 class Home extends Component {
|
||||
constructor ({ scanResults }) {
|
||||
super()
|
||||
this.scanResults = scanResults
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
<Table
|
||||
scanResults={this.scanResults}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default function Home ({ scanResults }) {
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
<Table
|
||||
scanResults={scanResults}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps () {
|
||||
try {
|
||||
await dbConnect()
|
||||
const scanResults = await models.ScanResult.find({})
|
||||
export async function getServerSideProps ({ locale }) {
|
||||
await dbConnect()
|
||||
const scanResults = await models.ScanResult.find({})
|
||||
|
||||
return {
|
||||
props: {
|
||||
scanResults: JSON.parse(JSON.stringify(scanResults))
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return {
|
||||
props: {}
|
||||
return {
|
||||
props: {
|
||||
scanResults: JSON.parse(JSON.stringify(scanResults)),
|
||||
...(await serverSideTranslations(locale, ['common', 'home']))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
0
app/public/locales/en/common.json
Normal file
11
app/public/locales/en/home.json
Normal 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"
|
||||
}
|
||||
0
app/public/locales/ru/common.json
Normal file
11
app/public/locales/ru/home.json
Normal 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": "Разница"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
$clr-text: #1f1f1f;
|
||||
@@ -2,21 +2,21 @@ $text-font: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
|
||||
sans-serif;
|
||||
|
||||
$font-h0: normal normal bold 56px/64px $text-font;
|
||||
$font-h1: normal normal bold 48px/56px $text-font;
|
||||
$font-h2: normal normal bold 40px/48px $text-font;
|
||||
$font-h3: normal normal bold 36px/44px $text-font;
|
||||
$font-h4: normal normal bold 32px/40px $text-font;
|
||||
$font-h5: normal normal bold 28px/36px $text-font;
|
||||
$font-h6: normal normal bold 24px/32px $text-font;
|
||||
$font-h7: normal normal bold 20px/26px $text-font;
|
||||
$font-h8: normal normal bold 18px/24px $text-font;
|
||||
$font-h9: normal normal bold 16px/20px $text-font;
|
||||
$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 normal 24px/30px $text-font;
|
||||
$font-r7: normal normal normal 22px/28px $text-font;
|
||||
$font-r6: normal normal normal 20px/26px $text-font;
|
||||
$font-r5: normal normal normal 18px/24px $text-font;
|
||||
$font-r4: normal normal normal 16px/22px $text-font;
|
||||
$font-r3: normal normal normal 14px/18px $text-font;
|
||||
$font-r2: normal normal normal 12px/16px $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;
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
@import './fonts';
|
||||
@import './colors';
|
||||
|
||||
@@ -5,7 +5,6 @@ body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font: $font-r4;
|
||||
color: $clr-text;
|
||||
}
|
||||
|
||||
* {
|
||||
|
||||
9
gather/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM node:20-alpine
|
||||
LABEL authors="anatolykopyl"
|
||||
|
||||
WORKDIR /usr/node/app
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
9
gather/docker-compose.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
warframe-center-gather:
|
||||
image: git.radner.ru/anatolykopyl/warframe-center-gather:latest
|
||||
container_name: warframe-center-gather
|
||||
working_dir: /usr/node/app
|
||||
command: >
|
||||
sh -c "node src/index.js"
|
||||
@@ -1,9 +0,0 @@
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'warframe-market-bot',
|
||||
script: './src/index.js',
|
||||
watch: true,
|
||||
ignore_watch: ['node_modules', 'public'],
|
||||
restart_delay: 1 * 60 * 1000
|
||||
}]
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
"shared-stuff": "file:../shared-stuff"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"pm2": "^5.2.0"
|
||||
"eslint-config-standard": "^16.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,16 @@ class Api {
|
||||
|
||||
async _get (url) {
|
||||
await this.limiter.removeTokens(1)
|
||||
return axios.get(url)
|
||||
return axios.get(url).catch((error) => {
|
||||
console.error(error)
|
||||
return {
|
||||
data: {
|
||||
payload: {
|
||||
orders: []
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async getOrders (url) {
|
||||
@@ -22,6 +31,10 @@ class Api {
|
||||
|
||||
async getSortedOrders (url) {
|
||||
const orders = await this.getOrders(url)
|
||||
if (orders.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return orders.sort(function (a, b) {
|
||||
return a.platinum - b.platinum
|
||||
})
|
||||
|
||||
@@ -7,25 +7,44 @@ async function initDB () {
|
||||
await mongoose.connect(process.env.MONGODB_URI)
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await initDB()
|
||||
const main = async (firstLaunch) => {
|
||||
if (firstLaunch) {
|
||||
await initDB()
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
console.log(`Looking at ${item.name}`)
|
||||
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()
|
||||
if (partsPrice < setPrice) {
|
||||
models.ScanResult.findOneAndUpdate(
|
||||
{ name: item.name },
|
||||
{ partsPrice, setPrice },
|
||||
{ upsert: true },
|
||||
() => {}
|
||||
)
|
||||
}
|
||||
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 main()
|
||||
}
|
||||
|
||||
main(true)
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
mongoose.disconnect().then(() => {
|
||||
process.exit()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
const Part = require('./Part')
|
||||
|
||||
function capitalize (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
||||
|
||||
module.exports = class Item {
|
||||
constructor (name) {
|
||||
this.name = name
|
||||
this.fullName = capitalize(name) + ' Prime'
|
||||
this.set = new Part(name, 'set')
|
||||
|
||||
this.parts = []
|
||||
|
||||
@@ -3,12 +3,17 @@ const Api = require('../api')
|
||||
module.exports = class Part {
|
||||
constructor (set, part, amount) {
|
||||
this.part = part
|
||||
this.url = `${set}_prime_${part}`
|
||||
this.urlPath = `${set}_prime_${part}`
|
||||
this.url = `https://warframe.market/items/${set}_prime_${part}`
|
||||
this.amount = amount ?? 1
|
||||
}
|
||||
|
||||
async getPrice () {
|
||||
const orders = await Api.getSortedOrders(this.url)
|
||||
const orders = await Api.getSortedOrders(this.urlPath)
|
||||
if (orders.length === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return Number(orders[0].platinum) * this.amount
|
||||
}
|
||||
}
|
||||
|
||||
10553
package-lock.json
generated
@@ -7,5 +7,12 @@
|
||||
"url": "ssh://git@git.radner.ru:3036/anatolykopyl/warframe-center.git"
|
||||
},
|
||||
"author": "Anatoly Kopyl",
|
||||
"workspaces": ["./app", "./gather", "./shared-stuff"]
|
||||
"workspaces": [
|
||||
"./app",
|
||||
"./gather",
|
||||
"./shared-stuff"
|
||||
],
|
||||
"dependencies": {
|
||||
"mongoose": "^6.2.7"
|
||||
}
|
||||
}
|
||||
|
||||
11
shared-stuff/dist/index.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.models = void 0;
|
||||
const ScanResult_1 = __importDefault(require("./models/ScanResult"));
|
||||
const models = {
|
||||
ScanResult: ScanResult_1.default
|
||||
};
|
||||
exports.models = models;
|
||||
17
shared-stuff/dist/models/ScanResult.js
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const mongoose_1 = __importDefault(require("mongoose"));
|
||||
const scanResultSchema = new mongoose_1.default.Schema({
|
||||
name: String,
|
||||
fullName: String,
|
||||
url: String,
|
||||
partsPrice: Number,
|
||||
setPrice: Number,
|
||||
difference: Number
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
exports.default = mongoose_1.default.models.ScanResult || mongoose_1.default.model('ScanResult', scanResultSchema);
|
||||
@@ -2,8 +2,14 @@ const mongoose = require('mongoose')
|
||||
|
||||
const scanResultSchema = new mongoose.Schema({
|
||||
name: String,
|
||||
fullName: String,
|
||||
url: String,
|
||||
partsPrice: Number,
|
||||
setPrice: Number
|
||||
setPrice: Number,
|
||||
difference: Number
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
})
|
||||
|
||||
module.exports = mongoose.models.ScanResult || mongoose.model('ScanResult', scanResultSchema)
|
||||
|
||||