Formeln von MathJax auf KaTeX umgestellt
This commit is contained in:
@@ -2,7 +2,7 @@ Brüche werden verwendet, um Anteile an einem Ganzen darzustellen. So kann es vo
|
|||||||
<br><br> Ein Bruch setzt sich aus einem Zähler, einem Bruchstrich und einem Nenner zusammen. Der Zähler wird über dem Bruchstrich geschrieben, der Nenner darunter.
|
<br><br> Ein Bruch setzt sich aus einem Zähler, einem Bruchstrich und einem Nenner zusammen. Der Zähler wird über dem Bruchstrich geschrieben, der Nenner darunter.
|
||||||
<br><br>
|
<br><br>
|
||||||
$$
|
$$
|
||||||
\begin{array}{c@{\quad}l}
|
\begin{array}{cl}
|
||||||
3 & \text{Zahler} \\
|
3 & \text{Zahler} \\
|
||||||
- & \text{Bruchstrich} \\
|
- & \text{Bruchstrich} \\
|
||||||
7 & \text{Nenner} \\
|
7 & \text{Nenner} \\
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
$$
|
$$
|
||||||
\begin{array}{r@{}r@{}r@{}r}
|
\begin{array}{rrrr}
|
||||||
& 8 & 4 & 0 & : & 4 & = & 2&1&0 \\
|
& 8 & 4 & 0 & : & 4 & = & 2&1&0 \\
|
||||||
\hline
|
\hline
|
||||||
- & 8 & & \\ % Erste Subtraktion
|
- & 8 & & \\ % Erste Subtraktion
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Kommen wir nun zur schriftlichen Multiplikation: Das Ziel dieses Artikels ist es
|
|||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
$$
|
$$
|
||||||
\begin{array}{r@{}r@{}r@{}r}
|
\begin{array}{rrrr}
|
||||||
& 1 & 2 & \times & 3 & 2 & \\ \hline % mal 32
|
& 1 & 2 & \times & 3 & 2 & \\ \hline % mal 32
|
||||||
& & & 3 & 6 \\ % 12 * 2 = 36
|
& & & 3 & 6 \\ % 12 * 2 = 36
|
||||||
+ &&&& 2 & 4 \\ \hline % 12 * 3 mit Zehnerstelle = 240
|
+ &&&& 2 & 4 \\ \hline % 12 * 3 mit Zehnerstelle = 240
|
||||||
@@ -23,7 +23,7 @@ Das Ergebnis ist somit 12 · 32 = 384.<br>
|
|||||||
<br> <br> Ein weiteres Beispiel <br>
|
<br> <br> Ein weiteres Beispiel <br>
|
||||||
|
|
||||||
$$
|
$$
|
||||||
\begin{array}{r@{}r@{}r@{}r@{}r@{}r}
|
\begin{array}{rrrrr}
|
||||||
2 & 8 & 4 & 6 & 8 & \times & 1 & 6 \\ \hline % mal 16
|
2 & 8 & 4 & 6 & 8 & \times & 1 & 6 \\ \hline % mal 16
|
||||||
&& \textcolor{red}{2} & \textcolor{red}{8} & \textcolor{red}{4} & \textcolor{red}{6} & \textcolor{red}{8} \\ % 28468 * 6 (Einerstelle)
|
&& \textcolor{red}{2} & \textcolor{red}{8} & \textcolor{red}{4} & \textcolor{red}{6} & \textcolor{red}{8} \\ % 28468 * 6 (Einerstelle)
|
||||||
+ && \textcolor{green}{1} & \textcolor{green}{7} & \textcolor{green}{0} & \textcolor{green}{8} & \textcolor{green}{0} & \textcolor{green}{8} \\ \hline % 28468 * 10 (Zehnerstelle)
|
+ && \textcolor{green}{1} & \textcolor{green}{7} & \textcolor{green}{0} & \textcolor{green}{8} & \textcolor{green}{0} & \textcolor{green}{8} \\ \hline % 28468 * 10 (Zehnerstelle)
|
||||||
|
|||||||
@@ -30,8 +30,29 @@ if (!isset($topicData)) {
|
|||||||
<link href="assets/css/topic.css" rel="stylesheet">
|
<link href="assets/css/topic.css" rel="stylesheet">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
<!--<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>-->
|
||||||
<script src="assets/js/sidebar.js"></script>
|
<script src="assets/js/sidebar.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.18/dist/katex.min.css" integrity="sha384-veTAhWILPOotXm+kbR5uY7dRamYLJf58I7P+hJhjeuc7hsMAkJHTsPahAl0hBST0" crossorigin="anonymous">
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.18/dist/katex.min.js" integrity="sha384-v6mkHYHfY/4BWq54f7lQAdtIsoZZIByznQ3ZqN38OL4KCsrxo31SLlPiak7cj/Mg" crossorigin="anonymous"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.18/dist/contrib/auto-render.min.js" integrity="sha384-hCXGrW6PitJEwbkoStFjeJxv+fSOOQKOPbJxSfM6G5sWZjAyWhXiTIIAmQqnlLlh" crossorigin="anonymous" onload="renderMathInElement(document.body);"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
renderMathInElement(document.body, {
|
||||||
|
// customised options
|
||||||
|
// • auto-render specific keys, e.g.:
|
||||||
|
delimiters: [
|
||||||
|
{left: '$$', right: '$$', display: true},
|
||||||
|
{left: '$', right: '$', display: false},
|
||||||
|
{left: '\\(', right: '\\)', display: false},
|
||||||
|
{left: '\\[', right: '\\]', display: true}
|
||||||
|
],
|
||||||
|
// • rendering keys, e.g.:
|
||||||
|
throwOnError : false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,88 @@ require_once("classes/TopicData.php");
|
|||||||
$allSubjects = SubjectData::getAll();
|
$allSubjects = SubjectData::getAll();
|
||||||
$editingTopic = null;
|
$editingTopic = null;
|
||||||
|
|
||||||
|
$defaultValues = array();
|
||||||
|
$defaultValues['displayName'] = "";
|
||||||
|
$defaultValues['id'] = "";
|
||||||
|
$defaultValues['subjectId'] = "";
|
||||||
|
$defaultValues['description'] = "";
|
||||||
|
$defaultValues['icon'] = "";
|
||||||
|
$defaultValues['relatedTopics'] = "";
|
||||||
|
$defaultValues['article'] = "";
|
||||||
|
|
||||||
|
$errors = array();
|
||||||
|
|
||||||
if (isset($_GET['subjectId']) && isset($_GET['topicId'])) {
|
if (isset($_GET['subjectId']) && isset($_GET['topicId'])) {
|
||||||
if (isset($allSubjects[$_GET['subjectId']]->getTopics()[$_GET['topicId']])) {
|
if (isset($allSubjects[$_GET['subjectId']]->getTopics()[$_GET['topicId']])) {
|
||||||
$editingTopic = $allSubjects[$_GET['subjectId']]->getTopics()[$_GET['topicId']];
|
$editingTopic = $allSubjects[$_GET['subjectId']]->getTopics()[$_GET['topicId']];
|
||||||
|
|
||||||
|
$defaultValues['displayName'] = $editingTopic->getDisplayName();
|
||||||
|
$defaultValues['id'] = $editingTopic->getId();
|
||||||
|
$defaultValues['subjectId'] = $editingTopic->getSubjectId();
|
||||||
|
$defaultValues['description'] = $editingTopic->getDescription();
|
||||||
|
$defaultValues['icon'] = $editingTopic->getIcon();
|
||||||
|
$defaultValues['relatedTopics'] = implode(", ", $editingTopic->getRelatedTopics());
|
||||||
|
$defaultValues['article'] = str_replace("$$", "<br>$$<br>", $editingTopic->getFinishedArticle());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
|
foreach ($defaultValues as $key => $value) {
|
||||||
|
$defaultValues[$key] = $_POST[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($_POST['displayName']) || trim($_POST['displayName']) == "") {
|
||||||
|
$errors["displayName"] = "Bitte geben Sie einen Namen an.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($_POST['id']) || trim($_POST['id']) == "") {
|
||||||
|
$errors["id"] = "Bitte geben Sie eine ID an.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($_POST['subjectId']) || trim($_POST['subjectId']) == "") {
|
||||||
|
$errors["subjectId"] = "Bitte geben Sie ein Fach an.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($_POST['description']) || trim($_POST['description']) == "") {
|
||||||
|
$errors["description"] = "Bitte geben Sie eine Beschreibung an.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($_POST['icon']) || trim($_POST['icon']) == "") {
|
||||||
|
$errors["icon"] = "Bitte geben Sie ein Icon an.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($_POST['article']) || trim($_POST['article']) == "") {
|
||||||
|
$errors["article"] = "Bitte geben Sie einen Erklärtext an.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(empty($errors)) {
|
||||||
|
$newTopic = false;
|
||||||
|
if(isset($allSubjects[$_POST['subjectId']]->getTopics()[$_POST['id']])) {
|
||||||
|
$newTopic = $allSubjects[$_POST['subjectId']]->getTopics()[$_POST['id']];
|
||||||
|
$newTopic->setDisplayName($_POST['displayName']);
|
||||||
|
$newTopic->setSubjectId($_POST['subjectId']);
|
||||||
|
$newTopic->setDescription($_POST['description']);
|
||||||
|
$newTopic->setIcon($_POST['icon']);
|
||||||
|
|
||||||
|
$relatedTopics = array();
|
||||||
|
foreach (explode(",", $_POST['relatedTopics']) as $relatedTopic) {
|
||||||
|
$relatedTopics[] = trim($relatedTopic);
|
||||||
|
}
|
||||||
|
$newTopic->setRelatedTopics($relatedTopics);
|
||||||
|
$newTopic->setArticle($_POST['article']);
|
||||||
|
} else {
|
||||||
|
$newTopic = TopicData::createNew($_POST['id'], $_POST['subjectId'], $_POST['displayName'], $_POST['icon'], $_POST['description'], $_POST['relatedTopics'], $_POST['article']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$newTopic) {
|
||||||
|
$errors["error"] = "Fehler beim Speichern des Themas.";
|
||||||
|
} else {
|
||||||
|
$newTopic->save();
|
||||||
|
header("Location: " . "topic.php?subject=" . $newTopic->getSubjectId() . "&topic=" . $newTopic->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -31,39 +107,43 @@ if (isset($_GET['subjectId']) && isset($_GET['topicId'])) {
|
|||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.18/dist/katex.min.css" integrity="sha384-veTAhWILPOotXm+kbR5uY7dRamYLJf58I7P+hJhjeuc7hsMAkJHTsPahAl0hBST0" crossorigin="anonymous">
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.18/dist/katex.min.js" integrity="sha384-v6mkHYHfY/4BWq54f7lQAdtIsoZZIByznQ3ZqN38OL4KCsrxo31SLlPiak7cj/Mg" crossorigin="anonymous"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.18/dist/contrib/auto-render.min.js" integrity="sha384-hCXGrW6PitJEwbkoStFjeJxv+fSOOQKOPbJxSfM6G5sWZjAyWhXiTIIAmQqnlLlh" crossorigin="anonymous" onload="renderMathInElement(document.body);"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="min-h-screen bg-gray-50">
|
<body class="min-h-screen bg-gray-50">
|
||||||
|
|
||||||
<form id="topicForm" class="space-y-8">
|
<form id="topicForm" method="post" class="space-y-8">
|
||||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-8">
|
||||||
<!-- Left column - Topic details -->
|
<!-- Left column - Topic details -->
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label for="topicNameSelect" class="block text-sm font-medium text-gray-700">Themenname</label>
|
<label for="topicNameSelect" class="block text-sm font-medium text-gray-700">Themenname</label>
|
||||||
<input id="topicNameSelect" type="text" name="displayName"
|
<input id="topicNameSelect" type="text" name="displayName"
|
||||||
class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm" required value="<?php
|
class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm" required value="<?php echo $defaultValues['displayName']; ?>">
|
||||||
if (isset($editingTopic)) {
|
</div>
|
||||||
echo $editingTopic->getDisplayName();
|
|
||||||
}
|
<div>
|
||||||
?>">
|
<label for="topicIdSelect" class="block text-sm font-medium text-gray-700">ID</label>
|
||||||
|
<input id="topicIdSelect" type="text" name="id"
|
||||||
|
class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm" required value="<?php echo $defaultValues['id']; ?>">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="topicSubjectSelect" class="block text-sm font-medium text-gray-700">Fach</label>
|
<label for="topicSubjectSelect" class="block text-sm font-medium text-gray-700">Fach</label>
|
||||||
<select id="topicSubjectSelect" name="subject"
|
<select id="topicSubjectSelect" name="subjectId"
|
||||||
class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm" required>
|
class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm" required>
|
||||||
<option value="">Fach auswählen...</option>
|
<option value="">Fach auswählen...</option>
|
||||||
<?php
|
<?php
|
||||||
foreach ($allSubjects as $subject) {
|
foreach ($allSubjects as $subject) {
|
||||||
$selected = "";
|
$selected = "";
|
||||||
if (isset($editingTopic)) {
|
if ($defaultValues['subjectId'] === $subject->getId()) {
|
||||||
if ($editingTopic->getSubjectId() === $subject->getId()) {
|
$selected = "selected";
|
||||||
$selected = "selected";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "<option $selected value='" . $subject->getID() . "'>" . $subject->getDisplayName() . "</option>";
|
echo "<option $selected value='" . $subject->getId() . "'>" . $subject->getDisplayName() . "</option>";
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</select>
|
</select>
|
||||||
@@ -73,32 +153,20 @@ if (isset($_GET['subjectId']) && isset($_GET['topicId'])) {
|
|||||||
<label for="topicDescriptionSelect"
|
<label for="topicDescriptionSelect"
|
||||||
class="block text-sm font-medium text-gray-700">Kurzbeschreibung</label>
|
class="block text-sm font-medium text-gray-700">Kurzbeschreibung</label>
|
||||||
<textarea id="topicDescriptionSelect" name="description" rows="5"
|
<textarea id="topicDescriptionSelect" name="description" rows="5"
|
||||||
class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm" required><?php
|
class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm" required><?php echo $defaultValues['description']; ?></textarea>
|
||||||
if (isset($editingTopic)) {
|
|
||||||
echo $editingTopic->getDescription();
|
|
||||||
}
|
|
||||||
?></textarea>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="iconSelect" class="block text-sm font-medium text-gray-700">Icon</label>
|
<label for="iconSelect" class="block text-sm font-medium text-gray-700">Icon</label>
|
||||||
<input class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm" type="text" name="icon"
|
<input class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm" type="text" name="icon"
|
||||||
id="iconSelect" value="<?php
|
id="iconSelect" value="<?php echo $defaultValues['icon']; ?>">
|
||||||
if (isset($editingTopic)) {
|
|
||||||
echo $editingTopic->getIcon();
|
|
||||||
}
|
|
||||||
?>">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="relatedTopicSelect" class="block text-sm font-medium text-gray-700">Verwandte Themen
|
<label for="relatedTopicSelect" class="block text-sm font-medium text-gray-700">Verwandte Themen
|
||||||
als IDs und kommagetrennt</label>
|
als IDs und kommagetrennt</label>
|
||||||
<input class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm" type="text"
|
<input class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm" type="text"
|
||||||
name="related-topics" id="relatedTopicSelect" value="<?php
|
name="relatedTopics" id="relatedTopicSelect" value="<?php echo $defaultValues['relatedTopics']; ?>">
|
||||||
if (isset($editingTopic)) {
|
|
||||||
echo implode(", ", $editingTopic->getRelatedTopics());
|
|
||||||
}
|
|
||||||
?>">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -120,24 +188,16 @@ if (isset($_GET['subjectId']) && isset($_GET['topicId'])) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="quillEditor" style="height: 400px;" class="bg-white border rounded-lg">
|
<div id="quillEditor" style="height: 400px;" class="bg-white border rounded-lg">
|
||||||
<?php
|
<?php echo $defaultValues['article']; ?>
|
||||||
if (isset($editingTopic)) {
|
|
||||||
$article = $editingTopic->getFinishedArticle();
|
|
||||||
$article = str_replace("$$", "<br>$$<br>", $article);
|
|
||||||
echo $article;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
|
||||||
<div class="border rounded-lg p-8 mt-8 bg-gray-50">
|
<div class="border rounded-lg p-8 mt-8 bg-gray-50">
|
||||||
<h4 class="text-lg font-medium text-gray-700 mb-4">Vorschau</h4>
|
<h4 class="text-lg font-medium text-gray-700 mb-4">Vorschau</h4>
|
||||||
<div id="contentPreview" class="prose prose-lg max-w-none bg-white p-6 rounded-lg shadow-sm"></div>
|
<div id="contentPreview" class="prose prose-lg max-w-none bg-white p-6 rounded-lg shadow-sm"></div>
|
||||||
</div>
|
</div>
|
||||||
-->
|
|
||||||
|
|
||||||
<div class="flex justify-end gap-4">
|
<div class="flex justify-end gap-4">
|
||||||
<button type="button" onclick="closeModal()"
|
<button type="button" onclick="closeModal()"
|
||||||
@@ -154,6 +214,17 @@ if (isset($_GET['subjectId']) && isset($_GET['topicId'])) {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const Inline = Quill.import('blots/inline');
|
||||||
|
const Block = Quill.import('blots/block');
|
||||||
|
const Embed = Quill.import('blots/embed');
|
||||||
|
|
||||||
|
class FormulaBlot extends Embed {
|
||||||
|
static blotName = 'formula';
|
||||||
|
static tagName = 'math';
|
||||||
|
}
|
||||||
|
|
||||||
|
//Quill.register(FormulaBlot);
|
||||||
|
|
||||||
const quill = new Quill('#quillEditor', {
|
const quill = new Quill('#quillEditor', {
|
||||||
modules: {
|
modules: {
|
||||||
toolbar: [
|
toolbar: [
|
||||||
@@ -161,15 +232,15 @@ if (isset($_GET['subjectId']) && isset($_GET['topicId'])) {
|
|||||||
['bold', 'italic', 'underline', 'strike'],
|
['bold', 'italic', 'underline', 'strike'],
|
||||||
[{'script': 'super'}, {'script': 'sub'}],
|
[{'script': 'super'}, {'script': 'sub'}],
|
||||||
[{'list': 'ordered'}, {'list': 'bullet'}],
|
[{'list': 'ordered'}, {'list': 'bullet'}],
|
||||||
['link', 'image'],
|
['link', 'image', 'formula'],
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
theme: 'snow'
|
theme: 'snow'
|
||||||
});
|
});
|
||||||
|
|
||||||
quill.on('text-change', (delta, oldDelta, source) => {
|
quill.on('text-change', (delta, oldDelta, source) => {
|
||||||
const html = quill.getSemanticHTML();
|
const html = quill.getSemanticHTML().replace(/ /g, " ");
|
||||||
//document.getElementById('contentPreview').innerHTML = html;
|
document.getElementById('contentPreview').innerHTML = html;
|
||||||
document.getElementById('article-upload-field').value = html;
|
document.getElementById('article-upload-field').value = html;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user