Update frontend
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
ff2b884d42
commit
2ed278683b
|
@ -63,8 +63,9 @@ module Backend
|
|||
@status = Status::JWTError
|
||||
else
|
||||
pp! payload
|
||||
|
||||
if @user = Db::User.find(payload.context.user)
|
||||
if payload.iss != "Mentorenwahl" || payload.vrs != Backend::VERSION
|
||||
@status = Status::JWTError
|
||||
elsif @user = Db::User.find(payload.context.user)
|
||||
@admin = user.not_nil!.admin
|
||||
@role = user.not_nil!.role.to_api
|
||||
@external =
|
||||
|
|
|
@ -48,7 +48,7 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# Creates user
|
||||
def create_user(context : Context, input : UserCreateInput, check_ldap : Bool = true) : User
|
||||
def create_user(context : Context, input : UserCreateInput, check_ldap : Bool = true) : User?
|
||||
context.admin!
|
||||
|
||||
raise Errors::LdapUserDoesNotExist.new if check_ldap && begin
|
||||
|
@ -64,7 +64,7 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# Deletes user by ID
|
||||
def delete_user(context : Context, id : Int32) : Int32
|
||||
def delete_user(context : Context, id : Int32) : Int32?
|
||||
context.admin!
|
||||
|
||||
user = Db::User.find!(id)
|
||||
|
@ -75,7 +75,7 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# Starts assignment job of mentors to students
|
||||
def assign_students(context : Context) : Bool
|
||||
def assign_students(context : Context) : Bool?
|
||||
context.admin!
|
||||
|
||||
Worker::Jobs::AssignStudentsJob.new.enqueue
|
||||
|
@ -92,6 +92,15 @@ module Backend
|
|||
# Teacher.new(teacher)
|
||||
# end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def register_teacher(context : Context, input : TeacherInput) : Teacher?
|
||||
context.teacher!(false)
|
||||
raise Errors::InvalidPermissions.new if context.user.not_nil!.teacher
|
||||
|
||||
teacher = Db::Teacher.create!(user_id: context.user.not_nil!.id, max_students: input.max_students)
|
||||
Teacher.new(teacher)
|
||||
end
|
||||
|
||||
# @[GraphQL::Field]
|
||||
# # Deletes teacher by ID
|
||||
# def delete_teacher(context : Context, id : Int32) : Int32
|
||||
|
@ -148,7 +157,7 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# Creates vote for authenticated user's student
|
||||
def create_vote(context : Context, input : VoteCreateInput) : Vote
|
||||
def create_vote(context : Context, input : VoteCreateInput) : Vote?
|
||||
context.student!
|
||||
|
||||
raise Errors::DuplicateTeachers.new if input.teacher_ids.uniq.size != input.teacher_ids.size
|
||||
|
|
|
@ -33,7 +33,7 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# Current authenticated user
|
||||
def me(context : Context) : User
|
||||
def me(context : Context) : User?
|
||||
context.authenticated!
|
||||
|
||||
User.new(context.user.not_nil!)
|
||||
|
@ -41,7 +41,7 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# User by ID
|
||||
def user(context : Context, id : Int32) : User
|
||||
def user(context : Context, id : Int32) : User?
|
||||
context.admin!
|
||||
|
||||
User.from_id(id)
|
||||
|
@ -49,14 +49,14 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# All users
|
||||
def users(context : Context) : Array(User)
|
||||
def users(context : Context) : Array(User)?
|
||||
context.admin!
|
||||
|
||||
Db::User.query.map { |user| User.new(user) }
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def user_by_username(context : Context, username : String) : User
|
||||
def user_by_username(context : Context, username : String) : User?
|
||||
context.admin!
|
||||
|
||||
User.new(Db::User.query.find { var(:username) == username }.not_nil!)
|
||||
|
@ -64,7 +64,7 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# All admins
|
||||
def admins(context : Context) : Array(User)
|
||||
def admins(context : Context) : Array(User)?
|
||||
context.admin!
|
||||
|
||||
Db::User.query.where(admin: true).map { |user| User.new(user) }
|
||||
|
@ -84,7 +84,7 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# Student by ID
|
||||
def student(context : Context, id : Int32) : Student
|
||||
def student(context : Context, id : Int32) : Student?
|
||||
context.admin!
|
||||
|
||||
Student.new(Db::Student.find!(id))
|
||||
|
@ -92,7 +92,7 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# All students
|
||||
def students(context : Context) : Array(Student)
|
||||
def students(context : Context) : Array(Student)?
|
||||
context.admin!
|
||||
|
||||
Db::Student.query.map { |student| Student.new(student) }
|
||||
|
@ -100,7 +100,7 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# Vote by ID
|
||||
def vote(context : Context, id : Int32) : Vote
|
||||
def vote(context : Context, id : Int32) : Vote?
|
||||
context.admin!
|
||||
|
||||
Vote.new(Db::Vote.find!(id))
|
||||
|
@ -108,7 +108,7 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# All votes
|
||||
def votes(context : Context) : Array(Vote)
|
||||
def votes(context : Context) : Array(Vote)?
|
||||
context.admin!
|
||||
|
||||
Db::Vote.query.map { |vote| Vote.new(vote) }
|
||||
|
@ -116,7 +116,7 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# All students voted
|
||||
def all_students_voted(context : Context) : Bool
|
||||
def all_students_voted(context : Context) : Bool?
|
||||
context.admin!
|
||||
|
||||
votes = Db::Vote.query.count
|
||||
|
@ -135,7 +135,7 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# Teacher vote by ID
|
||||
def teacher_vote(context : Context, id : Int32) : TeacherVote
|
||||
def teacher_vote(context : Context, id : Int32) : TeacherVote?
|
||||
context.admin!
|
||||
|
||||
TeacherVote.new(Db::TeacherVote.find!(id))
|
||||
|
@ -143,7 +143,7 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# All teacher votes
|
||||
def teacher_votes(context : Context) : Array(TeacherVote)
|
||||
def teacher_votes(context : Context) : Array(TeacherVote)?
|
||||
context.admin!
|
||||
|
||||
Db::TeacherVote.query.map { |vote| TeacherVote.new(vote) }
|
||||
|
|
|
@ -92,7 +92,7 @@ module Backend
|
|||
in Api::Schema::UserRole::Student
|
||||
Db::Student.create!(user_id: user.id)
|
||||
in Api::Schema::UserRole::Teacher
|
||||
Db::Teacher.create!(user_id: user.id)
|
||||
# Db::Teacher.create!(user_id: user.id)
|
||||
end
|
||||
|
||||
Worker::Jobs::CacheLdapUserJob.new(user.id).enqueue
|
||||
|
|
|
@ -18,3 +18,4 @@ Dockerfile
|
|||
.gitignore
|
||||
.dockerignore
|
||||
vendor/
|
||||
.editorconfig
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# Mentorenwahl: A fullstack application for assigning mentors to students based on their whishes.
|
||||
# Copyright (C) 2022 Dominic Grimm
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
root = true
|
||||
|
||||
[*.rs]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
|
@ -3,6 +3,12 @@ name = "frontend"
|
|||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = "z"
|
||||
panic = "abort"
|
||||
|
||||
[dependencies]
|
||||
yew = "0.19.3"
|
||||
wasm-logger = "0.2.0"
|
||||
|
@ -17,3 +23,4 @@ wasm-cookies = "0.1.0"
|
|||
lazy_static = "1.4.0"
|
||||
const_format = "0.2.30"
|
||||
yew-agent = "0.1.0"
|
||||
yew-side-effect = "0.2.0"
|
||||
|
|
|
@ -26,6 +26,12 @@ WORKDIR /usr/src/public
|
|||
COPY --from=builder /usr/src/frontend/dist .
|
||||
RUN minify . -r -o .
|
||||
|
||||
FROM alpine as binaryen
|
||||
RUN apk add --no-cache binaryen
|
||||
WORKDIR /usr/src/public
|
||||
COPY --from=public /usr/src/public .
|
||||
RUN find . -name "*.wasm" -type f | xargs -I % wasm-opt % -o % -O --intrinsic-lowering -Oz
|
||||
|
||||
FROM nginx:alpine as runner
|
||||
COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=public /usr/src/public /var/www/html
|
||||
COPY --from=binaryen /usr/src/public /var/www/html
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
mutation RegisterTeacher($maxStudents: Int!) {
|
||||
registerTeacher(input: { maxStudents: $maxStudents }) {
|
||||
id
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
query Config {
|
||||
config {
|
||||
minimumTeacherSelectionCount
|
||||
}
|
||||
}
|
|
@ -3,8 +3,12 @@ query Me {
|
|||
firstName
|
||||
role
|
||||
student {
|
||||
vote
|
||||
vote {
|
||||
id
|
||||
}
|
||||
}
|
||||
teacher {
|
||||
id
|
||||
}
|
||||
teacher
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
query StudentsCanVote {
|
||||
studentsCanVote
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
query Teachers {
|
||||
teachers {
|
||||
id
|
||||
user {
|
||||
name(formal: true)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,23 +3,23 @@ type Config {
|
|||
}
|
||||
|
||||
type Query {
|
||||
admins: [User!]!
|
||||
allStudentsVoted: Boolean!
|
||||
admins: [User!]
|
||||
allStudentsVoted: Boolean
|
||||
config: Config!
|
||||
me: User!
|
||||
me: User
|
||||
ok: Boolean!
|
||||
student(id: Int!): Student!
|
||||
students: [Student!]!
|
||||
student(id: Int!): Student
|
||||
students: [Student!]
|
||||
studentsCanVote: Boolean!
|
||||
teacher(id: Int!): Teacher!
|
||||
teacherVote(id: Int!): TeacherVote!
|
||||
teacherVotes: [TeacherVote!]!
|
||||
teacherVote(id: Int!): TeacherVote
|
||||
teacherVotes: [TeacherVote!]
|
||||
teachers: [Teacher!]!
|
||||
user(id: Int!): User!
|
||||
userByUsername(username: String!): User!
|
||||
users: [User!]!
|
||||
vote(id: Int!): Vote!
|
||||
votes: [Vote!]!
|
||||
user(id: Int!): User
|
||||
userByUsername(username: String!): User
|
||||
users: [User!]
|
||||
vote(id: Int!): Vote
|
||||
votes: [Vote!]
|
||||
}
|
||||
|
||||
type Student {
|
||||
|
@ -74,11 +74,16 @@ type LoginPayload {
|
|||
}
|
||||
|
||||
type Mutation {
|
||||
assignStudents: Boolean!
|
||||
createUser(checkLdap: Boolean! = true, input: UserCreateInput!): User!
|
||||
createVote(input: VoteCreateInput!): Vote!
|
||||
deleteUser(id: Int!): Int!
|
||||
assignStudents: Boolean
|
||||
createUser(checkLdap: Boolean! = true, input: UserCreateInput!): User
|
||||
createVote(input: VoteCreateInput!): Vote
|
||||
deleteUser(id: Int!): Int
|
||||
login(password: String!, username: String!): LoginPayload
|
||||
registerTeacher(input: TeacherInput!): Teacher
|
||||
}
|
||||
|
||||
input TeacherInput {
|
||||
maxStudents: Int!
|
||||
}
|
||||
|
||||
input UserCreateInput {
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
pub mod logged_in;
|
||||
pub mod teacher_registered;
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use yew_agent::{Agent, AgentLink, Context, HandlerId};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum Request {
|
||||
Registered,
|
||||
}
|
||||
|
||||
pub struct EventBus {
|
||||
link: AgentLink<Self>,
|
||||
subscribers: HashSet<HandlerId>,
|
||||
}
|
||||
|
||||
impl Agent for EventBus {
|
||||
type Reach = Context<Self>;
|
||||
type Message = ();
|
||||
type Input = Request;
|
||||
type Output = ();
|
||||
|
||||
fn create(link: AgentLink<Self>) -> Self {
|
||||
Self {
|
||||
link,
|
||||
subscribers: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _msg: Self::Message) {}
|
||||
|
||||
fn handle_input(&mut self, msg: Self::Input, _id: HandlerId) {
|
||||
match msg {
|
||||
Request::Registered => {
|
||||
for sub in self.subscribers.iter() {
|
||||
self.link.respond(*sub, ());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn connected(&mut self, id: HandlerId) {
|
||||
self.subscribers.insert(id);
|
||||
}
|
||||
|
||||
fn disconnected(&mut self, id: HandlerId) {
|
||||
self.subscribers.remove(&id);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct GraphQLErrorsProps {
|
||||
pub errors: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub struct GraphQLErrors;
|
||||
|
||||
impl Component for GraphQLErrors {
|
||||
type Message = ();
|
||||
type Properties = GraphQLErrorsProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
if let Some(errors) = &ctx.props().errors {
|
||||
<div>
|
||||
<p style="color: red;">{ "Errors:" }</p>
|
||||
<div>
|
||||
{ for errors.iter().map(|e| html! { <p style="color: red;">{ e }</p> }) }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert(errors: Option<Vec<graphql_client::Error>>) -> Option<Vec<String>> {
|
||||
errors.map(|x| x.iter().map(|e| e.message.to_owned()).collect())
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
pub mod graphql_errors;
|
||||
pub mod logged_in_handler;
|
||||
|
|
|
@ -12,3 +12,17 @@ lazy_static! {
|
|||
.unwrap()
|
||||
.to_string();
|
||||
}
|
||||
|
||||
pub fn client(token: Option<&String>) -> Result<reqwest::Client, reqwest::Error> {
|
||||
if let Some(x) = token {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
"Authorization",
|
||||
reqwest::header::HeaderValue::from_str(&format!("Bearer {}", x)).unwrap(),
|
||||
);
|
||||
|
||||
reqwest::Client::builder().default_headers(headers).build()
|
||||
} else {
|
||||
Ok(reqwest::Client::new())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,6 @@ use graphql_client::GraphQLQuery;
|
|||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/mutations/login.graphql",
|
||||
response_derives = "Debug,Serialize,Deserialize"
|
||||
response_derives = "Debug"
|
||||
)]
|
||||
pub struct Login;
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
pub mod login;
|
||||
pub mod register_teacher;
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
use graphql_client::GraphQLQuery;
|
||||
|
||||
#[derive(GraphQLQuery)]
|
||||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/mutations/register_teacher.graphql",
|
||||
response_derives = "Debug"
|
||||
)]
|
||||
pub struct RegisterTeacher;
|
|
@ -0,0 +1,9 @@
|
|||
use graphql_client::GraphQLQuery;
|
||||
|
||||
#[derive(GraphQLQuery)]
|
||||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/queries/config.graphql",
|
||||
response_derives = "Debug"
|
||||
)]
|
||||
pub struct Config;
|
|
@ -1,2 +1,5 @@
|
|||
pub mod config;
|
||||
pub mod me;
|
||||
pub mod ok;
|
||||
pub mod students_can_vote;
|
||||
pub mod teachers;
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
use graphql_client::GraphQLQuery;
|
||||
|
||||
#[derive(GraphQLQuery)]
|
||||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/queries/students_can_vote.graphql",
|
||||
response_derives = "Debug"
|
||||
)]
|
||||
pub struct StudentsCanVote;
|
|
@ -0,0 +1,9 @@
|
|||
use graphql_client::GraphQLQuery;
|
||||
|
||||
#[derive(GraphQLQuery)]
|
||||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/queries/teachers.graphql",
|
||||
response_derives = "Debug"
|
||||
)]
|
||||
pub struct Teachers;
|
|
@ -1,7 +1,6 @@
|
|||
use yew::prelude::*;
|
||||
use yew_agent::{Bridge, Bridged};
|
||||
use yew_router::prelude::*;
|
||||
use yew_router::scope_ext::RouterScopeExt;
|
||||
|
||||
use crate::agents;
|
||||
use crate::routes;
|
||||
|
@ -52,7 +51,9 @@ impl Component for LoggedIn {
|
|||
}
|
||||
|
||||
html! {
|
||||
if self.logged_in {
|
||||
{ for ctx.props().children.iter() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use std::rc::Rc;
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
use yew_side_effect::title::TitleProvider;
|
||||
|
||||
use frontend::routes;
|
||||
|
||||
|
@ -17,9 +19,14 @@ impl Component for App {
|
|||
}
|
||||
|
||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
let format_title =
|
||||
Rc::new(|m: &str| format!("{} | dergrimm.net", m)) as Rc<dyn Fn(&str) -> String>;
|
||||
|
||||
html! {
|
||||
<BrowserRouter>
|
||||
<TitleProvider default_title="Mentorenwahl | dergrimm.net" {format_title}>
|
||||
<Switch<routes::Route> render={Switch::render(routes::switch)} />
|
||||
</TitleProvider>
|
||||
</BrowserRouter>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
use graphql_client::reqwest::post_graphql;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::graphql;
|
||||
|
||||
pub enum Msg {
|
||||
DoneFetching {
|
||||
errors: Option<Vec<String>>,
|
||||
data: graphql::queries::me::me::ResponseData,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct HomeProps {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
enum State {
|
||||
Fetching,
|
||||
Done,
|
||||
}
|
||||
|
||||
pub struct Home {
|
||||
state: State,
|
||||
errors: Option<Vec<String>>,
|
||||
data: Option<graphql::queries::me::me::ResponseData>,
|
||||
}
|
||||
|
||||
impl Component for Home {
|
||||
type Message = Msg;
|
||||
type Properties = HomeProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
state: State::Fetching,
|
||||
errors: None,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::DoneFetching { errors, data } => {
|
||||
self.state = State::Done;
|
||||
self.errors = errors;
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
match self.state {
|
||||
State::Fetching => {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
"Authorization",
|
||||
reqwest::header::HeaderValue::from_str(&format!(
|
||||
"Bearer {}",
|
||||
ctx.props().token
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
ctx.link().send_future(async move {
|
||||
let response = post_graphql::<graphql::queries::me::Me, _>(
|
||||
&reqwest::Client::builder()
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.unwrap(),
|
||||
graphql::URL.as_str(),
|
||||
graphql::queries::me::me::Variables,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
log::debug!("{:?}", response.data);
|
||||
|
||||
Msg::DoneFetching {
|
||||
errors: response
|
||||
.errors
|
||||
.map(|x| x.iter().map(|e| e.message.to_owned()).collect()),
|
||||
data: response.data.unwrap(),
|
||||
}
|
||||
});
|
||||
|
||||
html! {
|
||||
<p>{ "Fetching..." }</p>
|
||||
}
|
||||
}
|
||||
State::Done => html! {
|
||||
if let Some(errors) = &self.errors {
|
||||
<p>{ "Errors:" }</p>
|
||||
<div>
|
||||
{ for errors.iter().map(|e| html! { <p style="color: red;">{ e }</p> }) }
|
||||
</div>
|
||||
} else {
|
||||
<h1>{ "Hey, !" }</h1>
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
use graphql_client::reqwest::post_graphql;
|
||||
use yew::prelude::*;
|
||||
use yew_side_effect::title::Title;
|
||||
|
||||
use crate::components;
|
||||
use crate::graphql;
|
||||
|
||||
pub mod student_home;
|
||||
pub mod student_vote;
|
||||
pub mod teacher_home;
|
||||
pub mod teacher_registration;
|
||||
|
||||
pub enum Msg {
|
||||
DoneFetching {
|
||||
errors: Option<Vec<String>>,
|
||||
me: graphql::queries::me::me::MeMe,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct HomeProps {
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
enum State {
|
||||
Fetching,
|
||||
Done,
|
||||
}
|
||||
|
||||
pub struct Home {
|
||||
state: State,
|
||||
errors: Option<Vec<String>>,
|
||||
me: Option<graphql::queries::me::me::MeMe>,
|
||||
}
|
||||
|
||||
impl Component for Home {
|
||||
type Message = Msg;
|
||||
type Properties = HomeProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
state: State::Fetching,
|
||||
errors: None,
|
||||
me: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::DoneFetching { errors, me } => {
|
||||
self.state = State::Done;
|
||||
self.errors = errors;
|
||||
self.me = Some(me);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<Title value="Home" />
|
||||
{
|
||||
match self.state {
|
||||
State::Fetching => {
|
||||
let client = graphql::client(ctx.props().token.as_ref()).unwrap();
|
||||
ctx.link().send_future(async move {
|
||||
let response = post_graphql::<graphql::queries::me::Me, _>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
graphql::queries::me::me::Variables,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Msg::DoneFetching {
|
||||
errors: components::graphql_errors::convert(response.errors),
|
||||
me: response.data.unwrap().me.unwrap(),
|
||||
}
|
||||
});
|
||||
|
||||
html! {
|
||||
<p>{ "Fetching..." }</p>
|
||||
}
|
||||
}
|
||||
State::Done => {
|
||||
if let Some(errors) = &self.errors {
|
||||
html! {
|
||||
<components::graphql_errors::GraphQLErrors errors={errors.clone()} />
|
||||
}
|
||||
} else {
|
||||
let me = self.me.as_ref().unwrap();
|
||||
|
||||
html! {
|
||||
<>
|
||||
<h1>{ format!("Hey, {}!", me.first_name) }</h1>
|
||||
<hr />
|
||||
{
|
||||
match me.role {
|
||||
graphql::queries::me::me::UserRole::Student => html! {
|
||||
<student_home::StudentHome
|
||||
token={ctx.props().token.as_ref().unwrap().to_owned()}
|
||||
voted={me.student.as_ref().unwrap().vote.is_some()}
|
||||
/>
|
||||
},
|
||||
graphql::queries::me::me::UserRole::Teacher => html! {
|
||||
<teacher_home::TeacherHome
|
||||
token={ctx.props().token.as_ref().unwrap().to_owned()}
|
||||
registered={me.teacher.is_some()}
|
||||
/>
|
||||
},
|
||||
graphql::queries::me::me::UserRole::Other(_) => html! {}
|
||||
}
|
||||
}
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
use crate::routes::home::student_vote;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct StudentHomeProps {
|
||||
pub token: String,
|
||||
pub voted: bool,
|
||||
}
|
||||
|
||||
pub struct StudentHome;
|
||||
|
||||
impl Component for StudentHome {
|
||||
type Message = ();
|
||||
type Properties = StudentHomeProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
if ctx.props().voted {
|
||||
<p>{ "Alles in Ordnung." }</p>
|
||||
} else {
|
||||
<student_vote::StudentVote token={ctx.props().token.to_owned()} />
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,220 @@
|
|||
use graphql_client::reqwest::post_graphql;
|
||||
use std::collections::HashMap;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components;
|
||||
use crate::graphql;
|
||||
|
||||
pub enum Msg {
|
||||
DoneFetchingCanVote {
|
||||
errors: Option<Vec<String>>,
|
||||
can_vote: bool,
|
||||
},
|
||||
DoneFetchingConfig {
|
||||
errors: Option<Vec<String>>,
|
||||
min: usize,
|
||||
},
|
||||
DoneFetchingTeachers {
|
||||
errors: Option<Vec<String>>,
|
||||
teachers: Vec<graphql::queries::teachers::teachers::TeachersTeachers>,
|
||||
},
|
||||
RadioSelect {
|
||||
priority: usize,
|
||||
teacher: i64,
|
||||
},
|
||||
Submit,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct StudentVoteProps {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
pub struct StudentVote {
|
||||
fetching: bool,
|
||||
can_vote: bool,
|
||||
errors: Option<Vec<String>>,
|
||||
min: usize,
|
||||
teachers: Option<Vec<graphql::queries::teachers::teachers::TeachersTeachers>>,
|
||||
votes: HashMap<usize, Option<i64>>,
|
||||
}
|
||||
|
||||
impl Component for StudentVote {
|
||||
type Message = Msg;
|
||||
type Properties = StudentVoteProps;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let client = graphql::client(Some(&ctx.props().token)).unwrap();
|
||||
ctx.link().send_future(async move {
|
||||
let can_vote_response =
|
||||
post_graphql::<graphql::queries::students_can_vote::StudentsCanVote, _>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
graphql::queries::students_can_vote::students_can_vote::Variables,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Msg::DoneFetchingCanVote {
|
||||
errors: components::graphql_errors::convert(can_vote_response.errors),
|
||||
can_vote: can_vote_response.data.unwrap().students_can_vote,
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
fetching: true,
|
||||
can_vote: false,
|
||||
errors: None,
|
||||
min: 0,
|
||||
teachers: None,
|
||||
votes: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::DoneFetchingCanVote { errors, can_vote } => {
|
||||
self.errors = errors;
|
||||
self.can_vote = can_vote;
|
||||
|
||||
let client = graphql::client(Some(&ctx.props().token)).unwrap();
|
||||
ctx.link().send_future(async move {
|
||||
let response = post_graphql::<graphql::queries::config::Config, _>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
graphql::queries::config::config::Variables,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Msg::DoneFetchingConfig {
|
||||
errors: components::graphql_errors::convert(response.errors),
|
||||
min: response
|
||||
.data
|
||||
.unwrap()
|
||||
.config
|
||||
.minimum_teacher_selection_count as usize,
|
||||
}
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
Msg::DoneFetchingConfig { errors, min } => {
|
||||
self.errors = errors;
|
||||
self.min = min;
|
||||
|
||||
let client = graphql::client(Some(&ctx.props().token)).unwrap();
|
||||
ctx.link().send_future(async move {
|
||||
let response = post_graphql::<graphql::queries::teachers::Teachers, _>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
graphql::queries::teachers::teachers::Variables,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Msg::DoneFetchingTeachers {
|
||||
errors: components::graphql_errors::convert(response.errors),
|
||||
teachers: response.data.unwrap().teachers,
|
||||
}
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
Msg::DoneFetchingTeachers { errors, teachers } => {
|
||||
self.fetching = false;
|
||||
self.errors = errors;
|
||||
self.teachers = Some(teachers);
|
||||
true
|
||||
}
|
||||
Msg::RadioSelect { priority, teacher } => {
|
||||
self.votes.insert(priority, Some(teacher));
|
||||
for (p, t) in self.votes.iter_mut() {
|
||||
if *p > priority && *t == Some(teacher) {
|
||||
*t = None;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
Msg::Submit => {
|
||||
log::debug!("{:?}", self.votes);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
if self.fetching {
|
||||
html! {
|
||||
<p>{ "Fetching..." }</p>
|
||||
}
|
||||
} else {
|
||||
let onsubmit = ctx.link().callback(|e: FocusEvent| {
|
||||
e.prevent_default();
|
||||
|
||||
Msg::Submit
|
||||
});
|
||||
let mut teachers: Vec<i64> = vec![];
|
||||
|
||||
html! {
|
||||
<>
|
||||
<h3>{ "Wähle deine Wunschmentoren aus:" }</h3>
|
||||
<form {onsubmit}>
|
||||
{ for (0..self.min).map(|i| {
|
||||
let curr_t = self.votes.get(&i);
|
||||
if let Some(te) = curr_t {
|
||||
if let Some(t) = te {
|
||||
teachers.push(*t);
|
||||
}
|
||||
}
|
||||
let name = format!("mentors_{}", i);
|
||||
|
||||
html! {
|
||||
<>
|
||||
<fieldset>
|
||||
<legend>{ format!("{}. Wahl", i + 1) }</legend>
|
||||
|
||||
{ for self.teachers.as_ref().unwrap().iter().enumerate().filter_map(|(j, t)| {
|
||||
let checked = curr_t == Some(&Some(t.id));
|
||||
|
||||
if teachers.contains(&t.id) && !checked {
|
||||
None
|
||||
} else {
|
||||
let id = format!("{}_{}", name, j);
|
||||
let teacher_id = t.id;
|
||||
let onchange = ctx.link().callback(move |_| Msg::RadioSelect { priority: i, teacher: teacher_id });
|
||||
|
||||
Some(html! {
|
||||
<div>
|
||||
<input
|
||||
type="radio"
|
||||
id={id.to_owned()}
|
||||
name={name.to_owned()}
|
||||
value={t.user.name.to_owned()}
|
||||
required=true
|
||||
{onchange}
|
||||
{checked}
|
||||
/>
|
||||
<label for={id}>{ &t.user.name }</label>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
}) }
|
||||
</fieldset>
|
||||
if i < self.min - 1 {
|
||||
<br />
|
||||
}
|
||||
</>
|
||||
}
|
||||
}) }
|
||||
|
||||
<div>
|
||||
<button type="submit">{ "Submit" }</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
use yew::prelude::*;
|
||||
use yew_agent::{Bridge, Bridged};
|
||||
|
||||
use crate::agents;
|
||||
use crate::routes::home::teacher_registration;
|
||||
|
||||
pub enum Msg {
|
||||
Registered,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct TeacherHomeProps {
|
||||
pub token: String,
|
||||
pub registered: bool,
|
||||
}
|
||||
|
||||
pub struct TeacherHome {
|
||||
registered: bool,
|
||||
_teacher_registered_producer: Box<dyn Bridge<agents::teacher_registered::EventBus>>,
|
||||
}
|
||||
|
||||
impl Component for TeacherHome {
|
||||
type Message = Msg;
|
||||
type Properties = TeacherHomeProps;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
registered: ctx.props().registered,
|
||||
_teacher_registered_producer: agents::teacher_registered::EventBus::bridge(
|
||||
ctx.link().callback(|_| Msg::Registered),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::Registered => {
|
||||
self.registered = true;
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
if self.registered {
|
||||
<p>{ "Alles in Ordnung." }</p>
|
||||
} else {
|
||||
<teacher_registration::TeacherRegistration token={ctx.props().token.to_owned()} />
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
use graphql_client::reqwest::post_graphql;
|
||||
use web_sys::HtmlInputElement;
|
||||
use yew::prelude::*;
|
||||
use yew_agent::{Dispatched, Dispatcher};
|
||||
|
||||
use crate::agents;
|
||||
use crate::components;
|
||||
use crate::graphql;
|
||||
|
||||
pub enum Msg {
|
||||
Submit,
|
||||
Registration(Option<Vec<String>>),
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct TeacherRegistrationProps {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
pub struct TeacherRegistration {
|
||||
max_students: NodeRef,
|
||||
errors: Option<Vec<String>>,
|
||||
teacher_registered_event_bus: Dispatcher<agents::teacher_registered::EventBus>,
|
||||
}
|
||||
|
||||
impl Component for TeacherRegistration {
|
||||
type Message = Msg;
|
||||
type Properties = TeacherRegistrationProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
max_students: NodeRef::default(),
|
||||
errors: None,
|
||||
teacher_registered_event_bus: agents::teacher_registered::EventBus::dispatcher(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::Submit => {
|
||||
if let Some(max_students) = self
|
||||
.max_students
|
||||
.cast::<HtmlInputElement>()
|
||||
.map(|x| x.value_as_number() as usize)
|
||||
{
|
||||
let client = graphql::client(Some(&ctx.props().token)).unwrap();
|
||||
ctx.link().send_future(async move {
|
||||
let response = post_graphql::<
|
||||
graphql::mutations::register_teacher::RegisterTeacher,
|
||||
_,
|
||||
>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
graphql::mutations::register_teacher::register_teacher::Variables {
|
||||
max_students: max_students as i64,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
log::debug!("{:?}", response.data.unwrap().register_teacher);
|
||||
|
||||
Msg::Registration(components::graphql_errors::convert(response.errors))
|
||||
});
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
Msg::Registration(errors) => {
|
||||
self.errors = errors;
|
||||
if self.errors.is_none() {
|
||||
self.teacher_registered_event_bus
|
||||
.send(agents::teacher_registered::Request::Registered);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let onsubmit = ctx.link().callback(|e: FocusEvent| {
|
||||
e.prevent_default();
|
||||
|
||||
Msg::Submit
|
||||
});
|
||||
|
||||
html! {
|
||||
<>
|
||||
<form {onsubmit}>
|
||||
<label for="max_students">
|
||||
{ "Maximale Anzahl von Schülern:" }
|
||||
<br />
|
||||
<input ref={self.max_students.clone()} type="number" id="max_students" name="max_students" min=1 />
|
||||
</label>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<button type="submit">{ "Submit" }</button>
|
||||
|
||||
<components::graphql_errors::GraphQLErrors errors={self.errors.clone()} />
|
||||
</form>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,9 @@ use graphql_client::reqwest::post_graphql;
|
|||
use web_sys::HtmlInputElement;
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
use yew_side_effect::title::Title;
|
||||
|
||||
use crate::components;
|
||||
use crate::cookie_names;
|
||||
use crate::graphql;
|
||||
use crate::routes;
|
||||
|
@ -40,7 +42,7 @@ impl Component for Login {
|
|||
if !username.is_empty() && !password.is_empty() {
|
||||
ctx.link().send_future(async {
|
||||
let response = post_graphql::<graphql::mutations::login::Login, _>(
|
||||
&reqwest::Client::new(),
|
||||
&graphql::client(None).unwrap(),
|
||||
graphql::URL.as_str(),
|
||||
graphql::mutations::login::login::Variables { username, password },
|
||||
)
|
||||
|
@ -57,11 +59,7 @@ impl Component for Login {
|
|||
)
|
||||
}
|
||||
|
||||
Msg::Login(
|
||||
response
|
||||
.errors
|
||||
.map(|x| x.iter().map(|e| e.message.to_owned()).collect()),
|
||||
)
|
||||
Msg::Login(components::graphql_errors::convert(response.errors))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -81,9 +79,7 @@ impl Component for Login {
|
|||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let link = ctx.link();
|
||||
|
||||
let onsubmit = link.callback(|e: FocusEvent| {
|
||||
let onsubmit = ctx.link().callback(|e: FocusEvent| {
|
||||
e.prevent_default();
|
||||
|
||||
Msg::Submit
|
||||
|
@ -91,11 +87,12 @@ impl Component for Login {
|
|||
|
||||
html! {
|
||||
<>
|
||||
<Title value="Login" />
|
||||
<form {onsubmit}>
|
||||
<label for="username">
|
||||
{ "Benutzername:" }
|
||||
<br />
|
||||
<input ref={self.username.clone()} type="text" id="username" />
|
||||
<input ref={self.username.clone()} type="text" id="username" name="username" />
|
||||
</label>
|
||||
|
||||
<br />
|
||||
|
@ -103,19 +100,15 @@ impl Component for Login {
|
|||
<label for="password">
|
||||
{ "Kennwort:" }
|
||||
<br />
|
||||
<input ref={self.password.clone()} type="password" id="password" />
|
||||
<input ref={self.password.clone()} type="password" id="password" name="password" />
|
||||
</label>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<button type="submit">{ "Login" }</button>
|
||||
</form>
|
||||
|
||||
if let Some(errors) = &self.errors {
|
||||
<div>
|
||||
{ for errors.iter().map(|e| html! { <p style="color: red;">{ e }</p> }) }
|
||||
</div>
|
||||
}
|
||||
<components::graphql_errors::GraphQLErrors errors={self.errors.clone()} />
|
||||
</form>
|
||||
</>
|
||||
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ pub fn switch(routes: &Route) -> Html {
|
|||
html! {
|
||||
<layouts::logged_in::LoggedIn {logged_in}>
|
||||
<layouts::main::Main {logged_in}>
|
||||
<home::Home token={token.unwrap()} />
|
||||
<home::Home {token} />
|
||||
</layouts::main::Main>
|
||||
</layouts::logged_in::LoggedIn>
|
||||
}
|
||||
|
|
|
@ -1,8 +1,22 @@
|
|||
use yew::prelude::*;
|
||||
use yew_side_effect::title::Title;
|
||||
|
||||
#[function_component(NotFound)]
|
||||
pub fn not_found() -> Html {
|
||||
pub struct NotFound;
|
||||
|
||||
impl Component for NotFound {
|
||||
type Message = ();
|
||||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<Title value="404 Not found" />
|
||||
<h1>{ "404" }</h1>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
yarn-error.log
|
||||
Dockerfile
|
||||
.gitignore
|
||||
.dockerignore
|
|
@ -1,24 +0,0 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
],
|
||||
plugins: ["svelte3", "@typescript-eslint"],
|
||||
ignorePatterns: ["*.cjs"],
|
||||
overrides: [{ files: ["*.svelte"], processor: "svelte3/svelte3" }],
|
||||
settings: {
|
||||
"svelte3/typescript": () => require("typescript"),
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaVersion: 2020,
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true,
|
||||
},
|
||||
};
|
|
@ -1,9 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
yarn-error.log
|
|
@ -1 +0,0 @@
|
|||
engine-strict=true
|
|
@ -1 +0,0 @@
|
|||
{}
|
|
@ -1,22 +0,0 @@
|
|||
FROM node:16-alpine as deps
|
||||
WORKDIR /usr/src/frontend
|
||||
COPY ./package.json ./yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
FROM node:16-alpine as builder
|
||||
WORKDIR /usr/src/frontend
|
||||
COPY --from=deps /usr/src/frontend/package.json .
|
||||
COPY --from=deps /usr/src/frontend/node_modules ./node_modules
|
||||
COPY svelte.config.js tsconfig.json ./
|
||||
COPY ./static ./static
|
||||
COPY ./src ./src
|
||||
RUN yarn build
|
||||
|
||||
FROM node:16-alpine as runner
|
||||
WORKDIR /usr/src/frontend
|
||||
COPY --from=deps /usr/src/frontend/package.json .
|
||||
COPY --from=deps /usr/src/frontend/node_modules ./node_modules
|
||||
COPY svelte.config.js .
|
||||
COPY --from=builder /usr/src/frontend/.svelte-kit ./.svelte-kit
|
||||
EXPOSE 3000
|
||||
CMD [ "yarn", "preview", "--host" ]
|
|
@ -1,40 +0,0 @@
|
|||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm init svelte@next
|
||||
|
||||
# create a new project in my-app
|
||||
npm init svelte@next my-app
|
||||
```
|
||||
|
||||
> Note: the `@next` is temporary
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment.
|
|
@ -1,37 +0,0 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "svelte-kit dev",
|
||||
"build": "svelte-kit build",
|
||||
"package": "svelte-kit package",
|
||||
"preview": "svelte-kit preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
||||
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^1.0.0-next.86",
|
||||
"@sveltejs/kit": "next",
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
||||
"@typescript-eslint/parser": "^5.10.1",
|
||||
"@urql/svelte": "^1.3.3",
|
||||
"cookie": "^0.4.2",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-svelte3": "^3.2.1",
|
||||
"graphql": "^16.3.0",
|
||||
"prettier": "^2.4.1",
|
||||
"prettier-plugin-svelte": "^2.4.0",
|
||||
"svelte": "^3.44.0",
|
||||
"svelte-check": "^2.2.6",
|
||||
"svelte-forms": "^2.2.1",
|
||||
"svelte-preprocess": "^4.9.4",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "^4.4.3",
|
||||
"typescript-cookie": "^1.0.3"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
// See https://kit.svelte.dev/docs#typescript
|
||||
// for information about these interfaces
|
||||
declare namespace App {
|
||||
interface Locals {
|
||||
user: {
|
||||
token?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface Platform {}
|
||||
|
||||
interface Session {
|
||||
user: Locals["user"];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface Stuff {}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="description" content="" />
|
||||
<link rel="icon" href="%svelte.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%svelte.head%
|
||||
</head>
|
||||
<body>
|
||||
<div>%svelte.body%</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,31 +0,0 @@
|
|||
import type { RequestEvent, ResolveOpts } from "@sveltejs/kit";
|
||||
import * as cookie from "cookie";
|
||||
|
||||
import * as cookieNames from "$lib/cookieNames";
|
||||
|
||||
export async function handle(input: {
|
||||
event: RequestEvent;
|
||||
opts?: ResolveOpts;
|
||||
resolve(event: RequestEvent, opts?: ResolveOpts): Promise<Response>;
|
||||
}): Promise<Response> {
|
||||
const header = input.event.request.headers.get("cookie");
|
||||
const cookies = header ? cookie.parse(header) : {};
|
||||
const token: string | undefined = cookies[cookieNames.TOKEN];
|
||||
|
||||
input.event.locals = {
|
||||
...input.event.locals,
|
||||
user: {
|
||||
token,
|
||||
},
|
||||
};
|
||||
|
||||
return input.resolve(input.event, input.opts);
|
||||
}
|
||||
|
||||
export async function getSession(event: RequestEvent): Promise<App.Session> {
|
||||
return {
|
||||
user: {
|
||||
token: event.locals.user.token,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<h3>STUDENT!</h3>
|
|
@ -1 +0,0 @@
|
|||
<h3>TEACHER!</h3>
|
|
@ -1,2 +0,0 @@
|
|||
const BASE = "mentorenwahl_";
|
||||
export const TOKEN = BASE + "token";
|
|
@ -1,50 +0,0 @@
|
|||
export type ID = number;
|
||||
|
||||
export interface Node {
|
||||
id: ID;
|
||||
}
|
||||
|
||||
export enum UserRole {
|
||||
STUDENT = "Student",
|
||||
TEACHER = "Teacher",
|
||||
}
|
||||
|
||||
export interface User extends Node {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
name: string;
|
||||
username: string;
|
||||
email: string;
|
||||
role: UserRole;
|
||||
admin: boolean;
|
||||
externalId: ID;
|
||||
student?: Student;
|
||||
teacher?: Teacher;
|
||||
}
|
||||
|
||||
export interface UserExternal {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface Student extends Node, UserExternal {
|
||||
vote?: Vote;
|
||||
}
|
||||
|
||||
export interface Teacher extends Node, UserExternal {
|
||||
maxStudents: number;
|
||||
}
|
||||
|
||||
export interface Vote extends Node {
|
||||
student: Student;
|
||||
}
|
||||
|
||||
export interface TeacherVote extends Node {
|
||||
teacher: Teacher;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface LoginPayload {
|
||||
user: User;
|
||||
token: string;
|
||||
bearer: string;
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { initClient } from "@urql/svelte";
|
||||
import { removeCookie } from "typescript-cookie";
|
||||
|
||||
import { session } from "$app/stores";
|
||||
import { goto } from "$app/navigation";
|
||||
import * as cookieNames from "$lib/cookieNames";
|
||||
|
||||
initClient({
|
||||
url: "/graphql",
|
||||
fetchOptions() {
|
||||
if ($session.user.token) {
|
||||
return {
|
||||
headers: {
|
||||
Authorization: `Bearer ${$session.user.token}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
||||
function logout(): void {
|
||||
$session.user.token = undefined;
|
||||
removeCookie(cookieNames.TOKEN);
|
||||
goto("/login");
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
{#if $session.user.token}
|
||||
<button on:click={logout}>Logout</button>
|
||||
{:else}
|
||||
<button><a href="/login">Login</a></button>
|
||||
{/if}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<hr />
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
|
@ -1,181 +0,0 @@
|
|||
<script lang="ts" context="module">
|
||||
export function load({ session }: { session: App.Session }) {
|
||||
if (!session.user.token) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: "/login",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
session,
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { operationStore, query, gql, mutation } from "@urql/svelte";
|
||||
// import * as svelteForms from "svelte-forms";
|
||||
// import * as validators from "svelte-forms/validators";
|
||||
|
||||
import type { User } from "$lib/graphql";
|
||||
import { UserRole } from "$lib/graphql";
|
||||
import StudentHome from "$lib/StudentHome.svelte";
|
||||
|
||||
interface Data {
|
||||
me: User;
|
||||
}
|
||||
|
||||
const meStore = operationStore<Data>(gql`
|
||||
query Me {
|
||||
me {
|
||||
firstName
|
||||
role
|
||||
admin
|
||||
student {
|
||||
id
|
||||
vote {
|
||||
id
|
||||
}
|
||||
}
|
||||
teacher {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
query(meStore);
|
||||
|
||||
// const teacherRegisterFormMaxStudents = svelteForms.field("maxStudents", 0, [
|
||||
// validators.required(),
|
||||
// validators.min(0),
|
||||
// ]);
|
||||
// const teacheRegisterForm = svelteForms.form(teacherRegisterFormMaxStudents);
|
||||
|
||||
// interface RegisterTeacherData {
|
||||
// registerTeacher: Teacher;
|
||||
// }
|
||||
|
||||
// interface RegisterTeacherVars {
|
||||
// maxStudents: number;
|
||||
// }
|
||||
|
||||
// const registerTeacherStore = operationStore<
|
||||
// RegisterTeacherData,
|
||||
// RegisterTeacherVars
|
||||
// >(gql`
|
||||
// mutation RegisterTeacher($maxStudents: Int!) {
|
||||
// registerTeacher(input: { maxStudents: $maxStudents }) {
|
||||
// id
|
||||
// }
|
||||
// }
|
||||
// `);
|
||||
|
||||
// const registerTeacherMutation = mutation(registerTeacherStore);
|
||||
|
||||
// async function registerTeacher(): Promise<void> {
|
||||
// await registerTeacherMutation({
|
||||
// maxStudents: $teacherRegisterFormMaxStudents.value,
|
||||
// });
|
||||
|
||||
// if (!$registerTeacherStore.error && $registerTeacherStore.data) {
|
||||
// location.reload();
|
||||
// }
|
||||
// }
|
||||
|
||||
// interface RegisterStudentData {
|
||||
// registerStudent: Student;
|
||||
// }
|
||||
|
||||
// const registerStudentStore = operationStore<RegisterStudentData>(gql`
|
||||
// mutation RegisterStudent() {
|
||||
// registerStudent() {
|
||||
// id
|
||||
// }
|
||||
// }
|
||||
// `);
|
||||
// const registerStudentMutation = mutation(registerStudentStore);
|
||||
|
||||
// async function registerStudent(): Promise<void> {
|
||||
// await registerStudentMutation();
|
||||
|
||||
// if (!$registerStudentStore.error && $registerStudentStore.data) {
|
||||
// location.reload();
|
||||
// }
|
||||
// }
|
||||
</script>
|
||||
|
||||
{#if $meStore.error}
|
||||
<p style="color: red;">{$meStore.error.message}</p>
|
||||
{:else if $meStore.fetching}
|
||||
<p>Laden...</p>
|
||||
{:else}
|
||||
<h1>Hey {$meStore.data.me.firstName}!</h1>
|
||||
<hr />
|
||||
|
||||
{#if $meStore.data.me.role === UserRole.TEACHER}
|
||||
<!-- {#if !$meStore.data.me.teacher}
|
||||
<p>Registriere dich jetzt als Lehrer:</p>
|
||||
<form on:submit|preventDefault={registerTeacher}>
|
||||
<label for="maxStudents">Maximale Schüler:</label>
|
||||
<br />
|
||||
<input
|
||||
type="number"
|
||||
name="maxStudents"
|
||||
min="0"
|
||||
bind:value={$teacherRegisterFormMaxStudents.value}
|
||||
/>
|
||||
<br />
|
||||
<label for="skif">SKIF:</label>
|
||||
<br />
|
||||
<input
|
||||
type="checkbox"
|
||||
name="skif"
|
||||
bind:checked={$teacherRegisterFormSkif.value}
|
||||
/>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<button type="submit" disabled={!$teacheRegisterForm.valid}
|
||||
>Registrieren</button
|
||||
>
|
||||
</form>
|
||||
{#if $registerTeacherStore.error}
|
||||
<p style="color: red;">{$registerTeacherStore.error.message}</p>
|
||||
{:else if $registerTeacherStore.fetching}
|
||||
<p>Laden...</p>
|
||||
{:else if $registerTeacherStore.data}
|
||||
<p>Registrierung erfolgreich!</p>
|
||||
{/if}
|
||||
{/if} -->
|
||||
{:else if $meStore.data.me.role === UserRole.STUDENT}
|
||||
<!-- {#if !$meStore.data.me.student}
|
||||
<p>Registriere dich jetzt als Schüler:</p>
|
||||
<form on:submit|preventDefault={registerStudent}>
|
||||
<label for="skif">SKIF:</label>
|
||||
<br />
|
||||
<input
|
||||
type="checkbox"
|
||||
name="skif"
|
||||
bind:checked={$registerStudentSkif.value}
|
||||
/>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<button type="submit" disabled={!$registerStudentForm.valid}
|
||||
>Registrieren</button
|
||||
>
|
||||
</form>
|
||||
{#if $registerStudentStore.error}
|
||||
<p style="color: red;">{$registerStudentStore.error.message}</p>
|
||||
{:else if $registerStudentStore.fetching}
|
||||
<p>Laden...</p>
|
||||
{:else if $registerStudentStore.data}
|
||||
<p>Registrierung erfolgreich!</p>
|
||||
{/if}
|
||||
{/if} -->
|
||||
<StudentHome />
|
||||
{/if}
|
||||
{/if}
|
|
@ -1,74 +0,0 @@
|
|||
<script lang="ts">
|
||||
import * as svelteForms from "svelte-forms";
|
||||
import * as validators from "svelte-forms/validators";
|
||||
import { setCookie, removeCookie } from "typescript-cookie";
|
||||
import { operationStore, mutation, gql } from "@urql/svelte";
|
||||
|
||||
import { session } from "$app/stores";
|
||||
import { goto } from "$app/navigation";
|
||||
import * as cookieNames from "$lib/cookieNames";
|
||||
import type { LoginPayload } from "$lib/graphql";
|
||||
|
||||
const username = svelteForms.field("username", undefined, [
|
||||
validators.required(),
|
||||
]);
|
||||
const password = svelteForms.field("password", undefined, [
|
||||
validators.required(),
|
||||
]);
|
||||
const loginForm = svelteForms.form(username, password);
|
||||
|
||||
interface Data {
|
||||
login: LoginPayload;
|
||||
}
|
||||
|
||||
interface Vars {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const loginStore = operationStore<Data, Vars>(gql`
|
||||
mutation Login($username: String!, $password: String!) {
|
||||
login(username: $username, password: $password) {
|
||||
token
|
||||
}
|
||||
}
|
||||
`);
|
||||
const login = mutation(loginStore);
|
||||
|
||||
async function submit(): Promise<void> {
|
||||
await login({
|
||||
username: $username.value as string,
|
||||
password: $password.value as string,
|
||||
});
|
||||
|
||||
if ($loginStore.error) {
|
||||
removeCookie(cookieNames.TOKEN);
|
||||
} else {
|
||||
$session.user.token = $loginStore.data.login.token;
|
||||
setCookie(cookieNames.TOKEN, $session.user.token, {
|
||||
expires: 1,
|
||||
});
|
||||
goto("/");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<label for="username">Benutzernane (Moodle):</label>
|
||||
<br />
|
||||
<input type="text" id="username" bind:value={$username.value} />
|
||||
<br />
|
||||
<label for="password">Kennwort:</label>
|
||||
<br />
|
||||
<input type="password" id="password" bind:value={$password.value} />
|
||||
|
||||
<br /><br />
|
||||
|
||||
<button type="submit" disabled={!$loginForm.valid}>Login</button>
|
||||
|
||||
<br />
|
||||
|
||||
{#if $loginStore.error}
|
||||
<p style="color: red;">{$loginStore.error.message}</p>
|
||||
{/if}
|
||||
</form>
|
|
@ -1,17 +0,0 @@
|
|||
import adapter from "@sveltejs/adapter-auto";
|
||||
import preprocess from "svelte-preprocess";
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: preprocess(),
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ["@urql/svelte"],
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"module": "es2020",
|
||||
"lib": ["es2020", "DOM"],
|
||||
"target": "es2020",
|
||||
/**
|
||||
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
|
||||
to enforce using \`import type\` instead of \`import\` for Types.
|
||||
*/
|
||||
"importsNotUsedAsValues": "error",
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
To have warnings/errors of the Svelte compiler at the correct position,
|
||||
enable source maps by default.
|
||||
*/
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"paths": {
|
||||
"$lib": ["src/lib"],
|
||||
"$lib/*": ["src/lib/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte", "src/lib/cookieNames.ts"]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue