commit 53b4158967942aae312a28b226068c5575141077
Author: MrFry
Date: Wed Mar 25 13:25:41 2020 +0100
Initial commit, with fully working project :p
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..8bd21c5
--- /dev/null
+++ b/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "qminingDataEditor",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "dev": "next",
+ "build": "next build",
+ "start": "next start",
+ "export": "next build && next export"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "next": "^9.3.1",
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1",
+ "unfetch": "^4.1.0"
+ }
+}
diff --git a/src/components/LoadingIndicator.js b/src/components/LoadingIndicator.js
new file mode 100644
index 0000000..1212c1c
--- /dev/null
+++ b/src/components/LoadingIndicator.js
@@ -0,0 +1,13 @@
+import React, { PureComponent } from 'react'
+
+class LoadingIndicator extends PureComponent {
+ render () {
+ return (
+
+ Loading...
+
+ )
+ }
+}
+
+export default LoadingIndicator
diff --git a/src/components/Question.js b/src/components/Question.js
new file mode 100644
index 0000000..96bf360
--- /dev/null
+++ b/src/components/Question.js
@@ -0,0 +1,79 @@
+import React, { PureComponent } from 'react'
+
+import styles from './question.module.css'
+
+class Question extends PureComponent {
+ render () {
+ const { subjInd, question, onChange, deleteQuestion } = this.props
+
+ let qdata = question.data
+ if (typeof qdata === 'object' && qdata.type === 'simple') {
+ qdata = ''
+ }
+ if (qdata) {
+ try {
+ qdata = JSON.stringify(qdata)
+ } catch (e) {}
+ }
+
+ const qChange = (e) => {
+ onChange(subjInd, question.ind, {
+ ...question,
+ [e.target.name]: e.target.value
+ })
+ }
+
+ // TODO
+ const qDataChange = (e) => {
+ try {
+ let newData = JSON.parse(e.target.value)
+ onChange(subjInd, question.ind, {
+ ...question,
+ data: newData
+ })
+ } catch (e) {
+ console.log('invalid JSON')
+ }
+ }
+
+ return (
+
+ )
+ }
+}
+
+export default Question
diff --git a/src/components/QuestionSearchResult.js b/src/components/QuestionSearchResult.js
new file mode 100644
index 0000000..d02475a
--- /dev/null
+++ b/src/components/QuestionSearchResult.js
@@ -0,0 +1,76 @@
+import React, { PureComponent } from 'react'
+
+import Questions from './Questions.js'
+
+import constants from '../constants.json'
+
+class QuestionSearchResult extends PureComponent {
+ render () {
+ const { data, searchTerm, onChange, deleteQuestion } = this.props
+
+ let subjs = []
+ let results = -1
+
+ const countReducer = (acc, subj) => {
+ return acc + subj.Questions.length
+ }
+
+ if (searchTerm) {
+ subjs = data.Subjects.reduce((acc, subj) => {
+ const resultQuestions = subj.Questions.reduce((qacc, question) => {
+ const keys = [ 'Q', 'A', 'data' ]
+ keys.some((key) => {
+ if (typeof question[key] !== 'string') {
+ return false
+ }
+ if (question[key] && question[key].toLowerCase().includes(searchTerm.toLowerCase())) {
+ qacc.push(question)
+ return true
+ }
+ })
+ return qacc
+ }, [])
+ if (resultQuestions.length > 0) {
+ acc.push({
+ Name: subj.Name,
+ Questions: resultQuestions,
+ ind: subj.ind
+ })
+ }
+ return acc
+ }, [])
+ results = subjs.reduce(countReducer, 0)
+ } else {
+ results = data.Subjects.reduce(countReducer, 0)
+ }
+
+ const renderCount = () => {
+ return (
+
+ {searchTerm ? '' : 'Kezdj el írni kereséshez!'} {results} {searchTerm ? 'találat' : 'kérdés' } {searchTerm ? subjs.length : data.Subjects.length} tárgy
+
+ )
+ }
+
+ if (results > constants.maxQuestionsToRender) {
+ return renderCount()
+ } else {
+ return (
+
+
+ {renderCount()}
+
+
+
+
+
+ )
+ }
+ }
+}
+
+export default QuestionSearchResult
diff --git a/src/components/Questions.js b/src/components/Questions.js
new file mode 100644
index 0000000..e539079
--- /dev/null
+++ b/src/components/Questions.js
@@ -0,0 +1,38 @@
+import React, { PureComponent } from 'react'
+
+import Question from './Question.js'
+
+import styles from './Questions.module.css'
+
+class Questions extends PureComponent {
+ render () {
+ const { subjs, onChange, deleteQuestion } = this.props
+
+ return (
+
+ {subjs.map((subj, i) => {
+ return (
+
+
+ {subj.Name}
+
+ { subj.Questions.map((question, i) => {
+ return (
+
+ )
+ })}
+
+ )
+ })}
+
+ )
+ }
+}
+
+export default Questions
diff --git a/src/components/Questions.module.css b/src/components/Questions.module.css
new file mode 100644
index 0000000..3c0472d
--- /dev/null
+++ b/src/components/Questions.module.css
@@ -0,0 +1,7 @@
+.subjName {
+ font-size: 24px;
+ background-color: #9999ff;
+ color: black;
+ padding: 10px;
+ word-wrap: break-word;
+}
diff --git a/src/components/Subject.js b/src/components/Subject.js
new file mode 100644
index 0000000..b05db08
--- /dev/null
+++ b/src/components/Subject.js
@@ -0,0 +1,33 @@
+import React, { PureComponent } from 'react'
+
+import Question from './Question.js'
+
+class Subject extends PureComponent {
+ render () {
+ const { subj, onChange, deleteQuestion } = this.props
+
+ if (subj) {
+ return (
+
+ {subj.Questions.map((question, i) => {
+ return (
+
+ )
+ })}
+
+ )
+ } else {
+ return (
+
+ )
+ }
+ }
+}
+
+export default Subject
diff --git a/src/components/SubjectSelector.js b/src/components/SubjectSelector.js
new file mode 100644
index 0000000..44a0f49
--- /dev/null
+++ b/src/components/SubjectSelector.js
@@ -0,0 +1,33 @@
+import styles from './SubjectSelector.module.css'
+
+export default function SubjectSelector (props) {
+ const { activeSubjName, searchTerm, data, onSubjSelect } = props
+
+ return (
+
+ {data.Subjects.map((subj, i) => {
+ if (!subj.Name.toLowerCase().includes(searchTerm.toLowerCase())) {
+ return null
+ }
+
+ return (
+
onSubjSelect(subj.Name)}
+ >
+
+ {subj.Name}
+
+
+ [ {subj.Questions.length} ]
+
+
+ )
+ })}
+
+ )
+}
diff --git a/src/components/SubjectSelector.module.css b/src/components/SubjectSelector.module.css
new file mode 100644
index 0000000..f9f2901
--- /dev/null
+++ b/src/components/SubjectSelector.module.css
@@ -0,0 +1,8 @@
+.questionCount {
+ justify-content: flex-end;
+ white-space: nowrap;
+}
+
+.subjName {
+ word-wrap: break-word;
+}
diff --git a/src/components/question.module.css b/src/components/question.module.css
new file mode 100644
index 0000000..1acac5b
--- /dev/null
+++ b/src/components/question.module.css
@@ -0,0 +1,33 @@
+.questionInput {
+ flex-grow: 1;
+ font-size: 22px;
+ background-color: var(--background-color);
+ color: white;
+ border: none;
+ padding: 8px;
+}
+
+.questionContainer {
+ margin-top: 30px;
+ margin-bottom: 30px;
+ margin-right: 10px;
+ margin-left: 10px;
+}
+.inputContainer {
+ width: 100%;
+ display: flex;
+}
+
+.deleteButton {
+ padding: 6px;
+ font-size: 22px;
+ background-color: var(--background-color);
+ color: white;
+ cursor: pointer;
+ border: 1px solid;
+ border-color: var(--background-color);
+}
+
+.deleteButton:hover {
+ border: 1px solid;
+}
diff --git a/src/components/questionView.js b/src/components/questionView.js
new file mode 100644
index 0000000..fd11bdb
--- /dev/null
+++ b/src/components/questionView.js
@@ -0,0 +1,48 @@
+import React, { useState } from 'react'
+
+import LoadingIndicator from '../components/LoadingIndicator.js'
+import QuestionSearchResult from '../components/QuestionSearchResult.js'
+
+import styles from './questionView.module.css'
+
+export default function questionView (props) {
+ const { data, onChange, deleteQuestion } = props
+ const [searchTerm, setSearchTerm] = useState('')
+
+ if (data) {
+ return (
+
+
+ { setSearchTerm(e.target.value) }}
+ />
+
+
+
+
+
+
+
+ )
+ } else {
+ return (
+
+ )
+ }
+}
diff --git a/src/components/questionView.module.css b/src/components/questionView.module.css
new file mode 100644
index 0000000..7280ea9
--- /dev/null
+++ b/src/components/questionView.module.css
@@ -0,0 +1,23 @@
+.searchBar {
+ margin: 10px;
+ padding: 10px;
+ color: white;
+ background-color: #212127;
+ border: none;
+ font-size: 18px;
+ flex-grow: 1;
+}
+
+.searchContainer {
+ width: 100%;
+ display: flex;
+}
+
+.clearButton {
+ width: 80px;
+ background-color: var(--background-color);
+ color: white;
+ font-size: 23px;
+ cursor: pointer;
+ border: none;
+}
diff --git a/src/components/subjectView.js b/src/components/subjectView.js
new file mode 100644
index 0000000..7ba3356
--- /dev/null
+++ b/src/components/subjectView.js
@@ -0,0 +1,60 @@
+import React, { useState } from 'react'
+
+import LoadingIndicator from '../components/LoadingIndicator.js'
+import Subject from '../components/Subject.js'
+import SubjectSelector from '../components/SubjectSelector.js'
+
+import styles from './subjectView.module.css'
+
+export default function SubjectView (props) {
+ const { data, onChange, deleteQuestion } = props
+ const [activeSubjName, setActiveSubjName] = useState('')
+ const [searchTerm, setSearchTerm] = useState('')
+
+ if (data) {
+ let currSubj = data.Subjects.find((subj) => {
+ return subj.Name === activeSubjName
+ })
+
+ return (
+
+
+ { setSearchTerm(e.target.value) }}
+ />
+
+
+
+
{ setActiveSubjName(subjName) }}
+ />
+
+
+
+
+
+ )
+ } else {
+ return (
+
+ )
+ }
+}
diff --git a/src/components/subjectView.module.css b/src/components/subjectView.module.css
new file mode 100644
index 0000000..d33ac33
--- /dev/null
+++ b/src/components/subjectView.module.css
@@ -0,0 +1,24 @@
+.searchBar {
+ margin: 10px;
+ padding: 10px;
+ color: white;
+ background-color: #212127;
+ border: none;
+ font-size: 18px;
+ flex-grow: 1;
+}
+
+
+.searchContainer {
+ width: 100%;
+ display: flex;
+}
+
+.clearButton {
+ width: 80px;
+ background-color: var(--background-color);
+ color: white;
+ font-size: 23px;
+ cursor: pointer;
+ border: none;
+}
diff --git a/src/constants.json b/src/constants.json
new file mode 100644
index 0000000..3465eed
--- /dev/null
+++ b/src/constants.json
@@ -0,0 +1,4 @@
+{
+ "apiUrl": "https://api.frylabs.net/",
+ "maxQuestionsToRender": 250
+}
diff --git a/src/defaultStyles.css b/src/defaultStyles.css
new file mode 100644
index 0000000..5fac2a8
--- /dev/null
+++ b/src/defaultStyles.css
@@ -0,0 +1,166 @@
+:root {
+ --text-color: #9999ff;
+ --bright-color: #f2f2f2;
+ --background-color: #212127;
+ --hoover-color: #202020;
+}
+
+body {
+ font: normal 14px Verdana;
+ color: #999999;
+}
+
+a {
+ color: white;
+}
+
+.link {
+ margin: 20px;
+ font-size: 20px;
+}
+
+.sidebarLink {
+ color: var(--text-color);
+ text-decoration: none;
+}
+
+.sidebar {
+ margin: 0;
+ padding: 0;
+ width: 200px;
+ background-color: #212127;
+ position: fixed;
+ height: 100%;
+ overflow: auto;
+}
+
+.sidebar a {
+ display: block;
+ color: black;
+ padding: 16px;
+ text-decoration: none;
+ color: var(--bright-color);
+}
+
+.sidebar a.active {
+ background-color: var(--text-color);
+ color: black;
+}
+
+.sidebar a:hover:not(.active) {
+ background-color: #555;
+ color: white;
+}
+
+.content {
+ margin-left: 200px;
+ padding: 1px 16px;
+}
+
+.menuicon div {
+ height: 5px;
+ background-color: var(--bright-color);
+ margin: 0px 0;
+ display: none;
+ width: 30px;
+}
+
+.sidebarheader {
+ font-size: 40px;
+ color: var(--bright-color);
+ display: flex;
+ text-align: center;
+}
+
+.headercontainer {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ flex-wrap: nowrap;
+ position: relative;
+ margin: 10px;
+}
+
+.question {
+ word-wrap: break-word;
+ font-weight: bold;
+ font-size: 17px;
+ color: #ffffff;
+}
+
+.answer {
+ word-wrap: break-word;
+ font-size: 15px;
+}
+
+.data {
+ word-wrap: break-word;
+ font-size: 13px;
+ color: #a1a1a1;
+}
+
+.loadingindicator {
+ text-align: center;
+ vertical-align: middle;
+
+ color: #fff;
+ font-size: 30px;
+}
+
+.uquestioncontainer {
+ margin: 5px;
+}
+
+.uquestioncontainer:hover {
+ background-color: var(--hoover-color);
+}
+
+.uquestionscontainer {
+ margin: 10px;
+}
+
+.uquestion {
+ font-weight: 'bold';
+ font-size: 16px;
+ color: #fff;
+ margin: 5px;
+}
+
+.uanswer {
+ margin: 5px;
+}
+
+.uquestionnumber {
+ color: #fff;
+ margin: 5px;
+ font-size: 20px;
+}
+
+.link {
+ margin: 10px;
+}
+
+.subjectSelector {
+ overflow: scroll;
+ height: 200px;
+ margin: 10px;
+}
+
+.subjItem {
+ font-size: 18px;
+ padding: 3px;
+ cursor: pointer;
+ float: 1;
+ display: flex;
+ justify-content: space-between;
+}
+
+.activeSubjItem {
+ background-color: var(--text-color);
+ color: black;
+}
+
+.subjItem:hover:not(.activeSubjItem) {
+ background-color: #555;
+ color: white;
+}
diff --git a/src/layout.js b/src/layout.js
new file mode 100644
index 0000000..f419d2d
--- /dev/null
+++ b/src/layout.js
@@ -0,0 +1,14 @@
+import Link from 'next/link'
+
+export default function Layout (props) {
+ return (
+
+ )
+}
diff --git a/src/pages/_app.js b/src/pages/_app.js
new file mode 100644
index 0000000..4d54978
--- /dev/null
+++ b/src/pages/_app.js
@@ -0,0 +1,23 @@
+// import App from 'next/app'
+
+import '../defaultStyles.css'
+
+function MyApp ({ Component, pageProps, router }) {
+ return (
+
+ )
+}
+
+// Only uncomment this method if you have blocking data requirements for
+// every single page in your application. This disables the ability to
+// perform automatic static optimization, causing every page in your app to
+// be server-side rendered.
+//
+// MyApp.getInitialProps = async (appContext) => {
+// // calls page's `getInitialProps` and fills `appProps.pageProps`
+// const appProps = await App.getInitialProps(appContext);
+//
+// return { ...appProps }
+// }
+
+export default MyApp
diff --git a/src/pages/_document.js b/src/pages/_document.js
new file mode 100644
index 0000000..3076cf6
--- /dev/null
+++ b/src/pages/_document.js
@@ -0,0 +1,22 @@
+import Document, { Html, Head, Main, NextScript } from 'next/document'
+
+class MyDocument extends Document {
+ static async getInitialProps (ctx) {
+ const initialProps = await Document.getInitialProps(ctx)
+ return { ...initialProps }
+ }
+
+ render () {
+ return (
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default MyDocument
diff --git a/src/pages/index.js b/src/pages/index.js
new file mode 100644
index 0000000..09da666
--- /dev/null
+++ b/src/pages/index.js
@@ -0,0 +1,136 @@
+import React, { useState, useEffect } from 'react'
+import fetch from 'unfetch'
+
+import SubjectView from '../components/subjectView'
+import QuestionView from '../components/questionView'
+import LoadingIndicator from '../components/LoadingIndicator'
+
+import styles from './index.module.css'
+import constants from '../constants.json'
+
+const views = {
+ subject: 'SUBJECT',
+ question: 'QUESTION'
+}
+
+// TODO:
+// Add question on subjects view
+// question.data editor
+// save data to server / load it from there
+// save: save question count and subj count
+// save deleted/removed questions ?
+// edit \n-s in questions / answers
+
+export default function Index (props) {
+ const [data, setData] = useState(null)
+ const [view, setView] = useState(views.subject)
+
+ const setIndexes = (d) => {
+ d.Subjects.forEach((subj, i) => {
+ subj.ind = i
+ subj.Questions.forEach((question, j) => {
+ question.ind = j
+ })
+ })
+ return d
+ }
+
+ useEffect(() => {
+ console.info('Fetching data')
+ fetch(`${constants.apiUrl}data.json`)
+ .then((resp) => {
+ return resp.json()
+ })
+ .then((resp) => {
+ if (data) {
+ console.warn('Tried to refetch data when it was still set!')
+ } else {
+ setData(setIndexes(resp))
+ }
+ })
+ }, [])
+
+ const deleteQuestion = (subjInd, questionInd) => {
+ data.Subjects[subjInd].Questions.splice(questionInd, 1)
+
+ setData({
+ ...setIndexes(data)
+ })
+ }
+
+ const onChange = (subjInd, questionInd, newVal) => {
+ data.Subjects[subjInd].Questions[questionInd] = newVal
+ setData({
+ ...data
+ })
+ }
+
+ const renderView = () => {
+ if (view === views.subject) {
+ return (
+
+ )
+ } else if (view === views.question) {
+ return (
+
+ )
+ } else {
+ return (
+
+ No view!
+
+ )
+ }
+ }
+
+ const downloadFile = async (data) => {
+ const json = JSON.stringify(data)
+ const blob = new Blob([json], { type: 'application/json' }) // eslint-disable-line
+ const href = await URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = href
+ link.download = 'data.json'
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+ }
+
+ if (!data) {
+ return (
+
+ )
+ }
+ return (
+
+
+ { setView(views.subject) }}>
+ Subject view
+
+ { setView(views.question) }}>
+ Question view
+
+ {
+ downloadFile(data)
+ }}
+ >
+ Download result
+
+
+ {renderView()}
+
+ )
+}
diff --git a/src/pages/index.module.css b/src/pages/index.module.css
new file mode 100644
index 0000000..7cb10e0
--- /dev/null
+++ b/src/pages/index.module.css
@@ -0,0 +1,33 @@
+.tabButton {
+ display: inline-block;
+ margin: 5px;
+ padding: 5px;
+ height: 45px;
+ font-size: 30px;
+ width: 25%;
+ text-align: center;
+ border-color: var(--background-color);
+ border: 1px solid;
+}
+
+.downloadButton {
+ display: inline-block;
+ margin: 5px;
+ padding: 5px;
+ height: 45px;
+ font-size: 30px;
+ width: 26%;
+ text-align: center;
+ border-color: var(--background-color);
+ border: 1px solid;
+ word-wrap: none;
+}
+
+.tabButton:hover {
+ border-color: white;
+}
+
+.buttonContainer {
+ text-align: center;
+ width: 100%;
+}