From 53b4158967942aae312a28b226068c5575141077 Mon Sep 17 00:00:00 2001 From: MrFry Date: Wed, 25 Mar 2020 13:25:41 +0100 Subject: [PATCH] Initial commit, with fully working project :p --- package.json | 21 +++ src/components/LoadingIndicator.js | 13 ++ src/components/Question.js | 79 ++++++++++ src/components/QuestionSearchResult.js | 76 ++++++++++ src/components/Questions.js | 38 +++++ src/components/Questions.module.css | 7 + src/components/Subject.js | 33 +++++ src/components/SubjectSelector.js | 33 +++++ src/components/SubjectSelector.module.css | 8 ++ src/components/question.module.css | 33 +++++ src/components/questionView.js | 48 +++++++ src/components/questionView.module.css | 23 +++ src/components/subjectView.js | 60 ++++++++ src/components/subjectView.module.css | 24 ++++ src/constants.json | 4 + src/defaultStyles.css | 166 ++++++++++++++++++++++ src/layout.js | 14 ++ src/pages/_app.js | 23 +++ src/pages/_document.js | 22 +++ src/pages/index.js | 136 ++++++++++++++++++ src/pages/index.module.css | 33 +++++ 21 files changed, 894 insertions(+) create mode 100644 package.json create mode 100644 src/components/LoadingIndicator.js create mode 100644 src/components/Question.js create mode 100644 src/components/QuestionSearchResult.js create mode 100644 src/components/Questions.js create mode 100644 src/components/Questions.module.css create mode 100644 src/components/Subject.js create mode 100644 src/components/SubjectSelector.js create mode 100644 src/components/SubjectSelector.module.css create mode 100644 src/components/question.module.css create mode 100644 src/components/questionView.js create mode 100644 src/components/questionView.module.css create mode 100644 src/components/subjectView.js create mode 100644 src/components/subjectView.module.css create mode 100644 src/constants.json create mode 100644 src/defaultStyles.css create mode 100644 src/layout.js create mode 100644 src/pages/_app.js create mode 100644 src/pages/_document.js create mode 100644 src/pages/index.js create mode 100644 src/pages/index.module.css 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 ( +
+
+ +
+
+ +
+
+ + { deleteQuestion(subjInd, question.ind) }} + > + Delete question + +
+
+ ) + } +} + +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 ( +
+ + Question view + + + subjectView view + +
+ ) +} 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%; +}