Merge branch 'new_style'

This commit is contained in:
mrfry 2021-04-21 12:12:47 +02:00
commit 21fdbb57a3
72 changed files with 3153 additions and 6934 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
node_modules/ node_modules/
.next/ .next/
out/ out/
/.vs
public/ public/

5296
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"babel-eslint": "^10.1.0",
"eslint-plugin-react": "^7.21.5", "eslint-plugin-react": "^7.21.5",
"next": "^10.0.3", "next": "^10.0.3",
"react": "^16.13.0", "react": "^16.13.0",

View file

@ -1,13 +1,12 @@
import React from 'react'
import styles from './button.module.css' import styles from './button.module.css'
export default function Button (props) { export default function Button(props) {
return ( return (
<div> <div>
<center> <center>
<a href={props.href}> <a href={props.href}>
<div className={styles.ircLink}> <div className={styles.button}>{props.text}</div>
{props.text}
</div>
</a> </a>
</center> </center>
</div> </div>

View file

@ -1,6 +1,6 @@
.load { .load {
border: 4px solid #f3f3f3; border: 4px solid #f3f3f3;
border-top: 4px solid #99f; border-top: 4px solid var(--text-color);
border-radius: 50%; border-radius: 50%;
width: 20px; width: 20px;
height: 20px; height: 20px;

View file

@ -1,33 +1,41 @@
import React, { PureComponent } from 'react' import React from 'react'
class Question extends PureComponent { function highlightText(text, toHighlight) {
render () { const re = new RegExp(toHighlight, 'gi')
const { question } = this.props return text.replace(re, `<mark>${toHighlight}</mark>`)
let qdata = question.data
if (typeof qdata === 'object' && qdata.type === 'simple') {
qdata = ''
}
if (qdata) {
try {
qdata = JSON.stringify(qdata)
} catch (e) {}
}
return (
<div className='questionContainer'>
<div className='question'>
{question.Q}
</div>
<div className='answer'>
{question.A}
</div>
<div className='data'>
{qdata || null}
</div>
</div>
)
}
} }
export default Question export default function Question({ question, searchTerm }) {
let qdata = question.data
if (typeof qdata === 'object' && qdata.type === 'simple') {
qdata = ''
}
if (qdata) {
try {
qdata = JSON.stringify(qdata)
} catch (e) {
//
}
}
const questionText = searchTerm
? highlightText(question.Q, searchTerm)
: question.Q
const answerText = searchTerm
? highlightText(question.A, searchTerm)
: question.A
return (
<div className="questionContainer">
<div
className="question"
dangerouslySetInnerHTML={{ __html: questionText }}
></div>
<div
className="answer"
dangerouslySetInnerHTML={{ __html: answerText }}
></div>
<div className="data">{qdata || null}</div>
</div>
)
}

View file

@ -1,4 +1,4 @@
import React, { PureComponent } from 'react' import React from 'react'
import Questions from './Questions.js' import Questions from './Questions.js'
@ -8,77 +8,74 @@ const countReducer = (acc, subj) => {
return acc + subj.Questions.length return acc + subj.Questions.length
} }
class QuestionSearchResult extends PureComponent { export default function QuestionSearchResult({ data, searchTerm }) {
render() { let subjs = []
const { data, searchTerm } = this.props let results = -1
let subjs = [] if (searchTerm) {
let results = -1 subjs = data.reduce((acc, subj) => {
const resultQuestions = subj.Questions.reduce((qacc, question) => {
if (searchTerm) { const keys = ['Q', 'A', 'data']
subjs = data.reduce((acc, subj) => { keys.some((key) => {
const resultQuestions = subj.Questions.reduce((qacc, question) => { if (typeof question[key] !== 'string') {
const keys = ['Q', 'A', 'data'] return false
keys.some((key) => { }
if (typeof question[key] !== 'string') { if (
return false question[key] &&
} question[key].toLowerCase().includes(searchTerm.toLowerCase())
if ( ) {
question[key] && qacc.push(question)
question[key].toLowerCase().includes(searchTerm.toLowerCase()) return true
) { }
qacc.push(question) })
return true return qacc
}
})
return qacc
}, [])
if (resultQuestions.length > 0) {
acc.push({
Name: subj.Name,
Questions: resultQuestions,
})
}
return acc
}, []) }, [])
results = subjs.reduce(countReducer, 0) if (resultQuestions.length > 0) {
} else { acc.push({
results = data.reduce(countReducer, 0) Name: subj.Name,
} Questions: resultQuestions,
})
}
return acc
}, [])
results = subjs.reduce(countReducer, 0)
} else {
results = data.reduce(countReducer, 0)
}
const renderCount = () => { const renderCount = () => {
return ( return (
<div> <div className="resultContainer">
{results === 0 && (
<div> <div>
{searchTerm ? '' : 'Kezdj el írni kereséshez! '} {`${results} találat. Az általad keresett kifejezés nem található,
{searchTerm próbáld bővíteni a keresési feltételt!`}
? `${results} találat, ${subjs.length} tárgyból`
: `Keresés ${results} kérdés és ${data.length} tárgyból`}
</div> </div>
{results === 0 && ( )}
<div> {results > 0 && <div>{`${results} találat.`}</div>}
{ </div>
'Pontos egyezést keres a kereső, próbálj kisebb részletre keresni' )
} }
</div>
)}
</div>
)
}
if (results > constants.maxQuestionsToRender) { if (results > constants.maxQuestionsToRender) {
return renderCount() return (
} else { <div>
return ( {searchTerm ? (
<div> <div className="resultContainer">
<div>{renderCount()}</div> Szűkítsd a keresési feltételeket a találatok megjelenítéséhez!
<div>
<Questions subjs={subjs} />
</div> </div>
) : null}
<hr />
</div>
)
} else {
return (
<div>
<div>{renderCount()}</div>
<div>
<Questions subjs={subjs} searchTerm={searchTerm} />
</div> </div>
) </div>
} )
} }
} }
export default QuestionSearchResult

View file

@ -5,22 +5,22 @@ import Question from './Question.js'
import styles from './Questions.module.css' import styles from './Questions.module.css'
class Questions extends PureComponent { class Questions extends PureComponent {
render () { render() {
const { subjs } = this.props const { subjs, searchTerm } = this.props
return ( return (
<div> <div>
<hr />
{subjs.map((subj, i) => { {subjs.map((subj, i) => {
return ( return (
<div key={i}> <div className={styles.questionBg} key={i}>
<div className={styles.subjName}> <div className={styles.subjName}>{subj.Name}</div>
{subj.Name} {subj.Questions.map((question, i) => {
</div>
{ subj.Questions.map((question, i) => {
return ( return (
<Question <Question
key={i} key={i}
question={question} question={question}
searchTerm={searchTerm}
/> />
) )
})} })}

View file

@ -1,7 +1,14 @@
.subjName { .subjName {
font-size: 24px; font-size: 24px;
background-color: #9999ff; font-weight: 600;
background-color: var(--text-color);
color: black; color: black;
margin-top: 25px;
padding: 10px; padding: 10px;
word-wrap: break-word; word-wrap: break-word;
} }
.questionBg {
background-color: #1b1b1c;
padding-bottom: 5px;
}

View file

@ -3,26 +3,19 @@ import React, { PureComponent } from 'react'
import Question from './Question.js' import Question from './Question.js'
class Subject extends PureComponent { class Subject extends PureComponent {
render () { render() {
const { subj } = this.props const { subj } = this.props
if (subj) { if (subj) {
return ( return (
<div > <div>
{subj.Questions.map((question, i) => { {subj.Questions.map((question, i) => {
return ( return <Question key={i} question={question} />
<Question
key={i}
question={question}
/>
)
})} })}
</div> </div>
) )
} else { } else {
return ( return <div />
<div />
)
} }
} }
} }

View file

@ -1,10 +1,10 @@
import styles from './SubjectSelector.module.css' import styles from './SubjectSelector.module.css'
export default function SubjectSelector (props) { export default function SubjectSelector(props) {
const { activeSubjName, searchTerm, data, onSubjSelect } = props const { activeSubjName, searchTerm, data, onSubjSelect } = props
return ( return (
<div className='subjectSelector'> <div className="subjectSelector">
{data.map((subj, i) => { {data.map((subj, i) => {
if (!subj.Name.toLowerCase().includes(searchTerm.toLowerCase())) { if (!subj.Name.toLowerCase().includes(searchTerm.toLowerCase())) {
return null return null
@ -12,16 +12,15 @@ export default function SubjectSelector (props) {
return ( return (
<div <div
className={activeSubjName === subj.Name className={
? 'subjItem activeSubjItem' activeSubjName === subj.Name
: 'subjItem' ? 'subjItem activeSubjItem'
: 'subjItem'
} }
key={i} key={i}
onClick={() => onSubjSelect(subj.Name)} onClick={() => onSubjSelect(subj.Name)}
> >
<span className={styles.subjName}> <span className={styles.subjName}>{subj.Name}</span>
{subj.Name}
</span>
<span className={styles.questionCount}> <span className={styles.questionCount}>
[ {subj.Questions.length} ] [ {subj.Questions.length} ]
</span> </span>

View file

@ -1,4 +1,4 @@
.ircLink { .button {
background-color: #9999ff; background-color: #9999ff;
border: none; border: none;
color: white; color: white;

View file

@ -22,14 +22,14 @@ function CommentInput({ onSubmit, onCancel }) {
onSubmit(val) onSubmit(val)
}} }}
> >
Submit Küldés
</span> </span>
<span <span
onClick={() => { onClick={() => {
onCancel() onCancel()
}} }}
> >
Cancel Bezárás
</span> </span>
</div> </div>
</div> </div>
@ -45,8 +45,8 @@ function Comment({ comment, index, onComment, onDelete, onReact, uid }) {
const commentStyle = admin const commentStyle = admin
? styles.adminComment ? styles.adminComment
: own : own
? styles.ownComment ? styles.ownComment
: '' : ''
return ( return (
<div className={styles.comment}> <div className={styles.comment}>
@ -63,25 +63,27 @@ function Comment({ comment, index, onComment, onDelete, onReact, uid }) {
</div> </div>
<div>User #{user}</div> <div>User #{user}</div>
</div> </div>
<div>{date}</div> <div className={styles.commentDate}>{date}</div>
</div> </div>
<div className={`${!displayed && styles.hidden}`}> <div className={`${!displayed && styles.hidden}`}>
<div className={styles.commentText}> {content}</div> <div className={styles.commentText}> {content}</div>
<div className={'actions'}> <div className={'actions'}>
<span <span
className={styles.reply_bttn}
onClick={() => { onClick={() => {
setCommenting(true) setCommenting(true)
}} }}
> >
Reply... Válasz...
</span> </span>
{own && ( {own && (
<span <span
className={styles.delete_bttn}
onClick={() => { onClick={() => {
onDelete([index]) onDelete([index])
}} }}
> >
Delete Törlés
</span> </span>
)} )}
<ReactButton <ReactButton
@ -179,11 +181,12 @@ export default function Comments({
{commentCount !== 0 ? ( {commentCount !== 0 ? (
<div className={'actions'}> <div className={'actions'}>
<span <span
className={styles.comment_bttn}
onClick={() => { onClick={() => {
setAddingNewComment(true) setAddingNewComment(true)
}} }}
> >
New comment Új komment
</span> </span>
</div> </div>
) : null} ) : null}
@ -191,7 +194,7 @@ export default function Comments({
<CommentInput <CommentInput
onSubmit={(e) => { onSubmit={(e) => {
if (!e) { if (!e) {
alert('Írj be valamit, hogy kommentelhess...') alert('Írj be valamit a szövegdobozba, hogy kommentelhess!')
return return
} }
setAddingNewComment(false) setAddingNewComment(false)
@ -207,18 +210,19 @@ export default function Comments({
) : null} ) : null}
</Modal> </Modal>
) : null} ) : null}
<div className={'actions'}> <div
<span className={'actions'}
onClick={() => { onClick={() => {
setCommentsShowing(true) setCommentsShowing(true)
if (commentCount === 0) { if (commentCount === 0) {
setAddingNewComment(true) setAddingNewComment(true)
} }
}} }}
> >
<span className={styles.comment_bttn}>
{commentCount === 0 {commentCount === 0
? 'New comment' ? 'Új komment'
: `Show ${commentCount} comment${commentCount > 1 ? 's' : ''}`} : `Kommentek mutatása (${commentCount})`}
</span> </span>
</div> </div>
</div> </div>

View file

@ -1,21 +1,58 @@
.comment_bttn {
font-size: 15px;
padding-top: 4px !important;
transition: width 0.5s, height 0.5s, ease-in 0.5s;
}
.comment_bttn:hover {
transition: width 0.5s, height 0.5s, ease-out 0.5s;
color: gainsboro;
}
.reply_bttn {
font-size: 15px;
margin-top: 14.5px !important;
margin-left: 6px !important;
padding-top: 4px !important;
transition: width 0.5s, height 0.5s, ease-in 0.5s;
}
.reply_bttn:hover {
transition: width 0.5s, height 0.5s, ease-out 0.5s;
color: gainsboro;
}
.delete_bttn {
font-size: 15px;
margin-top: 14.5px !important;
margin-left: 6px !important;
padding-top: 4px !important;
transition: width 0.5s, height 0.5s, ease-in 0.5s;
}
.delete_bttn:hover {
transition: width 0.5s, height 0.5s, ease-out 0.5s;
color: gainsboro;
}
.comment { .comment {
margin-left: 25px; margin-left: 10px;
margin-right: 30px;
padding: 8px 0px; padding: 8px 0px;
} }
.commentData { .commentData {
padding: 5px 2px; padding: 5px 2px;
border-left: 2px solid var(--text-color); border-left: 3px solid azure;
border-radius: 3px;
} }
.commentData:hover { .commentDate {
background-color: var(--hoover-color); text-align: right;
} }
.commentHeader { .commentHeader {
display: flex; display: flex;
justify-content: space-between; /*justify-content: space-between;*/
} }
.commentHeader > div { .commentHeader > div {
@ -38,7 +75,7 @@
} }
.ownComment { .ownComment {
border-left: 2px solid green; border-left: 5px dotted azure;
} }
.showHide { .showHide {
@ -58,5 +95,5 @@
} }
.adminComment { .adminComment {
border-left: 2px solid yellow; border-left: 3px solid gold;
} }

View file

@ -4,37 +4,23 @@ import Modal from './modal'
import styles from './composer.module.css' import styles from './composer.module.css'
function FileUploader({ onChange }) {
return (
<div className={styles.inputArea}>
<div className={styles.textTitle}>Fájl csatolása</div>
<input
className={styles.fileInput}
type="file"
name="file"
onChange={onChange}
/>
</div>
)
}
export default function Composer({ onSubmit }) { export default function Composer({ onSubmit }) {
const [editorShowing, setEditorShowing] = useState(false) const [editorShowing, setEditorShowing] = useState(false)
const [val, setVal] = useState('') const [val, setVal] = useState('')
const [type, setType] = useState('public')
const [title, setTitle] = useState('') const [title, setTitle] = useState('')
const [file, setFile] = useState()
return ( return (
<> <>
<div <center>
onClick={() => { <div
setEditorShowing(true) onClick={() => {
}} setEditorShowing(true)
className={styles.new} }}
> className={styles.new}
Új bejegyzés / feedback >
</div> Bejegyzés írása...
</div>
</center>
{editorShowing && ( {editorShowing && (
<Modal <Modal
closeClick={() => { closeClick={() => {
@ -42,73 +28,39 @@ export default function Composer({ onSubmit }) {
}} }}
> >
<div className={styles.container}> <div className={styles.container}>
{type !== 'private' && ( <input
<input placeholder={'Téma...'}
placeholder={'Téma'} required
value={title} value={title}
onChange={(e) => { onChange={(e) => {
setTitle(e.target.value) setTitle(e.target.value)
}} }}
/> />
)}
<textarea <textarea
placeholder={'...'} placeholder={'Írj ide valamit...'}
required
value={val} value={val}
onChange={(e) => { onChange={(e) => {
setVal(e.target.value) setVal(e.target.value)
}} }}
/> />
{type === 'private' && ( <div className={styles.composerAction}>
<FileUploader
onChange={(e) => {
setFile(e.target.files[0])
}}
/>
)}
<div className={styles.typeSelector}>
<div>Post típusa:</div>
<div title="Minden felhasználó látja főoldalon, és tudnak rá válaszolni">
<input
onChange={(e) => {
setType(e.target.value)
}}
type="radio"
name="type"
value="public"
defaultChecked
/>
Publikus
</div>
<div title="Csak a weboldal üzemeltetője látja">
<input
onChange={(e) => {
setType(e.target.value)
}}
type="radio"
name="type"
value="private"
/>
Privát
</div>
<div className={styles.tip}>
(Tartsd egered opciókra több infóért)
</div>
</div>
<div className={'actions'}>
<span <span
onClick={() => { onClick={() => {
onSubmit(type, title, val, file) if (!title) {
alert('Üres a tartalom!')
return
}
if (!val) {
alert('Üres a téma!')
return
}
onSubmit(title, val)
setEditorShowing(false) setEditorShowing(false)
}} }}
> >
Post Posztolás
</span>
<span
onClick={() => {
setEditorShowing(false)
}}
>
Cancel
</span> </span>
</div> </div>
</div> </div>

View file

@ -1,32 +1,97 @@
.container { .container {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
max-width: 400px;
width: 900px;
} }
.container > input, .container > input,
.container > textarea { .container > textarea {
margin: 5px 0px; margin: 5px 0px;
padding: 4px; padding: 4px;
width: 97%;
border-color: #5d5f61;
background-color: #1f2021;
border-radius: 5px;
font-size: 15px;
}
.container > textarea {
margin-left: 1px;
width: 99%;
resize: none;
} }
.typeSelector { .typeSelector {
display: inline-flex;
text-align: center;
margin-left: 6px;
}
.typeSelector > div {
display: flex; display: flex;
align-items: center;
} }
.tip { .tip {
font-size: 10px; font-size: 11px;
margin: 0px 10px; font-style: italic;
text-align: center;
margin: 15px 0px 2px 2px;
} }
.new { .new {
display: flex;
justify-content: center;
margin: 10px 0px;
height: 19px;
width: 30%;
display: flex;
background-color: var(--hoover-color);
border: none;
color: var(--text-color);
padding: 10px; padding: 10px;
text-align: center; font-size: 15px;
border: 2px dashed var(--text-color); margin: 8px;
border-radius: 3px;
cursor: pointer; cursor: pointer;
text-shadow: 1px 1px 8px black;
transition: width 0.5s, height 0.5s, ease-in 0.5s;
text-align: center;
text-decoration: none;
} }
.new:hover { .new:hover {
background-color: var(--hoover-color); text-shadow: 2px 2px 8px black;
transition: width 0.5s, height 0.5s, ease-out 0.5s;
background-color: var(--text-color);
font-weight: bold;
color: black;
}
.composerAction {
display: flex;
align-items: center;
justify-content: center;
width: 36%;
height: 100%;
}
.composerAction > span {
margin: 2px 2px;
padding: 0px 10px;
border: 1px solid #444;
border-radius: 5px;
cursor: pointer;
user-select: none;
font-size: 15px;
margin-top: 14.5px !important;
padding-top: 4px !important;
transform: translate(109%, 10%);
transition: width 0.5s, height 0.5s, ease-in 0.5s;
}
.composerAction > span:hover {
background-color: var(--hoover-color);
transition: width 0.5s, height 0.5s, ease-out 0.5s;
color: var(--text-color);
text-shadow: 2px 1.5px 10px black;
} }

View file

@ -1,23 +1,33 @@
.container { .container {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
margin-top: 8px;
margin-bottom: 0px;
margin-right: 22%;
margin-left: 22%;
padding: 5px; padding: 5px;
} }
.listItem { .listItem {
display: flex; display: flex;
justify-content: center; justify-content: center;
text-align: center;
padding: 5px; padding-top: 10px;
padding-bottom: 10px;
margin: 3px;
cursor: pointer; cursor: pointer;
color: white; color: #a1a1a1;
transition: width 0.5s, height 0.5s, ease-in 0.5s;
} }
.listItem:hover { .listItem:hover {
background-color: var(--hoover-color); background-color: var(--hoover-color);
transition: width 0.5s, height 0.5s, ease-out 0.5s;
text-shadow: 2px 1.5px 10px black;
color: var(--text-color);
} }
.text { .text {
text-align: center; text-align: center;
color: gainsboro;
} }

View file

@ -0,0 +1,22 @@
import React from 'react'
export default function ExternalLinkIcon({ size }) {
return (
<span style={{ margin: '4px', display: 'inline-block' }}>
<svg
width={size}
height={size}
viewBox={`0 0 24 24`}
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</span>
)
}

View file

@ -0,0 +1,106 @@
import React, { useState } from 'react'
import styles from './feedbackArea.module.css'
import constants from '../constants.json'
function FileUploader({ onChange }) {
return (
<div className={styles.inputArea}>
<div className={styles.textTitle}>Fájl csatolása</div>
<input
className={styles.fileInput}
type="file"
name="file"
onChange={onChange}
/>
</div>
)
}
function submitFeedback(from, content, file) {
return new Promise((resolve) => {
const promises = [
fetch(constants.apiUrl + 'postfeedback', {
method: 'POST',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: content,
from: from,
}),
}).then((res) => {
return res.json()
}),
]
if (file) {
const formData = new FormData() // eslint-disable-line
formData.append('file', file)
promises.push(
fetch(constants.apiUrl + 'postfeedbackfile', {
method: 'POST',
credentials: 'include',
headers: {
Accept: 'application/json',
},
body: formData,
}).then((res) => {
return res.json()
})
)
}
Promise.all(promises).then((res) => {
resolve(res)
})
})
}
export default function FeedbackArea({ from, allowFile }) {
const [feedback, setFeedback] = useState('')
const [file, setFile] = useState()
return (
<div className={styles.inputArea}>
<center>
<textarea
placeholder={'Ide kezdhetsz el írni...'}
onChange={(event) => setFeedback(event.target.value)}
value={feedback}
className={styles.contactFeedback}
/>
{allowFile && (
<div className={styles.fileContainer}>
<FileUploader
onChange={(e) => {
setFile(e.target.files[0])
}}
/>
</div>
)}
</center>
<center>
<div className={`${styles.send} buttonContainer`}>
<div
onClick={() => {
if (feedback) {
submitFeedback(from, feedback, file).then(() => {
alert('Üzenet elküldve!')
setFeedback('')
})
} else {
alert('Üresen hagytad az üzenet szövegének helyét!')
}
}}
>
Küldés
</div>
</div>
</center>
</div>
)
}

View file

@ -0,0 +1,25 @@
.contactFeedback {
color: var(--text-color);
background-color: var(--background-color);
font-size: 14px;
width: 600px;
box-sizing: border-box;
height: 150px;
resize: none;
margin-top: 15px;
padding: 5px;
}
.send {
width: 20%;
margin-bottom: 60px;
}
.inputArea {
display: block;
}
.fileContainer {
width: 400px;
margin: 5px;
}

View file

@ -9,27 +9,120 @@ import tabs from '../data/tabs.json'
import constants from '../constants.json' import constants from '../constants.json'
import BB from './b.js' import BB from './b.js'
import styles from './layout.module.css'
const renderSnow = () => { const renderSnow = () => {
const date = new Date() const date = new Date()
// if its december, and date is more than 5 // if its december, and date is more than 5
return date.getMonth() === 11 && date.getDate() > 5 return date.getMonth() === 11 && date.getDate() > 5
} }
function Donate() {
return (
<div>
<div className={styles.modalHead}>Donate</div>
<div style={{ textAlign: 'justify', margin: '8px' }}>
Paypalen és Patreonon látszódik a neved, de Patreonon könnyen meg lehet
változtatni. Ha név nélkül szeretnél adakozni, akkor írd át egy
nickname-re. De akárhova adakozol, a neved sehova se lesz kiadva, és nem
látja 3. személy.
</div>
<div className={styles.donateLogoContainer}>
<a
href={`${constants.siteUrl}patreon`}
target="_blank"
rel="noreferrer"
>
<img
style={{
border: 'none',
margin: '15px',
}}
src={`${constants.siteUrl}img/patreon-logo.png`}
/>
</a>
<a href={`${constants.siteUrl}donate`} target="_blank" rel="noreferrer">
<img
style={{
border: 'none',
margin: '15px',
}}
src={`${constants.siteUrl}img/paypal-logo.png`}
/>
</a>
</div>
</div>
)
}
function MessageButton({
userSpecificMotd,
setShowMotdModal,
refetchGlobalData,
}) {
return (
<div className="msgs">
<div
onClick={() => {
if (!userSpecificMotd) {
return
}
setShowMotdModal(true)
if (userSpecificMotd.seen) {
return
}
fetch(constants.apiUrl + 'infos', {
method: 'POST',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
userSpecificMotdSeen: true,
}),
})
.then((resp) => {
return resp.json()
})
.then(() => {
refetchGlobalData()
})
}}
style={{ cursor: userSpecificMotd ? 'pointer' : 'default' }}
title={
userSpecificMotd && !userSpecificMotd.seen ? 'Új üzeneted van!' : ''
}
>
{userSpecificMotd && !userSpecificMotd.seen ? '📬' : '📭'}
</div>
</div>
)
}
export default function Layout({ export default function Layout({
children, children,
route, router,
globalData, globalData,
refetchGlobalData, refetchGlobalData,
}) { }) {
let href = route
const [sidebarOpen, setSidebarOpen] = useState(true) const [sidebarOpen, setSidebarOpen] = useState(true)
const [windowSize, setWindowSize] = useState([100, 200]) const [windowSize, setWindowSize] = useState([100, 200])
const [showMotdModal, setShowMotdModal] = useState(false) const [showMotdModal, setShowMotdModal] = useState(false)
const [donateShowing, setDonateShowing] = useState(false)
const [showNewMsgModal, setShowNewMsgModal] = useState(true) const [showNewMsgModal, setShowNewMsgModal] = useState(true)
const userId = globalData.userId const userId = globalData.userId
const userSpecificMotd = globalData.userSpecificMotd const userSpecificMotd = globalData.userSpecificMotd
useEffect(
() => {
setDonateShowing(!!router.query.donate)
},
[router.query.donate]
)
let href = router.route
if (href === '/' || href === '') { if (href === '/' || href === '') {
href = 'index' href = 'index'
} }
@ -80,23 +173,33 @@ export default function Layout({
<div /> <div />
</span> </span>
<div className="sidebarheader"> <div className="sidebarheader">
<img <Link href="/">
style={{ maxWidth: '100%' }} <a>
src={`${constants.siteUrl}img/frylabs-logo_small_transparent.png`} <img
alt="Frylabs" style={{
/> maxWidth: '100%',
border: 'none',
margin: '1px',
}}
src={`${
constants.siteUrl
}img/frylabs-logo_small_transparent.png`}
alt="FryLabs"
/>
</a>
</Link>
</div> </div>
</div> </div>
{sidebarOpen ? ( {sidebarOpen ? (
<> <>
<div id="sideBarLinks" className="sidebarLinks"> <div id="sideBarLinks" className={styles.sidebarLinks}>
{Object.keys(tabs).map((key) => { {Object.keys(tabs).map((key) => {
const item = tabs[key] const item = tabs[key]
return ( return (
<Link href={item.href} key={key}> <Link href={item.href} key={key}>
<a <a
onClick={closeSideBar} onClick={closeSideBar}
className={href.includes(key) ? 'active' : undefined} className={href.includes(key) ? styles.active : undefined}
id={item.id || undefined} id={item.id || undefined}
> >
{item.text} {item.text}
@ -105,68 +208,26 @@ export default function Layout({
) )
})} })}
<a <a
href={`/dataeditor`} onClick={() => {
onClick={closeSideBar} closeSideBar()
target="_blank" setDonateShowing(true)
rel="noreferrer" }}
title={
'Kérdések szerkesztése, törlése, beküldése, és kitöltetlen tesztek'
}
>
Kérdés szerkesztő / kitöltetlen tesztek
</a>
<a
href={`/donate`}
className="donate"
onClick={closeSideBar}
target="_blank"
rel="noreferrer"
> >
Donate Donate
</a> </a>
</div> </div>
<div className="userStatus"> <div className={'userStatus'}>
<div className="msgs"> <MessageButton
<div userSpecificMotd={userSpecificMotd}
onClick={() => { setShowMotdModal={setShowMotdModal}
if (!userSpecificMotd) { refetchGlobalData={refetchGlobalData}
return />
} <div className={'uid'} title="User ID">
setShowMotdModal(true) UID: {userId || '...'}
if (userSpecificMotd.seen) {
return
}
fetch(constants.apiUrl + 'infos', {
method: 'POST',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
userSpecificMotdSeen: true,
}),
})
.then((resp) => {
return resp.json()
})
.then(() => {
refetchGlobalData()
})
}}
style={{ cursor: userSpecificMotd ? 'pointer' : 'default' }}
title={
userSpecificMotd && !userSpecificMotd.seen
? "You've got Mail!"
: ''
}
>
{userSpecificMotd && !userSpecificMotd.seen ? '📬' : '📭'}
</div>
<div title="User ID">UID: {userId || '...'}</div>
</div> </div>
<div <div
className="logout" className={'logout'}
title="Kijelentkezés"
onClick={() => { onClick={() => {
fetch(constants.apiUrl + 'logout', { fetch(constants.apiUrl + 'logout', {
method: 'GET', method: 'GET',
@ -182,31 +243,38 @@ export default function Layout({
Logout Logout
</div> </div>
</div> </div>
{showMotdModal ? (
<Modal
closeClick={() => {
setShowMotdModal(false)
}}
>
<div style={{ textAlign: 'center' }}>Üzenet admintól:</div>
<div
dangerouslySetInnerHTML={{ __html: userSpecificMotd.msg }}
/>
</Modal>
) : null}
</> </>
) : null} ) : null}
</div> </div>
<div className="content">{children}</div> {donateShowing ? (
<Modal
closeClick={() => {
setDonateShowing(false)
}}
>
<Donate />
</Modal>
) : null}
{showMotdModal ? (
<Modal
closeClick={() => {
setShowMotdModal(false)
}}
>
<div style={{ textAlign: 'center' }}>Üzenet admintól:</div>
<div dangerouslySetInnerHTML={{ __html: userSpecificMotd.msg }} />
</Modal>
) : null}
{userSpecificMotd && !userSpecificMotd.seen && showNewMsgModal ? ( {userSpecificMotd && !userSpecificMotd.seen && showNewMsgModal ? (
<Modal <Modal
closeClick={() => { closeClick={() => {
setShowNewMsgModal(false) setShowNewMsgModal(false)
}} }}
> >
Új üzeneted van, kattints 📬-ra bal alul megtekintéséhez! Új üzeneted van, kattints a 📬-ra bal alul a megtekintéséhez!
</Modal> </Modal>
) : null} ) : null}
<div className="content">{children}</div>
<BB /> <BB />
</div> </div>
) )

View file

@ -0,0 +1,49 @@
.modalHead {
text-align: center;
font-size: 22px;
font-weight: 700;
padding-top: 0px;
margin-top: 0px;
padding-bottom: 20px;
letter-spacing: 3px;
}
.donateLogoContainer {
display: flex;
justify-content: center;
}
.donateLogoContainer img {
max-width: 100px;
margin: 5px;
border-radius: 5px;
}
.sidebarLinks > * {
display: block;
text-align: center;
color: black;
font-size: 108%;
padding: 14px;
margin-top: 4px;
margin-bottom: 4px;
text-decoration: none;
color: var(--bright-color);
transition: width 0.5s, height 0.5s, ease-in 0.5s;
cursor: pointer;
}
.sidebarLinks a.active {
border: 0.5px solid var(--text-color);
color: white;
text-shadow: 2px 2px 8px black;
font-weight: bold;
}
.sidebarLinks a:hover:not(.active) {
background-color: var(--text-color);
color: black;
font-weight: bold;
text-shadow: 2px 2px 8px black;
transition: width 0.5s, height 0.5s, ease-out 0.5s;
}

View file

@ -12,7 +12,9 @@
display: flex; display: flex;
align-items: stretch; align-items: stretch;
max-height: 80%; max-height: 80%;
width: 80%; width: 50%;
max-width: 52%;
width: auto;
position: fixed; position: fixed;
background: var(--background-color); background: var(--background-color);
height: auto; height: auto;
@ -20,24 +22,27 @@
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
border-radius: 8px; border-radius: 8px;
padding-top: 38px;
padding: 20px 30px; padding-bottom: 25px;
cursor: auto; padding-right: 14px;
padding-left: 16px;
cursor: default;
} }
.close { .close {
cursor: pointer; cursor: pointer;
font-size: 18px; font-size: 16.5px;
position: absolute; position: fixed;
top: 5px;
top: 10px;
right: 10px; right: 10px;
margin-bottom: 10px;
text-align: right;
display: inline; display: inline;
} }
.children { .children {
max-height: 100%; max-height: 100%;
width: 100%; width: 100%;
overflow-y: scroll; overflow-y: auto;
overflow-x: hidden; overflow-x: auto;
} }

View file

@ -17,13 +17,17 @@ export default function NewsEntry({
const { reacts, title, content, user, comments, date, admin } = newsItem const { reacts, title, content, user, comments, date, admin } = newsItem
return ( return (
<div> <div className={styles.newsRoot}>
<div className={`${styles.newsContainer} ${admin && styles.adminPost}`}> <div
className={`${styles.newsContainer} ${admin &&
styles.adminPost} ${!admin && styles.userPost} ${uid == user &&
styles.ownPost} ${uid == user && admin && styles.adminPost}`}
>
<div className={styles.newsHeader}> <div className={styles.newsHeader}>
<div className={styles.newsTitle}>{title}</div> <div className={styles.newsTitle}>{title}</div>
<div className={styles.user}> <div className={styles.userinfo}>
<div>User #{user}</div> <div>User #{user}</div>
<div className={styles.newsDate}>{date}</div> <div className={styles.newsDate}> @ {date}</div>
</div> </div>
</div> </div>
{admin ? ( {admin ? (
@ -38,11 +42,12 @@ export default function NewsEntry({
<div className={'actions'}> <div className={'actions'}>
{uid === user ? ( {uid === user ? (
<span <span
className={styles.delete_bttn}
onClick={() => { onClick={() => {
onPostDelete() onPostDelete()
}} }}
> >
Delete Törlés
</span> </span>
) : null} ) : null}
<ReactButton <ReactButton
@ -62,8 +67,6 @@ export default function NewsEntry({
onDelete={onDelete} onDelete={onDelete}
comments={comments} comments={comments}
/> />
<hr />
<hr />
</div> </div>
) )
} }

View file

@ -1,32 +1,51 @@
.newsBody { .newsBody {
margin: 0px 5px; margin: 0px 12px;
font-size: 18px;
color: #fff; color: #fff;
white-space: pre-line; white-space: pre-line;
text-align: justify;
} }
.newsTitle { .newsTitle {
font-size: 20px; font-size: 25px;
color: var(--text-color); color: var(--text-color);
margin: 0px 5px; margin: 0px 11px;
padding-bottom: 10px;
} }
.newsDate { .newsDate {
margin: 0px 5px; margin-right: 12px;
margin-left: 14px;
} }
.newsContainer { .newsContainer {
margin: 5px 5px; margin: 5px 5px;
} }
.delete_bttn {
font-size: 15px;
margin-top: 12px !important;
margin-left: 2px !important;
margin-right: 5px !important;
padding-top: 4px !important;
transition: width 0.5s, height 0.5s, ease-in 0.5s;
}
.adminPost { .adminPost {
border-left: 2px solid yellow; border-left: 4px solid var(--text-color);
border-radius: 5px; }
.userPost {
border-left: 4px solid azure;
}
.ownPost {
border-left: 4px dotted azure;
} }
.newsContainer img { .newsContainer img {
max-width: 100%; max-width: 100%;
min-width: 200px; min-width: 200px;
border: 2px solid white;
} }
.newsHeader { .newsHeader {
@ -35,7 +54,17 @@
align-items: center; align-items: center;
} }
.user { .userinfo {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 12px;
}
.newsRoot {
background-color: var(--dark-color);
margin-left: 8px;
margin-right: 8px;
margin-bottom: 16px;
margin-top: 16px;
padding: 10px;
} }

View file

@ -23,7 +23,7 @@ function useOutsideAlerter(ref, action) {
function ExistingReacts({ existingReacts, onClick, uid }) { function ExistingReacts({ existingReacts, onClick, uid }) {
return ( return (
<div className={styles.reactionContainer}> <div className={styles.reactionContainer}>
<div>React</div> <div className={styles.react_bttn}>Reakció</div>
{existingReacts && {existingReacts &&
Object.keys(existingReacts).map((key) => { Object.keys(existingReacts).map((key) => {
const currReact = existingReacts[key] const currReact = existingReacts[key]
@ -33,7 +33,7 @@ function ExistingReacts({ existingReacts, onClick, uid }) {
} }
return ( return (
<div <div
title={currReact.join(', ')} title={`'${key}': ${currReact.join(', ')}`}
className={`${currReact.includes(uid) && styles.reacted}`} className={`${currReact.includes(uid) && styles.reacted}`}
key={key} key={key}
onClick={(e) => { onClick={(e) => {

View file

@ -1,6 +1,8 @@
.reactionContainer { .reactionContainer {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 15px !important;
margin: 4px 2px;
} }
.reactionContainer > input { .reactionContainer > input {
@ -20,10 +22,13 @@
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
transition: width 0.5s, height 0.5s, ease-in 0.5s;
} }
.reactionContainer > div:hover { .reactionContainer > div:hover {
background-color: var(--hoover-color); background-color: var(--hoover-color);
color: gainsboro;
transition: width 0.5s, height 0.5s, ease-out 0.5s;
} }
.break { .break {
@ -32,9 +37,20 @@
} }
.reacted { .reacted {
color: yellow; background-color: var(--text-color);
color: black;
}
.reacted:hover {
background-color: #96810b !important;
color: #42423e !important;
} }
.reactContainer { .reactContainer {
display: inline-block; display: inline-block;
} }
.react_bttn {
font-size: 15px;
padding-top: 3px !important;
}

View file

@ -1,20 +0,0 @@
import React from 'react'
import styles from './sidebar.module.css'
export default function Sidebar(props) {
const { onClose } = props
return (
<div className={styles.container}>
<div
className={styles.closeBorder}
onClick={() => {
onClose()
}}
>
{'⏩'}
</div>
<div className={styles.content}>{props.children}</div>
</div>
)
}

View file

@ -1,31 +0,0 @@
.container {
background-color: var(--background-color);
border-left: 3px solid #99f;
top: 0;
right: 0px;
height: 100%;
width: 400px;
max-width: 100%;
position: fixed;
display: flex;
}
.closeBorder {
font-size: 14px;
padding: 2px;
cursor: pointer;
display: flex;
flex-flow: column;
justify-content: center;
color: white;
background-color: #222;
}
.closeBorder:hover {
background-color: #333;
}
.content {
width: 100%;
}

View file

@ -1,6 +1,6 @@
import constants from '../constants.json' import constants from '../constants.json'
export default function Sleep (props) { export default function Sleep(props) {
const hours = new Date().getHours() const hours = new Date().getHours()
if (hours < 4 || hours > 23) { if (hours < 4 || hours > 23) {
return ( return (
@ -9,9 +9,11 @@ export default function Sleep (props) {
<img <img
style={{ style={{
margin: '10px', margin: '10px',
width: '300px' width: '300px',
border: '2px solid white',
}} }}
src={constants.siteUrl + 'img/aludni.jpeg'} src={constants.siteUrl + 'img/aludni.jpeg'}
title="Ezt a képet azert látod, mert ilyenkor már igazán nem ezen az oldalon kellene járnod"
/> />
</center> </center>
</div> </div>

View file

@ -3,20 +3,7 @@
padding: 4px; padding: 4px;
} }
.groupContainer { .todoButtons {
display: flex; height: 38px;
} text-wrap: normal;
.group {
padding: 8px 16px;
cursor: pointer;
user-select: none;
}
.group:hover {
background-color: #333;
}
.selectedGroup {
background-color: #444;
} }

View file

@ -11,6 +11,7 @@
.categoryName { .categoryName {
text-align: center; text-align: center;
margin-top: 85px !important;
margin: 5px 0px; margin: 5px 0px;
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;

View file

@ -11,7 +11,7 @@ export default function TodoCard(props) {
<div <div
className={styles.card} className={styles.card}
style={{ style={{
border: `2px solid ${group || '#99f'}`, border: `1px solid ${categories[category].color || '#f2cb05'}`,
}} }}
onClick={() => { onClick={() => {
onClick(props.cardData) onClick(props.cardData)
@ -27,7 +27,7 @@ export default function TodoCard(props) {
backgroundColor: categories[category].color, backgroundColor: categories[category].color,
color: 'white', color: 'white',
borderRadius: '2px', borderRadius: '2px',
padding: '0px 2px', padding: '3px',
}} }}
> >
{categories[category].name} {categories[category].name}
@ -35,9 +35,9 @@ export default function TodoCard(props) {
</div> </div>
<div className={styles.numbers}> <div className={styles.numbers}>
<div className={`${voted && styles.voted}`}> <div className={`${voted && styles.voted}`}>
<div>{`Votes: ${votes.length}`}</div> <div>{`Szavazatok: ${votes.length}`}</div>
</div> </div>
<div>{points}</div> <div>{`Nehézség: ${points}`}</div>
</div> </div>
</div> </div>
) )

View file

@ -1,12 +1,14 @@
.card { .card {
border-radius: 2px; border-radius: 5px;
padding: 5px; padding: 7px;
margin: 6px 3px; margin: 8px 4px;
cursor: pointer; cursor: pointer;
background-color: #171616;
} }
.voted { .voted {
color: #cf9; color: var(--text-color);
font-weight: 600;
} }
.card:hover { .card:hover {
@ -15,7 +17,7 @@
} }
.card > div { .card > div {
margin: 5px 0px; margin: 6px 4px;
} }
.description { .description {
@ -25,7 +27,7 @@
} }
.category { .category {
font-size: 10px; font-size: 12px;
white-space: nowrap; white-space: nowrap;
} }
@ -39,6 +41,6 @@
} }
.id { .id {
margin: 0px 3px; margin: 1px 6px 1px 1px;
color: #999; color: #999;
} }

View file

@ -6,6 +6,8 @@ export default function TodoRow(props) {
const { categories, userId, onClick } = props const { categories, userId, onClick } = props
const { name, category, votes, id, group } = props.rowData const { name, category, votes, id, group } = props.rowData
const voted = votes.includes(userId) const voted = votes.includes(userId)
const borderColor =
categories[category].borderColor || categories[category].color
return ( return (
<div <div
@ -14,7 +16,8 @@ export default function TodoRow(props) {
}} }}
className={styles.row} className={styles.row}
style={{ style={{
border: `2px solid ${group || '#99f'}`, border: `2px dashed ${borderColor || 'white'}`,
borderRadius: '3px',
}} }}
> >
<div className={styles.id}>{`#${id}`}</div> <div className={styles.id}>{`#${id}`}</div>
@ -23,11 +26,11 @@ export default function TodoRow(props) {
<div <div
style={{ style={{
wordBreak: 'break-all', wordBreak: 'break-all',
fontSize: '10px', fontSize: '12px',
backgroundColor: categories[category].color, backgroundColor: categories[category].color,
color: 'white', color: 'white',
borderRadius: '2px', borderRadius: '2px',
padding: '0px 2px', padding: '3px',
}} }}
> >
{categories[category].name} {categories[category].name}
@ -35,7 +38,7 @@ export default function TodoRow(props) {
</div> </div>
<div <div
className={`${styles.votes} ${voted && styles.voted}`} className={`${styles.votes} ${voted && styles.voted}`}
>{`Votes: ${votes.length}`}</div> >{`Szavazatok: ${votes.length}`}</div>
</div> </div>
) )
} }

View file

@ -1,10 +1,9 @@
.row { .row {
display: flex; border-radius: 5px;
border: 2px solid #99f; padding: 7px;
border-radius: 2px; margin: 8px 4px;
padding: 5px;
margin: 6px 3px;
cursor: pointer; cursor: pointer;
background-color: #171616;
} }
.row:hover { .row:hover {
@ -13,12 +12,12 @@
} }
.row > div { .row > div {
margin: 0px 5px; margin: 6px 4px;
} }
.id { .id {
flex: 0 20px; flex: 0 20px;
margin: 0px 3px; margin: 1px 6px 1px 1px;
color: #999; color: #999;
} }
@ -30,10 +29,14 @@
} }
.votes { .votes {
display: flex;
justify-content: space-between;
font-size: 12px;
} }
.voted { .voted {
color: #cf9; color: var(--text-color);
font-weight: 600;
} }
.catName { .catName {

View file

@ -25,18 +25,18 @@ export default function Todos({
// TODO: hide vote button if not voteable // TODO: hide vote button if not voteable
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.name}> <div className={styles.title}>
<span className={styles.id}>#{id}</span> <span className={styles.id}>#{id}</span>
{name} <div className={'subtitle'} style={{ textAlign: 'left' }}>
{name}
</div>
<span <span
style={{ style={{
margin: '0px 5px',
fontSize: '10px',
backgroundColor: categories[category].color, backgroundColor: categories[category].color,
color: 'white', color: 'white',
fontSize: '12px',
borderRadius: '2px', borderRadius: '2px',
padding: '0px 2px', padding: '3px',
whiteSpace: 'nowrap',
}} }}
> >
{categories[category].name} {categories[category].name}
@ -45,7 +45,7 @@ export default function Todos({
<hr /> <hr />
{group && ( {group && (
<div className={styles.row}> <div className={styles.row}>
<div>Csoport:</div> <div style={{ color: group }}>Csoport:</div>
<div style={{ color: group }}> <div style={{ color: group }}>
{namedGroups[group] ? namedGroups[group].name : group} {namedGroups[group] ? namedGroups[group].name : group}
</div> </div>
@ -64,31 +64,47 @@ export default function Todos({
<div>{votes.length}</div> <div>{votes.length}</div>
</div> </div>
{gitlink && ( {gitlink && (
<div className={styles.row}> <center>
<a href={gitlink} target={'_blank'} rel={'noreferrer'}> <div>
Git link <a
</a> href={gitlink}
</div> style={{ textDecoration: 'none' }}
target={'_blank'}
rel={'noreferrer'}
>
Git link
</a>
</div>
</center>
)} )}
<hr /> <hr />
{description && ( {description && (
<> <>
<div className={styles.title}>Leírás</div> <div className={'subtitle'} style={{ textAlign: 'left' }}>
<div dangerouslySetInnerHTML={{ __html: description }} /> Leírás
</div>
<div
className={styles.description}
dangerouslySetInnerHTML={{ __html: description }}
/>
<hr /> <hr />
</> </>
)} )}
<div className={styles.category}></div> <div className={styles.category} />
{voteable && ( {voteable && (
<div <center>
className={`${styles.button} ${votes.includes(userId) && <div className={'buttonContainer'} style={{ width: '40%' }}>
styles.voted}`} <div
onClick={() => { className={`${styles.button} ${votes.includes(userId) &&
voteOn(card) styles.voted}`}
}} onClick={() => {
> voteOn(card)
{votes.includes(userId) ? 'Szavazat visszavonása' : 'Szavazás'} }}
</div> >
{votes.includes(userId) ? 'Szavazat visszavonása' : 'Szavazás'}
</div>
</div>
</center>
)} )}
</div> </div>
) )

View file

@ -9,14 +9,17 @@
} }
.title { .title {
color: #99f; color: var(--text-color);
font-size: 18px; font-size: 18px;
} }
.name { .title {
color: #99f; color: var(--text-color);
font-size: 20px; font-size: 20px;
margin: 20px 0px; margin: 5px 0px;
}
.name {
} }
.category { .category {
@ -25,18 +28,26 @@
} }
.id { .id {
font-size: 16px; font-size: 20px;
margin: 0px 3px; margin: 0px 3px;
color: #999; color: #999;
} }
.description {
overflow-x: auto;
overflow-y: auto;
max-height: calc(38vh);
margin: 5px;
}
.row { .row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin: 2px 10px;
} }
.button { .button {
border: 2px solid #99f; border: 2px solid var(--text-color);
border-radius: 3px; border-radius: 3px;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;

View file

@ -20,29 +20,31 @@ export default function TodoBoard(props) {
return ( return (
<div className={styles.container} key={key}> <div className={styles.container} key={key}>
<div className={styles.title}>{table.name}</div> <div className={styles.title}>{table.name}</div>
{tableCards.map((card, i) => { <div className={styles.scroll}>
const shouldHide = {tableCards.map((card, i) => {
card.state !== key || const shouldHide =
(selectedGroup !== null && card.state !== key ||
selectedGroup !== 'uncat' && (selectedGroup !== null &&
card.group !== selectedGroup) || selectedGroup !== 'uncat' &&
(selectedGroup === 'uncat' && card.group !== undefined) card.group !== selectedGroup) ||
(selectedGroup === 'uncat' && card.group !== undefined)
if (shouldHide) { if (shouldHide) {
return null return null
} }
return ( return (
<TodoRow <TodoRow
onClick={onClick} onClick={onClick}
key={i} key={i}
type={key} type={key}
rowData={card} rowData={card}
userId={userId} userId={userId}
categories={categories} categories={categories}
/> />
) )
})} })}
</div>
</div> </div>
) )
})} })}

View file

@ -11,6 +11,8 @@
.container { .container {
flex-direction: column; flex-direction: column;
margin-top: 30px;
margin-bottom: 50px;
} }
.title { .title {
@ -18,3 +20,9 @@
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: bold;
} }
.scroll {
max-height: 500px;
overflow-y: auto;
overflow-x: hidden;
}

View file

@ -4,11 +4,9 @@ import LoadingIndicator from '../LoadingIndicator.js'
import TodoBoard from './todoBoard.js' import TodoBoard from './todoBoard.js'
import TodoTable from './todoTable.js' import TodoTable from './todoTable.js'
import TodoSidebar from './todoSidebar.js' import TodoSidebar from './todoSidebar.js'
import Modal from '../modal.js'
import Sidebar from '../sidebar.js'
import styles from './todo.module.css' import styles from './todo.module.css'
// import styles from './todos.module.css'
import constants from '../../constants.json' import constants from '../../constants.json'
const byVotes = (a, b) => { const byVotes = (a, b) => {
@ -26,7 +24,6 @@ export default function Todos() {
const [selectedGroup, setSelectedGroup] = useState(null) const [selectedGroup, setSelectedGroup] = useState(null)
useEffect(() => { useEffect(() => {
console.info('Fetching todos')
fetch(`${constants.apiUrl}todos`, { fetch(`${constants.apiUrl}todos`, {
credentials: 'include', credentials: 'include',
}) })
@ -96,13 +93,14 @@ export default function Todos() {
const sg = namedGroups[selectedGroup] const sg = namedGroups[selectedGroup]
return ( return (
<> <>
<div className={styles.groupContainer}> <div className={'buttonContainer'}>
{groups.map((group) => { {groups.map((group) => {
const namedGroup = namedGroups[group] const namedGroup = namedGroups[group]
return ( return (
<div <div
className={`${styles.group} ${group === selectedGroup && className={`${'buttonContainer'}, ${
styles.selectedGroup}`} styles.todoButtons
} ${group === selectedGroup && 'activeButton'}`}
key={group} key={group}
onClick={() => { onClick={() => {
setSelectedGroup(group) setSelectedGroup(group)
@ -113,8 +111,9 @@ export default function Todos() {
) )
})} })}
<div <div
className={`${styles.group} ${'uncat' === selectedGroup && className={`${'buttonContainer'}, ${
styles.selectedGroup}`} styles.todoButtons
} ${'uncat' === selectedGroup && 'activeButton'}`}
onClick={() => { onClick={() => {
setSelectedGroup('uncat') setSelectedGroup('uncat')
}} }}
@ -122,7 +121,8 @@ export default function Todos() {
{'Kategorizálatlan'} {'Kategorizálatlan'}
</div> </div>
<div <div
className={styles.group} className={`${'buttonContainer'}, ${styles.todoButtons} ${null ===
selectedGroup && 'activeButton'}`}
onClick={() => { onClick={() => {
setSelectedGroup(null) setSelectedGroup(null)
}} }}
@ -140,8 +140,8 @@ export default function Todos() {
return ( return (
<div> <div>
{sidebarCard && ( {sidebarCard && (
<Sidebar <Modal
onClose={() => { closeClick={() => {
setSidebarCard(null) setSidebarCard(null)
}} }}
> >
@ -165,15 +165,10 @@ export default function Todos() {
}) })
}} }}
/> />
</Sidebar> </Modal>
)} )}
{renderGrouper()} {renderGrouper()}
<div <div>
style={{
// width: '100%',
marginRight: sidebarCard ? 380 : 0,
}}
>
<TodoBoard <TodoBoard
columns={cols} columns={cols}
cards={bCards} cards={bCards}

View file

@ -1,7 +1,6 @@
{ {
"siteUrl": "https://qmining.frylabs.net/", "siteUrl": "https://qmining.frylabs.net/",
"apiUrl": "https://api.frylabs.net/", "apiUrl": "https://api.frylabs.net/",
"devApiUrl": "http://localhost:8080/",
"mobileWindowWidth": 700, "mobileWindowWidth": 700,
"maxQuestionsToRender": 250 "maxQuestionsToRender": 250
} }

View file

@ -1,8 +1,5 @@
{ {
"header": [ "header": ["Miben", "Hogy"],
"Miben",
"Hogy"
],
"rows": { "rows": {
"feedbacker": { "feedbacker": {
"name": "Visszajelzésben", "name": "Visszajelzésben",

View file

@ -1,22 +0,0 @@
{
"install": {
"href": "/install",
"text": "Install"
},
"allqr": {
"href": "/allqr.txt",
"text": "Összes kérdés TXT"
},
"data": {
"href": "/data.json",
"text": "Összes kérdés JSON"
},
"irc": {
"href": "/irc?index",
"text": "IRC chat"
},
"dataeditor": {
"href": "/dataeditor?index",
"text": "Dataeditor"
}
}

View file

@ -1,26 +1,34 @@
{ {
"index": { "index": {
"href": "/", "href": "/",
"text": "Home" "text": "Főoldal"
}, },
"manual": { "script": {
"href": "/manual", "href": "/script",
"text": "Manual" "text": "Script"
}, },
"allQuestions": { "allQuestions": {
"href": "/allQuestions", "href": "/allQuestions",
"text": "Kérdések és tárgyak" "text": "Kérdések és tárgyak"
}, },
"pwRequest": {
"href": "/pwRequest",
"text": "Jelszó kérés"
},
"contribute": { "contribute": {
"href": "/contribute", "href": "/contribute",
"text": "Todos, contribute" "text": "Teendők"
}, },
"ranklist": { "ranklist": {
"href": "/ranklist", "href": "/ranklist",
"text": "Ranklista" "text": "Ranklista"
},
"pwRequest": {
"href": "/pwRequest",
"text": "Jelszó generálás"
},
"faq": {
"href": "/faq",
"text": "GYIK"
},
"contact": {
"href": "/contact",
"text": "Kapcsolat"
} }
} }

View file

@ -1,25 +1,43 @@
:root { :root {
--text-color: #9999ff; --text-color: #f2cb05;
--primary-color: #9999ff; --primary-color: #f2cb05;
--bright-color: #f2f2f2; --bright-color: #f2f2f2;
--background-color: #222426; --background-color: #222426;
--hoover-color: #202020; --hoover-color: #393939;
--dark-color: #191919;
} }
@import url('https://fonts.googleapis.com/css2?family=Kameron&family=Overpass+Mono:wght@300;400&display=swap');
body { body {
font: normal 14px Verdana; font-family: 'Kameron', serif;
font-family: 'Overpass Mono', monospace;
color: #999999; color: #999999;
cursor: default;
}
img {
margin: 10px;
border: 2px solid white;
}
li {
padding: 2px 0px;
} }
a { a {
color: white; color: white;
} }
a:hover {
color: #c1c1c1;
}
textarea { textarea {
color: var(--text-color); color: var(--text-color);
background-color: var(--background-color); background-color: var(--background-color);
box-sizing: border-box; box-sizing: border-box;
height: 120px; height: 140px;
width: 100%; width: 100%;
border: 1px solid #666; border: 1px solid #666;
border-radius: 3px; border-radius: 3px;
@ -28,8 +46,16 @@ textarea {
input { input {
color: var(--text-color); color: var(--text-color);
background-color: var(--background-color); background-color: var(--background-color);
border: 1px solid #666;
border-radius: 5px;
width: 80%;
font-family: inherit;
border: 1px solid #444; border: 1px solid #444;
border-radius: 3px; width: 98%;
}
input:focus {
border: 0px solid #444;
} }
.link { .link {
@ -45,7 +71,6 @@ input {
.sidebar { .sidebar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 200px; width: 200px;
@ -57,25 +82,7 @@ input {
.content { .content {
margin-left: 200px; margin-left: 200px;
padding: 1px 16px; padding: 1px 15px;
}
.sidebarLinks a {
display: block;
color: black;
padding: 16px;
text-decoration: none;
color: var(--bright-color);
}
.sidebarLinks a.active {
background-color: var(--text-color);
color: black;
}
.sidebarLinks a:hover:not(.active) {
background-color: #555;
color: white;
} }
.menuicon div { .menuicon div {
@ -99,7 +106,11 @@ input {
align-items: center; align-items: center;
flex-wrap: nowrap; flex-wrap: nowrap;
position: relative; position: relative;
margin: 10px; overflow: hidden;
padding-top: 15px;
padding-bottom: 17px;
padding-right: 2px;
padding-left: 2px;
} }
@media screen and (max-width: 700px) { @media screen and (max-width: 700px) {
@ -108,9 +119,11 @@ input {
height: auto; height: auto;
position: relative; position: relative;
} }
.sidebar a { .sidebar a {
float: left; float: left;
} }
div.content { div.content {
margin-left: 0; margin-left: 0;
} }
@ -139,6 +152,7 @@ input {
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 50%; top: 50%;
padding-top: 20px;
transform: translateX(-50%) translateY(-50%); transform: translateX(-50%) translateY(-50%);
} }
@ -153,8 +167,10 @@ input {
} }
.rtfmImage { .rtfmImage {
text-align: center;
display: flex; display: flex;
justify-content: center; justify-content: center;
border: 2px solid white;
width: 100%; width: 100%;
} }
@ -164,19 +180,25 @@ input {
} }
} }
.endofpage {
padding-bottom: 20px;
}
.questionContainer { .questionContainer {
margin: 10px; margin: 6px;
padding: 10px;
} }
.questionContainer:hover { .questionContainer:hover {
background-color: var(--hoover-color); background-color: #141414;
color: var(--text-color);
} }
.question { .question {
word-wrap: break-word; word-wrap: break-word;
font-weight: bold; font-weight: bold;
font-size: 17px; font-size: 17px;
color: #ffffff; color: gainsboro;
} }
.answer { .answer {
@ -193,7 +215,6 @@ input {
.loadingindicator { .loadingindicator {
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
color: #fff; color: #fff;
font-size: 30px; font-size: 30px;
} }
@ -203,16 +224,22 @@ input {
} }
.subjectSelector { .subjectSelector {
overflow: scroll; overflow: auto;
height: 350px; height: auto;
max-height: 250px;
margin: 10px; margin: 10px;
padding: 5px;
padding-right: 10px;
padding-left: 8px;
background-color: #141414;
} }
.subjItem { .subjItem {
font-size: 18px; font-size: 18px;
padding: 3px; padding: 3px;
padding-top: 5px;
margin-top: 4px;
cursor: pointer; cursor: pointer;
float: 1;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
@ -227,47 +254,83 @@ input {
color: white; color: white;
} }
.donate {
background-color: #222a26;
}
.rtfmImage { .rtfmImage {
text-align: center;
justify-content: center; justify-content: center;
margin: 0px 10px; margin: 0px 10px;
} }
.warning {
color: red;
font-weight: 100;
font-size: 17.5px;
}
#manualWarn {
font-size: 14px;
}
.manual_img {
padding: 20px 2px;
}
.subtitle {
color: var(--text-color);
font-size: 23px;
text-align: center;
font-weight: 100;
margin: 0px;
padding-top: 8px;
}
.pageHeader {
background-color: var(--text-color);
height: 45px;
max-width: 100%;
color: black !important;
margin: 5px 0px 0px 0px;
}
.pageHeader > h1 {
padding-top: 6px;
letter-spacing: 7px;
text-align: center;
margin: 0px;
}
.manualUsage { .manualUsage {
margin-top: 5px;
display: flex; display: flex;
} }
select { .manualBody {
cursor: pointer; text-align: justify;
width: 100%;
max-width: 100%;
height: 30px;
}
select:hover {
border: 1px solid #99f;
} }
.userStatus { .userStatus {
display: flex; display: flex;
margin-top: auto; margin-top: auto;
margin-bottom: 20px; margin-bottom: 20px;
background-color: #373737;
align-items: center; align-items: center;
text-align: center;
justify-content: space-between; justify-content: space-between;
} }
.msgs { .msgs {
display: flex; font-size: 15px;
align-items: center; }
.uid {
font-size: 14px;
margin-right: 10px;
} }
.logout { .logout {
padding: 7px; padding: 6px;
margin-right: 7px;
cursor: pointer; cursor: pointer;
font-size: 15.5px;
} }
.logout:hover { .logout:hover {
@ -275,11 +338,13 @@ select:hover {
} }
.msgs :first-child { .msgs :first-child {
font-size: 35px; font-size: 27px;
margin-left: 4px;
} }
.msgs > div { .msgs > div {
margin: 0px 5px; padding: 2px 6px;
font-size: 13.5px;
} }
.actions { .actions {
@ -299,3 +364,114 @@ select:hover {
.actions > span:hover { .actions > span:hover {
background-color: var(--hoover-color); background-color: var(--hoover-color);
} }
.buttonContainer {
display: flex;
align-content: center;
justify-content: center;
margin: 10px 0px;
height: 50px;
}
.buttonContainer > * {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
background-color: var(--hoover-color);
border: none;
color: var(--text-color);
padding: 5px 15px;
font-weight: bold;
font-size: 15px;
margin: 8px 5px 2px 5px;
cursor: pointer;
text-shadow: 1px 1px 8px black;
transition: width 0.5s, height 0.5s, ease-in 0.5s;
user-select: none;
cursor: pointer;
text-align: center;
text-decoration: none;
}
.buttonContainer > *:hover {
text-shadow: 2px 2px 8px black;
transition: width 0.5s, height 0.5s, ease-out 0.5s;
background-color: var(--text-color);
color: black;
}
.buttonContainer > .activeButton {
background-color: var(--text-color);
color: black;
}
select {
cursor: pointer;
width: 100%;
max-width: 100%;
height: 30px;
}
select:hover {
border: 1px solid #f2cb05;
}
.checkbContainer {
display: flex;
justify-content: center;
align-items: center;
margin: 10px 0px 0px 0px;
}
.checkbContainer > input {
width: 5%;
background-color: #9c9c98;
color: azure;
font-family: inherit;
margin-bottom: 5px;
}
.selectContainer {
display: flex;
justify-content: center;
align-items: center;
}
.selectContainer > select,
.selectContainer > input {
width: 20%;
border-radius: 5px;
border: 1.5px solid white;
background-color: #9c9c98;
color: azure;
font-family: inherit;
margin: 6px 0px;
}
.selectContainer > select:hover,
.selectContainer > select:active {
border: 2px solid var(--text-color);
}
.selectContainer > div {
padding: 0px 5px;
margin-right: 8px;
}
.description {
padding-top: 1px;
padding-bottom: 5px;
font-size: inherit;
color: inherit;
text-align: center;
margin: 10px;
}
.resultContainer {
color: gainsboro;
font-size: 18px;
display: flex;
justify-content: center;
margin-top: 8px;
}

View file

@ -1,8 +1,15 @@
export default function Custom404 () { export default function Custom404() {
return ( return (
<center> <center>
<h1>404</h1> <h1>404</h1>
<iframe width='660' height='465' src='https://www.youtube-nocookie.com/embed/GOzwOeONBhQ' frameBorder='0' allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture' allowFullScreen /> <iframe
width="660"
height="465"
src="https://www.youtube-nocookie.com/embed/GOzwOeONBhQ"
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</center> </center>
) )
} }

View file

@ -40,7 +40,7 @@ function MyApp({ Component, pageProps, router }) {
return ( return (
<Layout <Layout
route={router.route} router={router}
globalData={globalData} globalData={globalData}
refetchGlobalData={getGlobalProps} refetchGlobalData={getGlobalProps}
> >

View file

@ -1,16 +1,16 @@
import Document, { Html, Head, Main, NextScript } from 'next/document' import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document { class MyDocument extends Document {
static async getInitialProps (ctx) { static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx) const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps } return { ...initialProps }
} }
render () { render() {
return ( return (
<Html> <Html>
<Head /> <Head />
<body bgcolor='#222426'> <body bgcolor="#222426">
<Main /> <Main />
<NextScript /> <NextScript />
</body> </body>

View file

@ -1,9 +1,16 @@
function Error ({ statusCode }) { function Error({ statusCode }) {
const render404 = () => { const render404 = () => {
return ( return (
<center> <center>
<h1>404</h1> <h1>404</h1>
<iframe width='100%' height='465' src='https://www.youtube-nocookie.com/embed/GOzwOeONBhQ' frameBorder='0' allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture' allowFullScreen /> <iframe
width="100%"
height="465"
src="https://www.youtube-nocookie.com/embed/GOzwOeONBhQ"
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</center> </center>
) )
} }

View file

@ -6,6 +6,7 @@ import QuestionSearchResult from '../components/QuestionSearchResult.js'
import Subject from '../components/Subject.js' import Subject from '../components/Subject.js'
import SubjectSelector from '../components/SubjectSelector.js' import SubjectSelector from '../components/SubjectSelector.js'
import Sleep from '../components/sleep' import Sleep from '../components/sleep'
import ExternalLinkIcon from '../components/externalLinkIcon'
import styles from './allQuestions.module.css' import styles from './allQuestions.module.css'
@ -74,8 +75,10 @@ export default function AllQuestions({ router }) {
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
const [activeSubjName, setActiveSubjName] = useState('') const [activeSubjName, setActiveSubjName] = useState('')
const [dbs, setDbs] = useState(null) const [dbs, setDbs] = useState(null)
const [selectedDb, setSelectedDb] = useState('')
const [data, setData] = useState(null) const [data, setData] = useState(null)
const [fetchingData, setFetchingData] = useState(false) const [fetchingData, setFetchingData] = useState(false)
const subjectCount = data ? data.length : 0 const subjectCount = data ? data.length : 0
const questionCount = data ? data.reduce(countReducer, 0) : 0 const questionCount = data ? data.reduce(countReducer, 0) : 0
@ -87,6 +90,7 @@ export default function AllQuestions({ router }) {
const querySearch = router.query.question const querySearch = router.query.question
? decodeURIComponent(router.query.question) ? decodeURIComponent(router.query.question)
: '' : ''
console.log(querySearch) console.log(querySearch)
fetchDbs().then((res) => { fetchDbs().then((res) => {
@ -98,40 +102,65 @@ export default function AllQuestions({ router }) {
if (dbs) { if (dbs) {
return ( return (
<> <>
<select <div className={'pageHeader'}>
className={styles.select} <h1>Kérdések és tárgyak</h1>
defaultValue={-1} </div>
onChange={(event) => { <Sleep />
const key = event.target.value <div className={'description'}>
setData(null) Ezen az oldalon tudsz manuálisan keresni a kérdések és a tárgyak
setFetchingData(true) között, vagy ellenőrizni, hogy egy adott tárgy szerepel-e a kérdés-
if (key === 'all') { és tárgyadatbázisban. Ezen kívül a kérdéseket le is töltheted
fetchAllData(dbs).then((res) => { offline használatra. (txt formátumban)
setData(mergeData(res)) </div>
setFetchingData(false) <center>
}) <div className={`buttonContainer ${styles.dataEditor}`}>
} else { <div
fetchData(dbs[key]).then((res) => { onClick={() => {
setData(res.data) window.open(`${constants.siteUrl}dataeditor`, '_blank')
setFetchingData(false) }}
}) >
} Kérdés szerkesztő
}} <ExternalLinkIcon size={15} />
> </div>
<option disabled value={-1}> </div>
{' -- Válassz egy kérdés adatbázist -- '} </center>
</option> <div className={'selectContainer'}>
{dbs.map((db, i) => { <select
return ( defaultValue={-1}
<option value={i} key={db.path}> onChange={(event) => {
{db.name} const key = event.target.value
</option> setData(null)
) setFetchingData(true)
})} if (key === 'all') {
<option value={'all'} key={'all'}> setSelectedDb(key)
{'All'} fetchAllData(dbs).then((res) => {
</option> setData(mergeData(res))
</select> setFetchingData(false)
})
} else {
setSelectedDb(dbs[key].name)
fetchData(dbs[key]).then((res) => {
setData(res.data)
setFetchingData(false)
})
}
}}
>
<option disabled value={-1}>
{' Válassz egy adatbázist!'}
</option>
{dbs.map((db, i) => {
return (
<option value={i} key={db.path}>
{db.name}
</option>
)
})}
<option value={'all'} key={'all'}>
{'Összes kérdés'}
</option>
</select>
</div>
</> </>
) )
} else { } else {
@ -148,31 +177,28 @@ export default function AllQuestions({ router }) {
return ( return (
<div> <div>
<Head>
<title>Tárgyak - Qmining | Frylabs.net</title>
</Head>
{data ? ( {data ? (
<> <>
<div className={styles.searchContainer}> <center>
<input <div className={styles.searchContainer}>
autoFocus <input
placeholder="Keresés..." placeholder="Kezdj el írni a kereséshez..."
className={styles.searchBar} type="text"
type="text" value={searchTerm}
value={searchTerm} onChange={(event) => {
onChange={(event) => { setSearchTerm(event.target.value)
setSearchTerm(event.target.value) }}
}} />
/> <button
<button onClick={() => {
onClick={() => { setSearchTerm('')
setSearchTerm('') }}
}} className={styles.clearButton}
className={styles.clearButton} >
> X
X </button>
</button> </div>
</div> </center>
<hr /> <hr />
<SubjectSelector <SubjectSelector
data={data} data={data}
@ -182,10 +208,9 @@ export default function AllQuestions({ router }) {
setActiveSubjName(subjName) setActiveSubjName(subjName)
}} }}
/> />
<hr />
<div>{/*{qCount} kérdés, {sCount} tárgy */}</div>
<Sleep />
<div> <div>
<hr />
<Subject subj={currSubj} /> <Subject subj={currSubj} />
</div> </div>
</> </>
@ -197,16 +222,11 @@ export default function AllQuestions({ router }) {
const renderQuestionBrowser = () => { const renderQuestionBrowser = () => {
return ( return (
<div> <div>
<Head>
<title>Qmining - Kérdés keresés | Frylabs.net</title>
</Head>
{data ? ( {data ? (
<> <>
<div className={styles.searchContainer}> <div className={styles.searchContainer}>
<input <input
autoFocus placeholder="Kezdj el írni a kereséshez..."
placeholder="Keresés..."
className={styles.searchBar}
type="text" type="text"
value={searchTerm} value={searchTerm}
onChange={(event) => { onChange={(event) => {
@ -228,7 +248,6 @@ export default function AllQuestions({ router }) {
X X
</button> </button>
</div> </div>
<hr />
<div> <div>
<QuestionSearchResult data={data} searchTerm={searchTerm} /> <QuestionSearchResult data={data} searchTerm={searchTerm} />
</div> </div>
@ -240,23 +259,43 @@ export default function AllQuestions({ router }) {
return ( return (
<div> <div>
{dbs ? ( <Head>
<title>Kérdések és tárgyak - Qmining | Frylabs.net</title>
</Head>
{dbs ? <>{renderDbSelector()}</> : <LoadingIndicator />}
{dbs && data ? (
<> <>
{renderDbSelector()} <div className={styles.info}>
{data && `${questionCount} kérdés, ${subjectCount} tárgy`} {`Összesen ${questionCount} kérdés, ${subjectCount} tárgyból`}
<div className={styles.typeSelector}> </div>
<div className={'buttonContainer'}>
<div <div
className={!subjectsShowing ? styles.activeTypeSelector : ''} className={!subjectsShowing ? 'activeButton' : ''}
onClick={() => setSubjectsShowing(false)} onClick={() => setSubjectsShowing(false)}
> >
Kérdések Kérdések
</div> </div>
<div <div
className={subjectsShowing ? styles.activeTypeSelector : ''} className={subjectsShowing ? 'activeButton' : ''}
onClick={() => setSubjectsShowing(true)} onClick={() => setSubjectsShowing(true)}
> >
Tárgyak Tárgyak
</div> </div>
<a
onClick={() => {
if (selectedDb === 'all') {
window.open(`${constants.apiUrl}allqr.txt`, '_blank')
} else {
window.open(
`${constants.apiUrl}allqr.txt?db=${selectedDb}`,
'_blank'
)
}
}}
>
{'Kérdések letöltése'}
<ExternalLinkIcon size={15} />
</a>
</div> </div>
{fetchingData ? ( {fetchingData ? (
<LoadingIndicator /> <LoadingIndicator />
@ -268,9 +307,9 @@ export default function AllQuestions({ router }) {
</div> </div>
)} )}
</> </>
) : ( ) : fetchingData ? (
<LoadingIndicator /> <LoadingIndicator />
)} ) : null}
</div> </div>
) )
} }

View file

@ -1,47 +1,29 @@
.searchBar {
margin: 10px;
color: white;
background-color: #222426;
border: none;
font-size: 18px;
flex-grow: 1;
}
.searchContainer { .searchContainer {
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: center;
margin-left: 10px;
padding-top: 10px;
padding-bottom: 2px;
} }
.clearButton { .clearButton {
width: 80px; width: 50px;
background-color: var(--background-color); background-color: var(--background-color);
color: white; color: white;
font-size: 23px; font-size: 18px;
cursor: pointer; cursor: pointer;
border: none; border: none;
margin-left: 10px;
} }
.typeSelector { .info {
margin: 10px 0px; text-align: center;
height: 50px; font-style: italic;
display: flex; padding-top: 5px;
} }
.typeSelector div { .dataEditor {
display: flex; margin-bottom: 25px;
align-items: center; width: 25%;
justify-content: center;
flex: 1;
font-size: 18px;
color: #fff;
cursor: pointer;
}
.typeSelector div:hover {
background-color: #333;
}
.activeTypeSelector {
background-color: #444;
} }

87
src/pages/contact.js Normal file
View file

@ -0,0 +1,87 @@
import React, { useState, useEffect } from 'react'
import Head from 'next/head'
import FeedbackArea from '../components/feedbackArea'
import constants from '../constants.json'
import LoadingIndicator from '../components/LoadingIndicator'
import Sleep from '../components/sleep'
import styles from './contact.module.css'
export default function Contact() {
const [contacts, setContacts] = useState()
useEffect(() => {
fetch(constants.apiUrl + 'contacts.json', {
method: 'GET',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})
.then((res) => {
return res.json()
})
.then((res) => {
setContacts(res)
})
}, [])
return (
<div>
<Head>
<title>Kapcsolat - Qmining | Frylabs.net</title>
</Head>
<div className={'pageHeader'}>
<h1>Kapcsolat</h1>
</div>
<br />
<Sleep />
<br />
<div>
<div className={'subtitle'}>Üzenet küldése</div>
<div className={styles.text}>
Weboldalon keresztüli üzenetküldés az adminnak (feedback). A válasz a
bal alsó postaládába (📬 ikon) fog érkezni.
</div>
</div>
<FeedbackArea from={'contact'} allowFile />
<div className={styles.container}>
{contacts ? (
<>
<div>
<div className={'subtitle'}>Alternatív módok</div>
<div className={styles.text}>
Az alábbi módokat is nyugodtan használhatod, a nevedet, e-mail
címedet, illetve semmilyen egyéb adatot nem adok ki harmadik fél
számára. (egyedül én fogom látni)
</div>
</div>
<div className={styles.contactsContainer}>
{Object.keys(contacts).map((key) => {
const { description, value, href } = contacts[key]
return (
<div key={key}>
<div>{description}</div>
{href ? (
<a target="blank" rel="noreferrer" href={href}>
{' '}
{value}{' '}
</a>
) : (
<div>{value}</div>
)}
</div>
)
})}
</div>
</>
) : (
<LoadingIndicator />
)}
</div>
</div>
)
}

View file

@ -0,0 +1,48 @@
.container {
display: flex;
flex-flow: column;
align-items: center;
}
.contactsContainer {
margin: 5px;
max-width: 900px;
}
.contactsContainer > * {
text-decoration: none;
color: gainsboro;
padding-top: 10px;
display: flex;
}
.contactsContainer > * > * {
padding: 5px;
}
.contactsContainer > * > *:nth-child(1) {
flex: 1;
}
.contactsContainer > * > *:nth-child(2) {
flex: 0 150px;
}
.text {
display: flex;
flex-flow: column;
align-items: center;
text-align: center;
padding-top: 8px;
padding-right: 100px;
padding-left: 100px;
}
.text > div:nth-child(1) {
font-size: 20px;
color: var(--text-color);
}
.contactTable {
border-spacing: 15px;
}

View file

@ -3,116 +3,98 @@ import Head from 'next/head'
import Sleep from '../components/sleep' import Sleep from '../components/sleep'
import Todos from '../components/todoStuff/todos' import Todos from '../components/todoStuff/todos'
import FeedbackArea from '../components/feedbackArea'
import Modal from '../components/modal'
import constants from '../constants.json' import constants from '../constants.json'
import styles from './contribute.module.css' import styles from './contribute.module.css'
import repos from '../data/repos.json' import repos from '../data/repos.json'
export default function contribute() { export default function contribute() {
const [newTask, setNewTask] = useState('') const [showFeedback, setShowFeedback] = useState(false)
const submitNewTask = async () => {
if (!newTask) {
return
}
fetch(constants.apiUrl + 'postfeedback', {
method: 'POST',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
newTask: newTask,
from: 'contribute',
}),
})
.then((resp) => {
return resp.json()
})
.then((resp) => {
if (resp.success) {
alert('Elküldve')
setNewTask('')
} else {
alert('Hiba küldés közben')
}
})
.catch((err) => {
alert('Hiba küldés közben')
console.error(err)
})
}
const renderNewTaskArea = () => {
return (
<div className={styles.inputArea}>
<textarea
onChange={(event) => setNewTask(event.target.value)}
value={newTask || ''}
className={styles.feedback}
/>
<button className={styles.button} onClick={submitNewTask}>
Küldés
</button>
</div>
)
}
return ( return (
<div> <div className={'endofpage'}>
<Head> <Head>
<title>Qmining - Todos | Frylabs.net</title> <title>Todos - Qmining | Frylabs.net</title>
</Head> </Head>
<div className={styles.description}> <div className={'pageHeader'}>
Egy kártyára kattintva nézheted meg a részleteket, vagy szavazhatsz. <h1>Teendők</h1>
Minél több szavazat érkezik egy kártyára, annál magasabb lesz a
pioritása. Jobb alsó szám minél több, annál nehezebb a feladat. A Done
oszlopban lévő feladatok kész vannak, de különböző okok miat még nem
lettek kiadva frissítésként. Ami az In Prod táblázatban van az van kint.
<br />
{
'Ha olyan taskot látsz amiben tudnál és szeretnél segíteni, akkor írj '
}
<a
href="http://qmining.frylabs.net/irc?contribute"
target="_blank"
rel="noreferrer"
>
{'IRC'}
</a>
-n, és útbaigazítalak.
</div> </div>
<div className={styles.description}> <Sleep />
Itt írhatsz új todo-ra ötleteket, vagy jelezhetsz hogy egyikben <div className={'description'}>
segítenél <p>
Ezen az oldalon új ötleteket adhatsz hozzá megvalósításra a teendők
listájához.
</p>
<p>
Emellet ha hozzáértő vagy, adott feladatok megvalósításában is
segíthetsz. (lásd: <a href="#gitrepo">lentebb</a>)
<br /> Ha egy kártyára kattintasz, a megjelenő ablakban láthatod annak
részleteit, illetve{' '}
<b>
<i>szavazhatsz</i>
</b>{' '}
is a feladatra, annak érdekében, hogy minél hamarabb megvalósulhasson.
Minél több szavazat érkezik egy kártyára, annál magasabb lesz a
prioritása. (értsd: a legtöbb szavazatot kapó kártya teendője lesz
legelőször megvalósítva)
<br /> A kurzort az oszlopcímekre mozgatva, további információkat
olvashatsz a kategóriák tulajdonságairól.
</p>
</div> </div>
{renderNewTaskArea()} <center>
<div className={`buttonContainer ${styles.newTaskButton}`}>
<div
onClick={() => {
setShowFeedback(true)
}}
>
Új feladat
</div>
</div>
</center>
<br />
<hr /> <hr />
<Todos /> <Todos />
<Sleep />
<hr /> <hr />
<div className={styles.title}>Git repos</div> <div className={'subtitle'}>
<hr /> <b>Git repos</b>
<hr /> </div>
<div className={styles.repos}> <div className={styles.repos} style={{ float: 'left' }} id={'gitrepo'}>
{Object.keys(repos.repos).map((key) => { {Object.keys(repos.repos).map((key) => {
let repo = repos.repos[key] let repo = repos.repos[key]
return ( return (
<a key={key} href={repo.href}> <div key={key}>
{repo.description} <ul>
</a> <li>
<a key={key} href={repo.href}>
{repo.description}
</a>
</li>
</ul>
</div>
) )
})} })}
<hr />
</div> </div>
<div style={{ textAlign: 'center' }}> <div
style={{ textAlign: 'right', marginRight: '100px', marginTop: '25px' }}
>
<img <img
style={{ maxWidth: '100%', width: '400px' }} style={{ maxWidth: '100%', width: '320px' }}
src={`${constants.siteUrl}img/bug.png`} src={`${constants.siteUrl}img/bug.png`}
/> />
</div> </div>
<hr />
{showFeedback && (
<Modal
closeClick={() => {
setShowFeedback(false)
}}
>
<FeedbackArea from={'contribute'} />
</Modal>
)}
</div> </div>
) )
} }

View file

@ -1,10 +1,3 @@
.description {
font-size: 15px;
color: white;
text-align: center;
margin: 10px;
}
.warning { .warning {
color: white; color: white;
padding: 10px; padding: 10px;
@ -12,34 +5,28 @@
text-align: center; text-align: center;
} }
.feedback {
color: var(--text-color);
background-color: var(--background-color);
font-size: 14px;
width: 100%;
box-sizing: border-box;
height: 60px;
}
.button {
background-color: var(--text-color);
border: none;
padding: 5px 15px;
margin: 5px;
color: white;
width: 200px;
}
.inputArea {
display: flex;
}
.title { .title {
color: #9999ff; color: var(--text-color);
font-size: 30px; font-size: 30px;
text-align: center; text-align: center;
} }
.repos {
margin-top: 6%;
margin-left: 10%;
}
.repos a { .repos a {
margin: 0px 5px; margin: 0px 5px;
text-decoration: none;
}
.newTaskButton {
width: 25%;
}
.imgContainer {
float: right;
text-align: right;
padding-right: 50px;
} }

290
src/pages/faq.js Normal file
View file

@ -0,0 +1,290 @@
import React, { useState, useEffect } from 'react'
import Sleep from '../components/sleep'
import Head from 'next/head'
function PasswordSection() {
return (
<>
<div className={'manualBody'}>
<p>
Ha ezt olvasod valszeg már neked is van. Azért lett bevezetve, hogy
nagyjából zárt legyen a felhasználók köre.
</p>
<ul>
<li>Minden felhasználónak más jelszava van.</li>
<li>
Elvileg elég csak 1 szer beírnod, és többet nem kell, de{' '}
<b>mentsd le biztos helyre a jelszót, hogy később is meglegyen</b>!
Ha többször kell megadnod, akkor az bug lesz. Ilyenkor ezt{' '}
<a
href="http://qmining.frylabs.net/feedback?man"
target="_blank"
rel="noreferrer"
>
jelentsd
</a>
.
</li>
<li>
<i>
Jelenleg nincs elfelejtett jelszó funkció, ha elfelejted, akkor az
örökre eltűnik!
</i>
</li>
<li>
Ha van jelszavad akkor <b>bizonyos határok között</b> te is{' '}
<a
href="https://qmining.frylabs.net/pwRequest?man"
target="_blank"
rel="noreferrer"
>
tudsz generálni
</a>{' '}
másoknak (ncore style).
</li>
<li>
Saját jelszavadat ne oszd meg, mivel egyszerre egy helyen lehetsz
belépve, máshol automatikusan ki leszel jelentkeztetve. (meg minek,
ha tudsz adni másoknak az előző pont alapján)
</li>
<li>
Mivel senkinek sincs felhasználóneve, csak egy UserID (amit bal alul
találsz), így az egész teljesen anonim. Emiatt a jelszavakat nem
lehet megváltoztatni, hogy a szükséges komplexitás megmaradjon.
</li>
</ul>
</div>
<hr />
</>
)
}
function FAQSection() {
return (
<>
<div className={'manualBody'}>
<ul>
<li>
<b>
Olyan helyeken fut le a script, ahol nem kellene, vagy
ideiglenesen ki akarod kapcsolni;
</b>
<br />
<i>
Tampermonkey bővitmény ikon -{'>'} click -{'>'} a scriptet
kapcsold ki. Csak ne felejtsd el visszakapcsolni ;)
</i>
</li>
<br />
<li>
<b>
Túl nagy a kérdést és a választ megjelenítő ablak, nem tudok a
válaszra kattintani;
</b>
<br />
<i>
A felugró ablakot ha minden jól megy akkor a szélénél fogva tudod
mozgatni, vagy egeret rajtatartva a görgővel tudod állítani az
áttetszőségét, vagy be tudod zárni jobb felül X-el, vagy egér
középső gombbal.
</i>
</li>
<br />
<li>
<b>Gombok, %-ok, számok;</b>
<br />
<img
style={{ maxWidth: '90%' }}
src="img/6.png"
alt="img"
className={'manual_img'}
/>
</li>
</ul>
</div>
<hr />
</>
)
}
function RiskSection() {
return (
<>
<ul>
<li>
<b>Bármikor észrevehetik hogy használod a scriptet</b>
<br />
A weboldalt már kevésbé, de úgy nem menti el a kérdéseket a script,
mert nem fut. Később manuálisan is be lehet majd küldeni
kérdés-válaszokat.
<p />
A script shadow-root hoz teszi hozzá az összes megjelenített
elementet, így ezeket szinte lehetetlen detektálni. A moodle
semmiféleképpen nem látja, hogy milyen más oldalak vannak megnyitva a
böngésződben. Nem látja az XMLHttp requesteket se, amit a script
végez. Egy Matomo nevű script látja hogy milyen oldalarka navigálsz a
moodle-ről, de a script nem linkekkel irányít át, hanem javascript
eseménnyel, amit nem tud nyomon követni.
<p />
Aztán ki tudja ténylegesen hogy lehet
</li>
<li>
<b>Bármikor leállhat a szerver</b>
<br />
És akkor nem bírod megnézni a válaszokat. Erre van az{' '}
<a
href="http://qmining.frylabs.net/allqr.txt?man"
target="_blank"
rel="noreferrer"
>
{' '}
összes kérdés TXT
</a>
</li>
<li>
<b>Akármelyik válasz rossz lehet</b>
<br />
Pl.: ha a script rosszul menti le, vagy rossz kérdésre ad választ
</li>
</ul>
<hr />
</>
)
}
function WebsiteSaveSection() {
return (
<>
<p>
Ha hibát észlesz, kérlek jelents. Hogy a hibákat a saját gépemen
reprodukálni tudjam, és könnyen ki bírjam javítani, sokszor jól jön, ha
egy lementett weboldalt megkapok, amin a hiba történik. Így lehet
menteni egy oldalt:
</p>
<center>
<img
style={{ maxWidth: '90%' }}
src="img/websitesave.png"
alt="img"
className={'manual_img'}
/>
<br />
<a href="/feedback" rel="noreferrer">
Ide tudod feltölteni
</a>
</center>
<p>
Mivel nincs hozzáférésem semmilyen egyetemi oldalhoz, így csak így tudom
hatékonyan tesztelni a scriptet. Ezért hatalmas segítség ha feltöltöd
azt az oldalt amin hibával találkozol.
</p>
<hr />
</>
)
}
function ScriptReinstallSection() {
return (
<>
<p>
Jelenleg két helyről lehet telepíteni a scriptet: greasyforkról és a
weboldalról. A greasyforkos telepítési lehetőség meg fog szűnni, így ha
onnan telepítetted, akkor nem lesznek frissítések elérhetők (amik nagyon
fontosak (de tényleg)). Ezért a következő rövid manővert kellene
végrehajtani, hogy minden zökkenőmentesen menjen:
</p>
<ul>
<li>Böngésző bővítményeidnél kattints a tampermonkey-ra</li>
<li>Válaszd ki alulról második opciót, ami dashboard néven fut</li>
<li>
Ekkor új tabban felugranak telepített scriptjeid. Keresd meg a
Moodle/Elearning/KMOOC test help-et, és a sor végén kattints a kuka
gombra
</li>
<li>Ha megkérdezi mondd neki, hogy biztos törölni akarod</li>
<li>
Ezután simán kattints{' '}
<a
href="http://qmining.frylabs.net/install?man"
target="_blank"
rel="noreferrer"
>
ide
</a>{' '}
a script újratelepítéséhez a weboldalról.
</li>
<li>
Kész! Lehet megkérdezi újra, hogy elérheti-e a szervert, de azt csak
egyszer. Szokásos módon engedélyezd, hogy le bírja kérni a helyes
válaszokat
</li>
</ul>
Ezzel semmi adat nem vész el, régi jelszó ugyanolyan marad (csak ne
felejtsd azt el)
<hr />
</>
)
}
const pages = {
faq: { name: 'GYIK', component: FAQSection },
pw: { name: 'Jelszavak', component: PasswordSection },
risk: { name: 'Kockázatok', component: RiskSection },
websitedl: { name: 'Weboldal letöltése', component: WebsiteSaveSection },
reinstall: {
name: 'Script újratelepítése',
component: ScriptReinstallSection,
},
}
export default function FAQ({ router }) {
const [currPage, setCurrPage] = useState(pages.faq)
const renderCurrPage = (page) => {
if (page) {
return <page.component />
} else {
return null
}
}
useEffect(() => {
if (router.query.tab) {
setCurrPage(pages[router.query.tab])
}
}, [router.query.tab])
return (
<div>
<Head>
<title>GYIK - Qmining | Frylabs.net</title>
</Head>
<div className={'pageHeader'}>
<h1>Gyakran Ismételt Kérdések</h1>
</div>
<br />
<Sleep />
<br />
<div className={'buttonContainer'}>
{Object.keys(pages).map((key) => {
const page = pages[key]
return (
<div
className={`${page === currPage && 'activeButton'}`}
key={key}
onClick={() => {
setCurrPage(page)
}}
>
{page.name}
</div>
)
})}
</div>
{renderCurrPage(currPage)}
<p className={'endofpage'} />
</div>
)
}

View file

@ -1,33 +1,30 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import fetch from 'unfetch' import fetch from 'unfetch'
import Head from 'next/head' import Head from 'next/head'
import Link from 'next/link'
import LoadingIndicator from '../components/LoadingIndicator' import LoadingIndicator from '../components/LoadingIndicator'
import Sleep from '../components/sleep' import Sleep from '../components/sleep'
import NewsEntry from '../components/newsEntry' import NewsEntry from '../components/newsEntry'
import Composer from '../components/composer' import Composer from '../components/composer'
import DbSelector from '../components/dbSelector.js'
import styles from './index.module.css' import styles from './index.module.css'
import constants from '../constants.json' import constants from '../constants.json'
const links = { const forumPostPerPage = 5
install: { const frontpageForumName = 'frontpage'
href: '/install',
text: 'Install',
},
irc: {
href: '/irc',
text: 'IRC chat',
},
}
function fetchNews() { function fetchForum(from) {
return new Promise((resolve) => { return new Promise((resolve) => {
fetch(`${constants.apiUrl}news.json`, { fetch(
credentials: 'include', `${
}) constants.apiUrl
}forumEntries?forumName=${frontpageForumName}&getContent=true${
from ? `&from=${from}` : ''
}&count=${forumPostPerPage}`,
{
credentials: 'include',
}
)
.then((resp) => { .then((resp) => {
return resp.json() return resp.json()
}) })
@ -47,6 +44,7 @@ function addPost(title, content) {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
forumName: frontpageForumName,
title: title, title: title,
content: content, content: content,
}), }),
@ -60,234 +58,226 @@ function addPost(title, content) {
}) })
} }
function postFeedback(content, file) { function updateForumPost(forum, postKey, postData) {
const postText = (file) => { return Object.keys(forum).reduce((acc, key) => {
return new Promise((resolve) => { const entry = forum[key]
fetch(constants.apiUrl + 'postfeedback', { if (key === postKey) {
method: 'POST', acc = {
credentials: 'include', ...acc,
headers: { [key]: postData,
Accept: 'application/json', }
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: content,
file: file ? file.name : null,
}),
})
.then((res) => {
return res.json()
})
.then((res) => {
resolve(res)
})
})
}
return new Promise((resolve) => {
if (file) {
const formData = new FormData() // eslint-disable-line
formData.append('file', file)
fetch(constants.apiUrl + 'postfeedbackfile', {
method: 'POST',
credentials: 'include',
headers: {
Accept: 'application/json',
},
body: formData,
})
.then((res) => {
return res.json()
})
.then((fileres) => {
postText(file).then((textres) => {
resolve({ fileres, textres })
})
})
} else { } else {
postText().then((res) => { acc = {
resolve(res) ...acc,
}) [key]: entry,
}
} }
}) return acc
}, {})
} }
export default function Index({ globalData }) { export default function Index({ globalData }) {
const userId = globalData.userId const userId = globalData.userId
const motd = globalData.motd const motd = globalData.motd
const [news, setNews] = useState(null) const [news, setNews] = useState(null)
const [allQrSelector, setAllQrSelector] = useState(null) const [nextEntryKey, setNextEntryKey] = useState()
// const userSpecificMotd = props.globalData.userSpecificMotd
useEffect(() => { useEffect(() => {
console.info('Fetching news.json') fetchForum().then((res) => {
fetchNews().then((res) => { const { entries, nextKey } = res
setNews(res) setNextEntryKey(nextKey)
setNews(entries)
}) })
}, []) }, [])
const renderNews = () => { const renderNews = () => {
if (news) { if (news) {
let newsItems = Object.keys(news) let newsItems = Object.keys(news).map((postKey) => {
.map((key) => { let newsEntryData = news[postKey]
let newsEntryData = news[key] return (
return ( <NewsEntry
<NewsEntry onPostDelete={() => {
onPostDelete={() => { fetch(constants.apiUrl + 'rmPost', {
fetch(constants.apiUrl + 'rmPost', { method: 'POST',
method: 'POST', credentials: 'include',
credentials: 'include', headers: {
headers: { Accept: 'application/json',
Accept: 'application/json', 'Content-Type': 'application/json',
'Content-Type': 'application/json', },
}, body: JSON.stringify({
body: JSON.stringify({ forumName: frontpageForumName,
newsKey: key, postKey: postKey,
}), }),
})
.then((res) => {
return res.json()
}) })
.then((res) => { .then((res) => {
return res.json() const { success, msg } = res
}) if (success) {
.then((res) => { setNews(
if (res.status === 'fail') { Object.keys(news).reduce((acc, key) => {
alert(res.msg) const entry = news[key]
} else { if (key !== postKey) {
setNews(res.news) acc = {
} ...acc,
}) [key]: entry,
}} }
onNewsReact={({ reaction, isDelete }) => { }
fetch(constants.apiUrl + 'react', { return acc
method: 'POST', }, {})
credentials: 'include', )
headers: { } else {
Accept: 'application/json', alert(msg)
'Content-Type': 'application/json', }
},
body: JSON.stringify({
reaction: reaction,
newsKey: key,
isDelete: isDelete,
}),
}) })
.then((res) => { }}
return res.json() onNewsReact={({ reaction, isDelete }) => {
}) fetch(constants.apiUrl + 'react', {
.then((res) => { method: 'POST',
console.log(res) credentials: 'include',
setNews(res.news) headers: {
}) Accept: 'application/json',
}} 'Content-Type': 'application/json',
onCommentReact={({ path, reaction, isDelete }) => { },
fetch(constants.apiUrl + 'react', { body: JSON.stringify({
method: 'POST', reaction: reaction,
credentials: 'include', postKey: postKey,
headers: { isDelete: isDelete,
Accept: 'application/json', forumName: frontpageForumName,
'Content-Type': 'application/json', }),
}, })
body: JSON.stringify({ .then((res) => {
type: 'reaction', return res.json()
newsKey: key,
path: path,
reaction: reaction,
isDelete: isDelete,
}),
}) })
.then((res) => { .then((res) => {
return res.json() setNews(res.news)
})
.then((res) => {
setNews(res.news)
})
}}
onDelete={(path) => {
fetch(constants.apiUrl + 'comment', {
method: 'POST',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'delete',
path: path,
newsKey: key,
}),
}) })
.then((res) => { }}
return res.json() onCommentReact={({ path, reaction, isDelete }) => {
}) fetch(constants.apiUrl + 'react', {
.then((res) => { method: 'POST',
if (res.status === 'fail') { credentials: 'include',
alert(res.msg) headers: {
} else { Accept: 'application/json',
setNews(res.news) 'Content-Type': 'application/json',
} },
}) body: JSON.stringify({
}} type: 'reaction',
onComment={(path, content) => { postKey: postKey,
fetch(constants.apiUrl + 'comment', { path: path,
method: 'POST', reaction: reaction,
credentials: 'include', isDelete: isDelete,
headers: { forumName: frontpageForumName,
Accept: 'application/json', }),
'Content-Type': 'application/json', })
}, .then((res) => {
body: JSON.stringify({ return res.json()
type: 'add',
path: path,
content: content,
newsKey: key,
}),
}) })
.then((res) => { .then((res) => {
return res.json() const { success, postData, msg } = res
}) if (success) {
.then((res) => { setNews(updateForumPost(news, postKey, postData))
setNews(res.news) } else {
}) alert(msg)
}} }
uid={userId} })
key={key} }}
newsKey={key} onDelete={(path) => {
newsItem={newsEntryData} fetch(constants.apiUrl + 'comment', {
/> method: 'POST',
) credentials: 'include',
}) headers: {
.reverse() Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'delete',
path: path,
postKey: postKey,
forumName: frontpageForumName,
}),
})
.then((res) => {
return res.json()
})
.then((res) => {
const { success, postData, msg } = res
if (success) {
setNews(updateForumPost(news, postKey, postData))
} else {
alert(msg)
}
})
}}
onComment={(path, content) => {
fetch(constants.apiUrl + 'comment', {
method: 'POST',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'add',
path: path,
content: content,
postKey: postKey,
forumName: frontpageForumName,
}),
})
.then((res) => {
return res.json()
})
.then((res) => {
const { success, postData, msg } = res
if (success) {
setNews(updateForumPost(news, postKey, postData))
} else {
alert(msg)
}
})
}}
uid={userId}
key={postKey}
newsKey={postKey}
newsItem={newsEntryData}
/>
)
})
return ( return (
<div> <div>
<div className={styles.title}>Forum</div> <div className={styles.title}>Fórum/Hírek</div>
<hr /> <hr />
<Composer <Composer
onSubmit={(type, title, content, file) => { onSubmit={(title, content) => {
if (!content) { addPost(title, content).then((res) => {
alert('Üres a tartalom!') const { success, newPostKey, newEntry, msg } = res
return if (success) {
} setNews({ [newPostKey]: newEntry, ...news })
console.log(type, title, content, file) } else {
if (type === 'private') { alert(msg)
postFeedback(content, file).then(
(/* { fileres, textres } */) => {
alert('Privát visszajelzés elküldve!')
}
)
} else {
if (!title) {
alert('Üres a téma!')
return
} }
addPost(title, content).then((res) => { })
setNews(res.news)
})
}
}} }}
/> />
<hr />
<div>{newsItems}</div> <div>{newsItems}</div>
{nextEntryKey && (
<div
className={styles.loadMoreButton}
onClick={() => {
fetchForum(nextEntryKey).then((res) => {
console.log(res)
const { entries, nextKey } = res
setNextEntryKey(nextKey)
setNews({ ...news, ...entries })
})
}}
>
Több bejegyzés betöltése
</div>
)}
</div> </div>
) )
} else { } else {
@ -297,9 +287,8 @@ export default function Index({ globalData }) {
const renderMotd = () => { const renderMotd = () => {
return ( return (
<div> <div className={styles.motd_body}>
<div className={styles.title}>MOTD</div> <div className={styles.title}>MOTD</div>
<hr />
{motd ? ( {motd ? (
<div <div
className={styles.motd} className={styles.motd}
@ -312,94 +301,15 @@ export default function Index({ globalData }) {
) )
} }
// const renderUserSpecificMotd = () => {
// return (
// <div>
// <hr />
// <div className={styles.subtitle}>Üzenet admintól:</div>
// {userSpecificMotd ? (
// <div
// className={styles.motd}
// dangerouslySetInnerHTML={{ __html: userSpecificMotd.msg }}
// />
// ) : (
// <div>...</div>
// )}
// </div>
// )
// }
const renderDbSelector = () => {
if (allQrSelector) {
return (
<DbSelector
text={`Válaszd ki melyik adatbázist szeretnéd letölteni (${allQrSelector}):`}
showAll={allQrSelector === 'txt'}
closeClick={() => {
setAllQrSelector(null)
}}
onDbSelect={(selectedDb) => {
if (allQrSelector === 'txt') {
if (selectedDb === 'all') {
window.open(`${constants.apiUrl}allqr.txt`, '_blank')
} else {
window.open(
`${constants.apiUrl}allqr.txt?db=${selectedDb.name}`,
'_blank'
)
}
} else if (allQrSelector === 'json') {
window.open(`${constants.apiUrl}${selectedDb.path}`, '_blank')
}
}}
/>
)
} else {
return null
}
}
return ( return (
<div> <div>
<Head> <Head>
<title>Qmining | Frylabs.net</title> <title>Qmining | Frylabs.net</title>
</Head> </Head>
<div className={styles.buttonContainer}>
{Object.keys(links).map((key) => {
let link = links[key]
return (
<Link key={key} href={link.href}>
<a className={styles.button} target="_blank">
{link.text}
</a>
</Link>
)
})}
<a
onClick={() => {
setAllQrSelector('txt')
}}
className={styles.button}
>
{'Összes kérdés TXT'}
</a>
<a
onClick={() => {
setAllQrSelector('json')
}}
className={styles.button}
>
{'Összes kérdés JSON'}
</a>
</div>
<hr />
{renderMotd()} {renderMotd()}
{/*{userSpecificMotd && renderUserSpecificMotd()} */} {/*{userSpecificMotd && renderUserSpecificMotd()} */}
<hr />
<hr />
<Sleep /> <Sleep />
{renderNews()} {renderNews()}
{renderDbSelector()}
</div> </div>
) )
} }

View file

@ -1,27 +1,5 @@
.buttonContainer { .hr {
display: flex; width: 100%;
}
.button {
color: white;
background-color: #303030;
margin: 2px;
padding: 10px 5px;
text-align: center;
font-size: 16px;
cursor: pointer;
word-wrap: break-word;
text-decoration: none;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
user-select: none;
}
.button:hover {
background-color: #555;
} }
.motd { .motd {
@ -30,20 +8,54 @@
margin: 5px; margin: 5px;
} }
.itemContainer {
width: 100%;
margin: 20px 5px;
background-color: var(--hoover-color);
}
.newsBody {
margin: 0px 5px;
padding: 10px 14px;
font-size: 17px;
color: #fff;
text-align: justify;
}
.motd_body {
border: 2px dashed var(--text-color);
padding-top: 13px;
padding-bottom: 15px;
padding-left: 5px;
padding-right: 5px;
margin-top: 18px;
margin-bottom: 30px;
margin-left: 5px;
margin-right: 5px;
font-size: 8px;
}
.title { .title {
color: #9999ff; color: var(--text-color);
font-size: 30px; font-size: 32px;
text-align: center; text-align: center;
letter-spacing: 2.5px;
} }
.subtitle { .subtitle {
color: #9999ff; color: var(--text-color);
font-size: 20px; font-size: 20px;
text-align: center; text-align: center;
} }
.newsTitle {
color: var(--text-color);
font-size: 28px;
padding-left: 17px;
}
.question { .question {
font-weight: 'bold'; font-weight: bold;
font-size: 16px; font-size: 16px;
color: #fff; color: #fff;
margin: 0px 5px; margin: 0px 5px;
@ -53,7 +65,32 @@
margin: 0px 5px; margin: 0px 5px;
} }
.itemNumber {
color: #a7a7a7;
margin: 0px 5px;
font-size: 22px;
padding-top: 12px;
padding-left: 13px;
padding-bottom: 3px;
}
.repos { .repos {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.loadMoreButton {
text-align: center;
background-color: var(--dark-color);
margin-left: 8px;
margin-right: 8px;
margin-bottom: 16px;
margin-top: 16px;
padding: 10px;
cursor: pointer;
}
.loadMoreButton:hover {
background-color: var(--hoover-color);
}

View file

@ -1,320 +0,0 @@
import React from 'react'
import Sleep from '../components/sleep'
import Head from 'next/head'
export default function Manual() {
return <div>{renderMaual()}</div>
}
function renderMaual() {
return (
<div>
<Head>
<title>Manual - Qmining | Frylabs.net</title>
</Head>
<center>
<h1>Manual</h1>
</center>
<hr />
<hr />
<Sleep />
<center>
Ez a userscript Moodle/Elearnig/KMOOC tesztek megoldása során segítséget
jelenít meg.
<h2>
Ha az oldalt vagy a scriptet használod: akármikor észrevehetik,
leállhat a szerver, és rossz lehet az összes válasz
</h2>
Valószínűleg semmi baj nem lesz, de én szóltam. Ha ez iránt aggódsz,
olvasd el a kockázatok részt
</center>
<hr />
<hr />
<center>
<h1>Userscript használata</h1>
</center>
<hr />
<hr />
<div className={'manualUsage'}>
<div>
<ul>
<li>
Tölts le egy userscript futtató kiegészítőt a böngésződhöz:{' '}
<a
href="https://www.tampermonkey.net/"
target="_blank"
rel="noreferrer"
>
Tampermonkey
</a>
</li>
<li>
<a
href="http://qmining.frylabs.net/install?man"
target="_blank"
rel="noreferrer"
>
Weboldalról
</a>{' '}
rakd fel a scriptet
</li>
<li>
Script majd udvariasan megkéri, hogy hagy beszélgessen a
szerverrel, mert mással nem tud, ezt engedélyezd.
</li>
<li>
Ezután:
<ul>
<li>
Teszt oldalon a kérdésre a választ kell látnod felül egy
felugró ablakszerűben
</li>
<li>
Teszt ellenőrzés oldalon a script beküldi a szervernek a
helyes válaszokat, az lementi az új kérdéseket, amik ezután
azonnal elérhetők lesznek
</li>
</ul>
</li>
</ul>
Egyéb fontos tudnivalók:
<ul>
<li>
Ezt ments sokszor akár minden nap:{' '}
<a
href="http://qmining.frylabs.net/allqr.txt?man"
target="_blank"
rel="noreferrer"
>
{' '}
Összes kérdés TXT
</a>{' '}
(ha elszállna a szerver)
</li>
<li>
<a
href="https://qmining.frylabs.net/allQuestions.html"
target="_blank"
rel="noreferrer"
>
Összes kérdés oldal
</a>
, ahol manuál tudsz keresni ha valami gáz lenne a scriptel
</li>
</ul>
<p /> Egyéb funkciók:
<ul>
<li>
Ha esetleg videókat nézel, akkor spaceval lehet play/pausolni, és
jobbra/balra gombbal ugrani a videóban.
</li>
</ul>
</div>
<div className={'rtfmImage'}>
<img
style={{ maxWidth: '100%', minWidth: '200px' }}
src="img/rtfm.jpg"
alt="img"
/>
</div>
</div>
<hr />
<hr />
<center>
<h1>Jelszavak</h1>
</center>
<hr />
<hr />
Ha ezt olvasod valszeg már van neked egy. Azért lett bevezetve, hogy
nagyjából zárt legyen a felhasználók köre
<ul>
<li>Minden felhasználónak más jelszava van</li>
<li>
Elvileg elég csak 1 szer beírnod, és nem kell többet, de{' '}
<b>mentsd le biztos helyre a jelszót, hogy később is meglegyen!</b> Ha
többször kell akkor az bug,{' '}
<a
href="http://qmining.frylabs.net/feedback?man"
target="_blank"
rel="noreferrer"
>
és szólj
</a>
</li>
<li>
<b>
Jelenleg nincs elfelejtett jelszó funkció, ha elfelejted akkor az
örökre eltűnik!
</b>
</li>
<li>
Ha van jelszavad akkor bizonyos határok között{' '}
<a
href="https://qmining.frylabs.net/pwRequest?man"
target="_blank"
rel="noreferrer"
>
te is tudsz generálni másoknak
</a>
(ncore style).
</li>
<li>
Saját jelszavad ne oszd meg, belépésnél máshonnan azonnal ki leszel
jelentkeztetve, és minek ha tudsz adni amúgy is
</li>
<li>
Mivel felhasználóneved nincs, így teljesen anoním az egész. Ez miatt
jelszót nem lehet megváltoztatni, hogy a szükséges komplexitás
megmaradjon
</li>
</ul>
<hr />
<hr />
<center>
<h1>Gyakran előforduló kérdések</h1>
</center>
<hr />
<hr />
<ul>
<li>
<b>
Olyan helyeken fut le a script, ahol nem kellene, vagy ideiglenesen
ki akarod kapcsolni
</b>
<br /> Tampermonkey bővitmény ikon -{'>'} click -{'>'} scriptet
kapcsold ki. Csak ne felejtsd visszakapcsolni ;)
</li>
<p />
<li>
<b>
Túl nagy a kérdést és a választ megjelenítő ablak, nem tudok a
válaszra kattintani
</b>
<br /> Zommolj ki egy kicsit, vagy kapcsold ki addig a scriptet.
Továbbá középső egérgombra kattintva el bírod tüntetni az ablakot,
amíg újra nem töltöd az oldalt, vagy másikra ugrasz.
</li>
<p />
<li>
<b>Gombok, %-ok, számok</b>
<br />
<img style={{ maxWidth: '100%' }} src="img/6.png" alt="img" />
</li>
<p />
</ul>
<hr />
<hr />
<center>
<h1>Kockázatok</h1>
</center>
<hr />
<hr />
<ul>
<li>
<b>Bármikor észrevehetik hogy használod a scriptet</b>
<br />
A weboldalt már kevésbé, de úgy nem menti el a kérdéseket a script,
mert nem fut. Később manuálisan is be lehet majd küldeni
kérdés-válaszokat.
<p />
Ha arra nem veszik a fáradságot, hogy a kérdéseket lecseréljék akkor
valószínűleg arra se hogy userscript futását detektáló kódot rakjanak
a weboldalra. A{' '}
<a href="https://moodle.org/" target="_blank" rel="noreferrer">
Moodle
</a>{' '}
egy nyílt forráskódú, valószínűleg self-hosted rendszer. Valószínűleg
az egyetem egy ezer éves debian szerverén fut, amihez senki se mer
nyúlni, nemhogy a moodle-t frissítse valaki.
<p />
A script shadow-root hoz teszi hozzá az összes megjelenített
elementet, így ezeket szinte lehetetlen detektálni. A moodle
semmiféleképpen nem látja, hogy milyen más oldalak vannak megnyitva a
böngésződben. Nem látja az XMLHttp requesteket se, amit a script
végez. Egy Matomo nevű script látja hogy milyen oldalarka navigálsz a
moodle-ről, de a script nem linkekkel irányít át, hanem javascript
eseménnyel, amit nem tud nyomon követni.
<p />
Aztán ki tudja ténylegesen hogy lehet
</li>
<li>
<b>Bármikor leállhat a szerver</b>
<br />
És akkor nem bírod megnézni a válaszokat. Erre van az{' '}
<a
href="http://qmining.frylabs.net/allqr.txt?man"
target="_blank"
rel="noreferrer"
>
{' '}
összes kérdés TXT
</a>
</li>
<li>
<b>Akármelyik válasz rossz lehet</b>
<br />
Pl.: ha a script rosszul menti le, vagy rossz kérdésre ad választ
</li>
</ul>
<hr />
<hr />
<center>
<h1>Egyéb</h1>
</center>
<hr />
<hr />
<h2 id="sitesave">Weboldal lementése</h2>
Hogy a hibákat a saját gépemen reprodukálni tudjam, és könnyen ki bírjam
javítani sokszor jól jön ha egy lementett weboldalt megkapok. Így lehet
letölteni egy oldalt:
<img style={{ maxWidth: '100%' }} src="img/websitesave.png" alt="img" />
<br />
<a
href="http://qmining.frylabs.net/feedback?man"
target="_blank"
rel="noreferrer"
>
Ide tudod feltölteni
</a>
<br />
Mivel nincs hozzáférésem semmilyen egyetemi oldalhoz, így csak így tudom
hatékonyan tesztelni a scriptet. Ezért hatalmas segítség ha feltöltöd azt
az oldalt amin hibával találkozol.
<hr />
<h2 id="scriptreinstall">Script újratelepítése</h2>
Jelenleg két helyről lehet telepíteni a scriptet: greasyforkról és a
weboldalról. A greasyforkos telepítési lehetőség meg fog szűnni, így ha
onnan telepítetted, akkor nem lesznek frissítések elérhetők (amik nagyon
fontosak (de tényleg)). Ezért a következő rövid manővert kellene
végrehajtani, hogy minden zökkenőmentesen menjen:
<ul>
<li>Böngésző bővítményeidnél kattints a tampermonkey-ra</li>
<li>Válaszd ki alulról második opciót, ami dashboard néven fut</li>
<li>
Ekkor új tabban felugranak telepített scriptjeid. Keresd meg a
Moodle/Elearning/KMOOC test help-et, és a sor végén kattints a kuka
gombra
</li>
<li>Ha megkérdezi mondd neki, hogy biztos törölni akarod</li>
<li>
Ezután simán kattints{' '}
<a
href="http://qmining.frylabs.net/install?man"
target="_blank"
rel="noreferrer"
>
ide a script újratelepítéséhez a weboldalról.
</a>
</li>
<li>
Kész! Lehet megkérdezi újra, hogy elérheti-e a szervert, de azt csak
egyszer. Szokásos módon engedélyezd, hogy le bírja kérni a helyes
válaszokat
</li>
</ul>
Ezzel semmi adat nem vész el, régi jelszó ugyanolyan marad (csak ne
felejtsd azt el)
</div>
)
}

View file

@ -1,45 +1,29 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import fetch from 'unfetch' import fetch from 'unfetch'
import Head from 'next/head' import Head from 'next/head'
import Link from 'next/link'
import Sleep from '../components/sleep'
import styles from './pwRequest.module.css' import styles from './pwRequest.module.css'
import constants from '../constants.json' import constants from '../constants.json'
export default function PwRequest(props) { function fetchAvailablePWS() {
const [result, setResult] = useState([]) return new Promise((resolve) => {
const [remaining, setRemaining] = useState('...')
const [requestedPWS, setRequestedPWS] = useState('...')
const [createDate, setCreateDate] = useState('...')
const [addPwPerDay, setAddPwPerDay] = useState('...')
const [daysAfterUserGetsPWs, setDaysAfterUserGetsPWs] = useState('...')
const [maxPWCount, setMaxPWCount] = useState('...')
const [addPWCount, setAddPWCount] = useState('...')
const [dayDiff, setDayDiff] = useState('...')
useEffect(() => {
console.info('Fetching avaible pws')
fetch(`${constants.apiUrl}avaiblePWS`, { fetch(`${constants.apiUrl}avaiblePWS`, {
credentials: 'include', credentials: 'include',
}) })
.then((resp) => { .then((resp) => {
return resp.json() return resp.json()
}) })
.then((data) => { .then((res) => {
setRemaining(data.avaiblePWS) resolve(res)
setCreateDate(data.userCreated)
setRequestedPWS(data.requestedPWS)
setAddPwPerDay(data.addPWPerDay)
setAddPWCount(data.addPWCount)
setDaysAfterUserGetsPWs(data.daysAfterUserGetsPWs)
setMaxPWCount(data.maxPWCount)
setDayDiff(data.dayDiff)
}) })
}, []) })
}
const handleSubmit = async () => { function requestPw() {
const rawResponse = await fetch(constants.apiUrl + 'getpw', { return new Promise((resolve) => {
fetch(constants.apiUrl + 'getpw', {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { headers: {
@ -48,85 +32,135 @@ export default function PwRequest(props) {
}, },
body: JSON.stringify({}), body: JSON.stringify({}),
}) })
try { .then((res) => {
rawResponse return res.json()
.json() })
.then((resp) => { .then((res) => {
if (resp.result === 'success') { resolve(res)
setResult([...result, resp.pw]) })
setRemaining(resp.remaining) })
setRequestedPWS(resp.requestedPWS) }
} else if (resp.result === 'success') {
setResult(['Nem vagy bejelentkezve!']) // this should never happpen export default function PwRequest({ globalData }) {
} else { const userId = globalData.userId || '...'
setResult([ const [result, setResult] = useState([])
...result, const [data, setData] = useState({
'Jelszó kérési lehetőségeid elfogytak, nézz vissza később', userCreated: '...',
]) availablePWS: '...',
setRemaining(0) requestedPWS: '...',
} maxPWCount: '...',
}) daysAfterUserGetsPWs: '...',
.catch((e) => { addPWPerDay: '...',
setResult([...result, 'Szerver oldali hiba!']) addPWCount: '...',
console.error(e) dayDiff: '...',
}) userCount: '...',
} catch (e) { })
setResult([...result, 'Szerver oldali hiba!'])
console.error(e) const {
} userCreated,
} availablePWS,
requestedPWS,
maxPWCount,
daysAfterUserGetsPWs,
addPWPerDay,
addPWCount,
dayDiff,
userCount,
} = data
useEffect(() => {
fetchAvailablePWS().then((data) => {
setData(data)
})
}, [])
return ( return (
// TODO: újrafogalmazás, remove hány nap után kapnak új jelszót először
<div> <div>
<Head> <Head>
<title>Jelszó kérés - Qmining | Frylabs.net</title> <title>Jelszó generálás - Qmining | Frylabs.net</title>
</Head> </Head>
<div id="form"> <div className={'pageHeader'}>
<div className={styles.text}> <h1>Jelszó generálás</h1>
Itt új jelszavakat tudsz kérni új felhasználóknak. Közös
jelszóhasználat nem ajánlott, mert ha valaki belép azzal a jelszóval
amit te használsz akkor téged kiléptet mindenhonnan. Szerintem van
elég jelszó hogy ne kelljen közös
</div>
<div className={styles.text}>
Új felhasználóknak
<span>{' ' + daysAfterUserGetsPWs + ' '}</span>
napot kell várni, míg kapnak lehetőséget jelszó generálásra.
</div>
<div className={styles.text}>
<span>{' ' + addPwPerDay + ' '}</span>
naponta
<span>{' ' + addPWCount + ' '}</span>
új lehetőség van jelszót generálni, maximum
<span>{' ' + maxPWCount + ' '}</span>
lehetőség gyűlhet össze
</div>
<div className={styles.text}>
Még kérhető jelszavak:
<span>{' ' + remaining}</span>. Felhasználó létrehozva:
<span>{' ' + createDate}</span>,<span>{' ' + dayDiff + ' '}</span>
napja. Eddig
<span>{' ' + requestedPWS + ' '}</span>
jelszót kértél.
</div>
<div className={styles.buttonContainer}>
<div onClick={handleSubmit} className={styles.button}>
Jelszó kérése
</div>
</div>
{result ? (
<div className={styles.pwContainer}>
{result.map((r, i) => {
return (
<div key={i} className={styles.pw}>
{i + 1}.: {r}
</div>
)
})}
</div>
) : null}
</div> </div>
<center>
<Sleep />
<div id="form">
<div className={styles.text}>
<p className={styles.descrip}>
Minden felhasználó egyedi jelszót kap. Ne használjatok többen egy
jelszót, mert egy idő után -biztonsági okokból- kidob a rendszer,
ha ugyan az a felhasználó több helyen is belépve marad. A
jelszavakról bővebben a{' '}
<Link href="/faq?tab=pw">
<a>GYIK</a>
</Link>{' '}
vonatkozó részében olvashatsz.
</p>
</div>
<div className={styles.text}>
Minden <span>{' ' + daysAfterUserGetsPWs + ' '}</span> napnál
régebbi felhasználó generálhat magának{' '}
<span>{' ' + maxPWCount + 'db '}</span> jelszót. Miután lekértél{' '}
<span>{' ' + maxPWCount + 'db'}</span>-ot, a lehetőségeid nem
fogytak el, ismét lesz módod további{' '}
<span>{' ' + maxPWCount + 'db'}</span>-ot kérni, összességében
korlátlan mennyiségben. Az egyszerre rendelkezésre álló lehetőségeid
számát lentebb látod, ez maximum egyszerre{' '}
<span>{' ' + maxPWCount + 'db '}</span> lehet. Ezt követően{' '}
<span>{' ' + addPWPerDay + ' '}</span>
naponta kapsz további <span>{' ' + addPWCount + ' '}</span> új
lehetőséget generálásra, amíg a lehetőségeid száma ismét el nem éri
a <span>{' ' + maxPWCount + 'db'}</span>-ot. Eddig
<span>{' ' + requestedPWS + ' '}</span>
jelszót kértél le, vagyis jelen pillanatban
<span>{' ' + (availablePWS - requestedPWS) + 'db '}</span>
jelszót generálhatsz még.
<br />
<br />A jelenleg bejelentkezett felhasználó{' '}
<span>{' ' + dayDiff + ' '}</span>
napos,{' '}
<span>
{userCreated ? new Date(userCreated).toLocaleString() : '...'}
</span>
-kor lett létrehozva.
<br />
Az oldalnak jelenleg
<span>{' ' + userCount + ' '}</span>
felhasználója van.
</div>
<div className={`buttonContainer ${styles.pwButton}`}>
<div
onClick={() => {
requestPw().then((res) => {
setData(res)
if (res.success) {
setResult([...result, res.pw])
} else {
setResult([
...result,
'Jelszó kérési lehetőségeid elfogytak, nézz vissza később',
])
}
})
}}
>
Jelszó kérése
</div>
</div>
{result ? (
<div className={styles.pwContainer}>
{result.map((res, i) => {
return (
<div key={i} className={styles.pw}>
{i + 1}.: {res}
</div>
)
})}
</div>
) : null}
</div>
</center>
</div> </div>
) )
} }

View file

@ -1,33 +1,23 @@
.buttonContainer {
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.button {
cursor: pointer;
text-align: center;
background-color: var(--text-color);
border: none;
padding: 10px 30px;
color: white;
width: 200px;
}
.text { .text {
text-align: center;
font-size: 18px; font-size: 18px;
color: white; color: white;
padding: 10px; padding: 10px;
} }
.descrip {
color: #acabab;
font-weight: bold;
padding-top: 4%;
padding-bottom: 4%;
font-size: 20px;
}
.pw { .pw {
margin: 10px; margin: 10px;
} }
.pwContainer { .pwContainer {
font-family: "Courier New", Courier, monospace; font-family: 'Courier New', Courier, monospace;
text-align: center; text-align: center;
font-size: 24px; font-size: 24px;
color: white; color: white;
@ -42,3 +32,7 @@
.text span { .text span {
color: var(--text-color); color: var(--text-color);
} }
.pwButton {
width: 20% !important;
}

View file

@ -85,20 +85,22 @@ function sortDataBy(data, key) {
}) })
} }
export default function RankList() { export default function RankList({ globalData }) {
const userId = globalData.userId
const [ranklist, setRanklist] = useState(null) const [ranklist, setRanklist] = useState(null)
const [selfUserId, setSelfUserId] = useState('...') const [selfUserId, setSelfUserId] = useState('...')
const [key, setKey] = useState('newQuestions') const [key, setKey] = useState('newQuestions')
const [since, setSince] = useState('all') const [since, setSince] = useState('all')
const [sum, setSum] = useState({}) const [sum, setSum] = useState()
const [ownEntryOnTop, setOwnEntryOnTop] = useState(false)
const getList = () => { const getList = () => {
setSum({}) setSum()
setRanklist(null) setRanklist(null)
getListFromServer(since) getListFromServer(since)
.then((data) => { .then((data) => {
setRanklist(data.list || []) setRanklist(data.list || [])
setSum(data.sum || {}) setSum(data.sum)
if (data.selfuserId) { if (data.selfuserId) {
setSelfUserId(data.selfuserId) setSelfUserId(data.selfuserId)
} }
@ -112,11 +114,22 @@ export default function RankList() {
getList() getList()
}, []) }, [])
useEffect(() => { useEffect(
getList() () => {
}, [since]) getList()
},
[since]
)
const list = ranklist && sortDataBy(ranklist, key) const list =
ranklist &&
sortDataBy(ranklist, key).reduce((acc, entry, i) => {
if (entry.userId === userId && ownEntryOnTop) {
return [{ rank: i, ...entry }, ...acc]
} else {
return [...acc, { rank: i, ...entry }]
}
}, [])
const updateSince = (keyword) => { const updateSince = (keyword) => {
setSince(keyword) setSince(keyword)
@ -127,82 +140,119 @@ export default function RankList() {
<Head> <Head>
<title>Ranklista - Qmining | Frylabs.net</title> <title>Ranklista - Qmining | Frylabs.net</title>
</Head> </Head>
<div> <div className={styles.container}>
<div <div>
className={styles.infoText} <center>
>{`A felhasználó ID-d: #${selfUserId}`}</div> <div className={'pageHeader'}>
<div className={styles.text}> <h1>Ranklista</h1>
Az adatok kb 2020. április eleje óta vannak gyűjtve, és azonnal </div>
frissülnek. </center>
</div> <div
<Sleep /> className={styles.infoText}
<div className={styles.sinceTable}> >{`A felhasználó ID-d: #${selfUserId}`}</div>
<div className={styles.sinceHeaderRow}> <div className={styles.text}>
{Object.keys(sinceOptions).map((key) => { <p>
const val = sinceOptions[key] Az adatok kb 2020. április eleje óta vannak gyűjtve, és azonnal
return ( frissülnek.
<div </p>
key={key} </div>
className={`${styles.clickable} ${key === since && <Sleep />
styles.selected}`} <div className={'selectContainer'}>
onClick={() => { <div>Megjelenítés: </div>
updateSince(key) <select
}} onChange={(e) => {
> updateSince(e.target.value)
{val.name} }}
</div> >
) {Object.keys(sinceOptions).map((key) => {
})} const val = sinceOptions[key]
return (
<option key={key} value={key}>
{val.name}
</option>
)
})}
</select>
</div>
<div className={'selectContainer'}>
<div>Rendezés: </div>
<select
value={key}
onChange={(e) => {
setKey(e.target.value)
}}
>
{Object.keys(selectOptions).map((key) => {
const val = selectOptions[key]
return (
<option key={key} value={key}>
{val.name}
</option>
)
})}
</select>
</div>
<div className={'checkbContainer'}>
<div>Saját hely mutatása felül:</div>
<input
type="checkbox"
checked={ownEntryOnTop}
onChange={(e) => {
setOwnEntryOnTop(e.target.checked)
}}
/>
</div> </div>
</div> </div>
<div className={styles.table}> {sum && list ? (
<div className={styles.headerRow}> <>
<div>{'Rank'}</div> <div className={`${styles.sumRow}`}>
<div>{'Felhasználó ID'}</div> <div />
{Object.keys(selectOptions).map((listKey) => { <div>
const val = selectOptions[listKey] <b>{'Összesen: '}</b>
return ( </div>
<div <div>{sum.newQuestions.toLocaleString('hu')}</div>
className={`${styles.clickable} ${listKey === key && <div>{sum.allQuestions.toLocaleString('hu')}</div>
styles.selected}`} <div>{sum.count.toLocaleString('hu')}</div>
key={listKey} </div>
onClick={() => { <div className={styles.headerRow}>
setKey(listKey) <div>
}} <b>{'Rank'}</b>
> </div>
{val.name} <div>{'Felhasználó ID'}</div>
</div> {Object.keys(selectOptions).map((listKey) => {
) const val = selectOptions[listKey]
})} return (
</div> <div
<div className={`${styles.row} ${styles.sumrow}`}> className={`${listKey === key && styles.selected}`}
<div /> key={listKey}
<div>{'Összesen'}</div> >
<div>{sum.newQuestions}</div> {val.name}
<div>{sum.allQuestions}</div> </div>
<div>{sum.count}</div> )
</div> })}
{list ? ( </div>
list.map((listItem, i) => { <div className={styles.table}>
return ( {list.map((listItem, i) => {
<div return (
className={`${styles.row} ${listItem.userId === selfUserId && <div
styles.selfRow}`} className={`${styles.row} ${listItem.userId ===
key={i} selfUserId && styles.selfRow}`}
> key={i}
<div>{i + 1}</div> >
<div>{'#' + listItem.userId}</div> <div>{listItem.rank + 1}</div>
{Object.keys(selectOptions).map((listKey) => { <div>{'#' + listItem.userId}</div>
const val = listItem[listKey] {Object.keys(selectOptions).map((listKey) => {
return <div key={listKey}>{val}</div> const val = listItem[listKey]
})} return <div key={listKey}>{val}</div>
</div> })}
) </div>
}) )
) : ( })}
<LoadingIndicator /> </div>
)} </>
</div> ) : (
<LoadingIndicator />
)}
</div> </div>
</div> </div>
) )

View file

@ -1,47 +1,72 @@
.table, .container {
.sinceTable {
display: flex; display: flex;
flex-direction: column; flex-flow: column;
padding: 0px 10px; height: calc(98vh);
cursor: default;
} }
.table :nth-child(1), .container:first-child {
.sinceTable :nth-child(1) { flex: 0 1 auto;
color: white;
} }
.row, .table {
.headerRow, flex: 1 1 auto;
.sinceHeaderRow { overflow-y: scroll;
overflow-x: hidden;
background-color: #1b1b1c;
padding-top: 5px;
padding-bottom: 10px;
}
.sumRow {
font-size: 16px;
font-weight: 500;
display: flex;
padding-top: 5px;
padding-right: 16px;
margin-top: 30px;
background-color: var(--hoover-color);
}
.headerRow {
font-size: 15px;
display: flex; display: flex;
padding: 1px; padding: 1px;
padding-right: 15px;
background-color: var(--hoover-color);
}
.row {
display: flex;
padding: 1.5px;
} }
.row:hover { .row:hover {
background-color: #333333; background-color: #242323;
cursor: default;
text-shadow: 1px 1px 6px #969696;
color: var(--text-color);
} }
.row div, .row div,
.headerRow div, .headerRow div, .sumRow div {
.sinceHeaderRow div {
flex: 1; flex: 1;
padding: 0px 5px; padding: 0px 5px;
text-align: center; text-align: center;
} }
.headerRow div, .headerRow > div, .sumRow > div {
.sinceHeaderRow div { padding: 5px;
padding: 15px 5px;
} }
.row :nth-child(1), .row :nth-child(1),
.headerRow :nth-child(1) { .headerRow :nth-child(1), .sumRow :nth-child(1) {
flex: 0 30px; flex: 0 50px;
} }
.row :nth-child(2), .row :nth-child(2),
.headerRow :nth-child(2) { .headerRow :nth-child(2), .sumRow :nth-child(2) {
flex: 0 100px; flex: 0 130px;
text-align: left; text-align: left;
} }
@ -54,26 +79,27 @@
} }
.selfRow { .selfRow {
background-color: #9999ff; background-color: var(--text-color);
color: black; color: black;
} }
.selfRow:hover { .selfRow:hover {
background-color: #9999ee; background-color: #96810b;
color: gainsboro;
} }
.text { .text {
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;
font-style: italic;
color: var(--text-color);
padding-bottom: 10px;
} }
.infoText { .infoText {
text-align: center; text-align: center;
font-size: 20px; font-size: 20px;
} padding-top: 20px;
.selected {
background-color: #9999ff;
} }
.sumrow { .sumrow {

113
src/pages/script.js Normal file
View file

@ -0,0 +1,113 @@
import React from 'react'
import Sleep from '../components/sleep'
import Head from 'next/head'
import ExternalLinkIcon from '../components/externalLinkIcon'
export default function Script() {
return (
<div>
<Head>
<title>Script - Qmining | Frylabs.net</title>
</Head>
<div className={'pageHeader'}>
<h1>Script</h1>
</div>
<div className={'buttonContainer'}>
<a href="https://www.tampermonkey.net/">
Ajánlott userscript kezelő bővítmény
<ExternalLinkIcon size={15} />
</a>
<a href="/install">
Script telepítése
<ExternalLinkIcon size={15} />
</a>
</div>
<Sleep />
{renderManual()}
</div>
)
}
function renderManual() {
return (
<>
<center>
<h2 className={'subtitle'}>A userscript telepítése, és használata</h2>
</center>
<div>
<p>
Ez a userscript Moodle/Elearnig/KMOOC tesztek megoldása során
segítséget jelenít meg.
</p>
<ol>
<li>
Tölts le egy userscript futtató kiegészítőt a böngésződhöz: pl. a{' '}
<a
href="https://www.tampermonkey.net/"
target="_blank"
rel="noreferrer"
>
Tampermonkey
</a>
-t.
</li>
<li>
<a
href="http://qmining.frylabs.net/install"
target="_blank"
rel="noreferrer"
>
Kattints ide hogy felrakd a scriptet
</a>{' '}
</li>
<li>
A script ezt követően udvariasan megkér, hogy hadd beszélgessen a
szerverrel, ezt engedélyezd neki. (Always allow domain)
</li>
<li>
A támogatott oldalakon a script egy apró menü ablakot jelenít meg a
weboldal bal alsó részén
</li>
<li>
Ezután a kitöltendő teszt oldalán a kérdésre a választ kell látnod
felül egy lebegő ablakban.
</li>
<li>
Teszt ellenőrzés oldalon a script beküldi a szervernek a helyes
válaszokat, az lementi az új kérdéseket, amik ezután azonnal
elérhetők lesznek (neked, és másoknak is)
</li>
</ol>
Egyéb fontos tudnivalók:
<ul>
<li>
Ezt ments sokszor akár minden nap:{' '}
<a
href="http://qmining.frylabs.net/allqr.txt"
target="_blank"
rel="noreferrer"
>
{' '}
Összes kérdés TXT
</a>{' '}
(az összes összegyűjtött kérdés, ha elszállna a szerver)
</li>
<li>
Az{' '}
<a href="/allQuestions" rel="noreferrer">
összes kérdés oldal
</a>{' '}
az oldal, ahol manuálisan tudsz keresni, ha valami gáz lenne a
scripttel.
</li>
</ul>
<div>
Ha útközben elakadsz, vagy hibát észlelsz, akkor oldalt a Kapcsolat
résznél sok elérhetőséget találsz, amin segítséget kérhetsz.
</div>
</div>
</>
)
}

View file

View file

@ -3,13 +3,13 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://fonts.googleapis.com/css?family=Quicksand&display=swap" rel="stylesheet"> <title>Thank you! - Qmining | Frylabs.net</title>
<title>Thank you!</title>
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Kameron&family=Overpass+Mono:wght@300;400&display=swap');
:root{ :root{
--size: 100px; --size: 100px;
--bgcolor: #222426; --bgcolor: #222426;
--color: #fcff4f; --color: #f2f2f2;
--shadow: rgba(30,2,5,.2); --shadow: rgba(30,2,5,.2);
} }
.surface { .surface {
@ -51,21 +51,28 @@
html,body{ html,body{
height:100vh; height: 95vh;
overflow: hidden; overflow: hidden;
} }
body { body {
font-family: 'Kameron', serif;
font-family: 'Overpass Mono', monospace;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: var(--bgcolor); background-color: var(--bgcolor);
cursor: default;
} }
#qminer {
color: #f2cb05;
}
#text{ #text{
font-family: 'Quicksand', sans-serif; font-weight: 100;
color: white; font-size: 25px;
font-size: 24px; color: #f2f2f2;
text-shadow: 1px 1px 2px rgba(0,0,0,0.6); text-shadow: 1px 1px 2px black;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -74,16 +81,19 @@
} }
#backbutton{ #backbutton{
text-decoration: none; text-decoration: none;
color: white; color: azure;
cursor: pointer; cursor: pointer;
margin: 10px; margin: 25px;
background-color: #4bb9bd; background-color: #f2cb05;
box-shadow: 0px 1px 3px rgba(0,0,0,0.6);
border-radius: 3px; border-radius: 3px;
padding: 3px 10px; padding: 3px 10px;
padding-top: 4px;
transition: width 0.5s, height 0.5s, ease-in 0.5s;
text-shadow: 1px 1px 2px black;
} }
#backbutton:hover{ #backbutton:hover{
box-shadow: 0px 1px 2px rgba(0,0,0,0.6); transition: width 0.5s, height 0.5s, ease-out 0.5s;
background-color: #96810b;
} }
.coincontainer { .coincontainer {
position: relative; position: relative;
@ -103,12 +113,11 @@
<div class="shadow"></div> <div class="shadow"></div>
</div></div> </div></div>
<center> <center>
<div id="text"><span>Thanks for the gold, kind question miner!</span><a id="backbutton" href="<%= siteurl %>">Return</a></div> <div id="text"><span>Thanks for the gold, kind <span id="qminer"><b>question miner</b></span>!</span><a id="backbutton" href="<%= siteurl %>">Return</a></div>
</center> </center>
<div class="coincontainer"><div class="surface"> <div class="coincontainer"><div class="surface">
<div class="coin"></div> <div class="coin"></div>
<div class="shadow"></div> <div class="shadow"></div>
</div></div> </div></div>
</body> </body>
</html> </html>

View file

@ -2,7 +2,7 @@ import styles from './thanks.module.css'
import constants from '../constants.json' import constants from '../constants.json'
import Head from 'next/head' import Head from 'next/head'
export default function Thanks () { export default function Thanks() {
return ( return (
<div> <div>
<Head> <Head>

View file

@ -1,83 +0,0 @@
import React, { useState } from 'react'
import Head from 'next/head'
import Sleep from '../components/sleep'
import Todos from '../components/todoStuff/todos'
import constants from '../constants.json'
import styles from './todos.module.css'
export default function contribute() {
const [newTask, setNewTask] = useState('')
const submitNewTask = async () => {
if (!newTask) {
return
}
fetch(constants.apiUrl + 'postfeedback', {
method: 'POST',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
newTask: newTask,
from: 'contribute',
}),
})
.then((resp) => {
return resp.json()
})
.then((resp) => {
if (resp.success) {
alert('Elküldve')
setNewTask('')
} else {
alert('Hiba küldés közben')
}
})
.catch((err) => {
alert('Hiba küldés közben')
console.error(err)
})
}
const renderNewTaskArea = () => {
return (
<div className={styles.inputArea}>
<textarea
onChange={(event) => setNewTask(event.target.value)}
value={newTask || ''}
className={styles.feedback}
/>
<button className={styles.button} onClick={submitNewTask}>
Küldés
</button>
</div>
)
}
return (
<div>
<Head>
<title>Todos - Qmining | Frylabs.net</title>
</Head>
<div className={styles.description}>
Egy kártyára kattintva nézheted meg a részleteket, vagy szavazhatsz.
Minél több szavazat érkezik egy kártyára, annál magasabb lesz a
pioritása. Jobb alsó szám minél több, annál nehezebb a feladat. A Done
oszlopban lévő feladatok kész vannak, de különböző okok miat még nem
lettek kiadva frissítésként. Ami az In Prod táblázatban van az van kint.
</div>
<Todos />
<div className={styles.description}>
Itt írhatsz új todo-ra ötleteket, vagy jelezhetsz hogy egyikben
segítenél
</div>
{renderNewTaskArea()}
<Sleep />
</div>
)
}

View file

@ -1,41 +0,0 @@
.description {
font-size: 15px;
color: white;
text-align: center;
margin: 10px;
}
.warning {
color: white;
padding: 10px;
font-size: 26px;
text-align: center;
}
.feedback {
color: var(--text-color);
background-color: var(--background-color);
font-size: 14px;
width: 100%;
box-sizing: border-box;
height: 60px;
}
.button {
background-color: var(--text-color);
border: none;
padding: 5px 15px;
margin: 5px;
color: white;
width: 200px;
}
.inputArea {
display: flex;
}
.title {
color: #9999ff;
font-size: 30px;
text-align: center;
}

330
src/pages/userFiles.js Normal file
View file

@ -0,0 +1,330 @@
import React, { useState, useEffect } from 'react'
import Head from 'next/head'
import LoadingIndicator from '../components/LoadingIndicator'
import Modal from '../components/modal'
import styles from './userFiles.module.css'
import constants from '../constants.json'
function listUserDir(subdir) {
return new Promise((resolve) => {
fetch(
`${constants.apiUrl}listUserDir${subdir ? `?subdir=${subdir}` : ''}`,
{
credentials: 'include',
}
)
.then((resp) => {
return resp.json()
})
.then((res) => {
if (res.success) {
resolve(res)
} else {
alert(res.msg)
}
})
})
}
function FileUploader({ onChange }) {
return (
<div>
<div>Fájl csatolása</div>
<input type="file" name="file" onChange={onChange} />
</div>
)
}
function deleteFile(currDir, name) {
return new Promise((resolve) => {
fetch(constants.apiUrl + 'deleteUserFile', {
method: 'POST',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
dir: currDir,
fname: name,
}),
})
.then((res) => {
return res.json()
})
.then((res) => {
if (res.success) {
resolve(res)
} else {
alert(res.msg)
}
})
})
}
function newSubj(name) {
return new Promise((resolve) => {
fetch(constants.apiUrl + 'newUserDir', {
method: 'POST',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
}),
})
.then((res) => {
return res.json()
})
.then((res) => {
if (res.success) {
resolve(res)
} else {
alert(res.msg)
}
})
})
}
function uploadFile(dir, file) {
return new Promise((resolve) => {
const formData = new FormData() // eslint-disable-line
formData.append('file', file)
formData.append('dir', dir)
fetch(constants.apiUrl + 'uploadUserFile', {
method: 'POST',
credentials: 'include',
headers: {
Accept: 'application/json',
},
body: formData,
})
.then((res) => {
return res.json()
})
.then((res) => {
resolve(res)
})
})
}
export default function UserFiles({ router, globalData }) {
const userId = globalData.userId
const [dirs, setDirs] = useState()
const [sortBy, setSortBy] = useState('name')
const [sortDirection, setSortDirection] = useState(true)
const [addingNew, setAddingNew] = useState()
const [newSubjName, setNewSubjName] = useState()
const [file, setFile] = useState()
const currDir = router.query.dir ? decodeURIComponent(router.query.dir) : ''
useEffect(() => {
const dir = router.query.dir ? decodeURIComponent(router.query.dir) : ''
setDirs(null)
if (router.query.dir) {
listUserDir(dir).then((res) => {
setDirs(res.files)
})
} else {
listUserDir().then((res) => {
setDirs(res.dirs)
})
}
}, [router.query.dir])
const dirSorter = (a, b) => {
if (a[sortBy] < b[sortBy]) {
return sortDirection ? -1 : 1
} else if (a[sortBy] > b[sortBy]) {
return sortDirection ? 1 : -1
} else {
return 0
}
}
const renderDirList = (dirs) => {
return (
<div>
{currDir && (
<div className={`buttonContainer ${styles.backButton}`}>
<div
onClick={() => {
router.back()
// FIXME: consistend going back with browser back button
// back button works like broser back button, unless it would result in site leave
// history: nothing > opened site/usrFiles?dir=...
// history: site > site/userFiles > site/usrFiles?dir=...
router.push(`${router.pathname}`, undefined, { shallow: true })
}}
>
Vissza
</div>
</div>
)}
<div className={`${styles.tableContainer} ${styles.header}`}>
<div
onClick={(e) => {
const name = e.target.getAttribute('name')
if (name) {
if (sortBy === name) {
setSortDirection(!sortDirection)
} else {
setSortDirection(true)
}
setSortBy(name)
}
}}
>
<div name="name">Fájl név</div>
<div name="date">Feltöltés dátuma</div>
<div name="size">Méret</div>
<div name="user">Feltöltő user</div>
</div>
</div>
<div className={`${styles.tableContainer} ${styles.rows}`}>
<div
onClick={() => {
setAddingNew(currDir ? 'file' : 'dir')
}}
>
<div>{currDir ? 'Új fájl feltöltése...' : 'Új tárgy...'}</div>
</div>
{dirs.length !== 0 && (
<>
{dirs.sort(dirSorter).map((dir) => {
const { name, date, path, size, user } = dir
return (
<div
title={name}
key={name}
onClick={() => {
if (path) {
window.open(`${constants.apiUrl}${path}`, '_blank')
} else {
router.push(
`${router.pathname}?dir=${encodeURIComponent(name)}`,
undefined,
{ shallow: true }
)
}
}}
>
<div>{name}</div>
<div>{new Date(date).toDateString()}</div>
<div>
{currDir
? (size / 1000000).toFixed(2).toString() + ' MB'
: size + ' fájl'}
</div>
<div>
{user &&
user !== -1 &&
(userId === user ? (
<div
className={styles.deleteButton}
onClick={(e) => {
e.stopPropagation()
if (confirm(`Biztos törlöd '${name}'-t ?`)) {
deleteFile(currDir, name).then(() => {
listUserDir(currDir).then((res) => {
setDirs(res.files)
})
})
}
}}
>
Törlés
</div>
) : (
`#${user}`
))}
</div>
</div>
)
})}
</>
)}
</div>
</div>
)
}
return (
<div>
<Head>
<title>Study Docs - Qmining | Frylabs.net</title>
</Head>
<div className="pageHeader">
<h1>Study Docs</h1>
</div>
<div className={styles.description}>
Ide tárgyanként lehet feltölteni
</div>
{dirs ? renderDirList(dirs) : <LoadingIndicator />}
{addingNew && (
<Modal
closeClick={() => {
setAddingNew(null)
}}
>
<div className={styles.uploadContainer}>
{addingNew === 'file' ? (
<>
<FileUploader
onChange={(e) => {
setFile(e.target.files[0])
}}
/>
<div
className="buttonContainer"
onClick={() => {
uploadFile(currDir, file).then(() => {
listUserDir(currDir).then((res) => {
setDirs(res.files)
setAddingNew(null)
})
})
}}
>
<div>Feltöltés</div>
</div>
</>
) : (
<>
<div>Új tárgy neve</div>
<div>
<input
type="text"
onChange={(e) => {
setNewSubjName(e.target.value)
}}
/>
</div>
<div
className="buttonContainer"
onClick={() => {
newSubj(newSubjName).then(() => {
listUserDir().then((res) => {
setDirs(res.dirs)
setAddingNew(null)
})
})
}}
>
<div>OK</div>
</div>
</>
)}
</div>
</Modal>
)}
</div>
)
}

View file

@ -0,0 +1,78 @@
.tableContainer {
user-select: none;
display: flex;
flex-flow: column;
}
.tableContainer > div {
padding: 15px 5px;
display: flex;
align-items: stretch;
}
.tableContainer > div > div {
padding: 5px;
}
.tableContainer > div > div:nth-child(1) {
flex: 1;
}
.tableContainer > div > div:nth-child(2) {
flex: 0 240px;
}
.tableContainer > div > div:nth-child(3) {
flex: 0 160px;
}
.tableContainer > div > div:nth-child(4) {
flex: 0 100px;
}
.rows > div {
cursor: pointer;
}
.rows > div:nth-child(2n + 1) {
background-color: var(--dark-color);
}
.rows > div:hover {
background-color: var(--hoover-color);
}
.header > div > div {
display: flex;
align-items: center;
cursor: pointer;
}
.header > div > div:hover {
background-color: var(--hoover-color);
}
.uploadContainer > div {
padding: 5px;
text-align: center;
}
.deleteButton {
text-align: center;
padding: 2px;
border: 1px solid var(--hoover-color);
border-radius: 3px;
cursor: pointer;
}
.deleteButton:hover {
background-color: var(--dark-color);
}
.backButton {
width: 30%;
}
.description {
padding: 5px;
text-align: center;
}