diff --git a/Dashboard.php b/Dashboard.php new file mode 100755 index 0000000..9ba739c --- /dev/null +++ b/Dashboard.php @@ -0,0 +1,41 @@ + + + + + + Tableau de bord + + + + + + + + + + +
+ + + + + + + +
+ + + + +
+ +
+ + + + \ No newline at end of file diff --git a/Docs/BDD.sql b/Docs/BDD.sql new file mode 100644 index 0000000..b90db3e --- /dev/null +++ b/Docs/BDD.sql @@ -0,0 +1,84 @@ +-- MySQL Workbench Forward Engineering + +SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0; +SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0; +SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES'; + +-- ----------------------------------------------------- +-- Schema projetphp +-- ----------------------------------------------------- + +-- ----------------------------------------------------- +-- Schema projetphp +-- ----------------------------------------------------- +CREATE SCHEMA IF NOT EXISTS `projetphp` DEFAULT CHARACTER SET utf8 ; +USE `projetphp` ; + +-- ----------------------------------------------------- +-- Table `projetphp`.`Medecin` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `projetphp`.`Medecin` ( + `id` INT NOT NULL AUTO_INCREMENT, + `Civilite` CHAR(1) NOT NULL, + `Prenom` VARCHAR(45) NOT NULL, + `Nom` VARCHAR(45) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE INDEX `id_UNIQUE` (`id` ASC)) +ENGINE = InnoDB; + + +-- ----------------------------------------------------- +-- Table `projetphp`.`Patient` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `projetphp`.`Patient` ( + `Civilite` CHAR(1) NOT NULL, + `Nom` VARCHAR(45) CHARACTER SET 'big5' NOT NULL, + `Prenom` VARCHAR(45) NOT NULL, + `Adresse` VARCHAR(100) NOT NULL, + `Ville` VARCHAR(50) NOT NULL, + `CodePostal` SMALLINT(4) NOT NULL, + `DateNaissance` DATE NOT NULL, + `LieuNaissance` VARCHAR(50) NOT NULL, + `NumSecuriteSociale` INT(15) NOT NULL, + `Id` INT NOT NULL AUTO_INCREMENT, + `MedecinTraitant` INT NULL, + UNIQUE INDEX `NumSecuriteSociale_UNIQUE` (`NumSecuriteSociale` ASC), + PRIMARY KEY (`Id`), + UNIQUE INDEX `Id_UNIQUE` (`Id` ASC), + INDEX `fk_Patient_Medecin_idx` (`MedecinTraitant` ASC), + CONSTRAINT `fk_Patient_Medecin` + FOREIGN KEY (`MedecinTraitant`) + REFERENCES `projetphp`.`Medecin` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB; + + +-- ----------------------------------------------------- +-- Table `projetphp`.`RDV` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `projetphp`.`RDV` ( + `id` INT NOT NULL, + `DateRDV` TIMESTAMP NULL, + `Duree` TIME NULL, + `Patient_Id` INT NOT NULL, + `Medecin_id` INT NOT NULL, + PRIMARY KEY (`id`), + INDEX `fk_RDV_Patient1_idx` (`Patient_Id` ASC), + INDEX `fk_RDV_Medecin1_idx` (`Medecin_id` ASC), + CONSTRAINT `fk_RDV_Patient1` + FOREIGN KEY (`Patient_Id`) + REFERENCES `projetphp`.`Patient` (`Id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION, + CONSTRAINT `fk_RDV_Medecin1` + FOREIGN KEY (`Medecin_id`) + REFERENCES `projetphp`.`Medecin` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB; + + +SET SQL_MODE=@OLD_SQL_MODE; +SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS; +SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS; diff --git a/Docs/Model.mwb b/Docs/Model.mwb new file mode 100644 index 0000000..d746949 Binary files /dev/null and b/Docs/Model.mwb differ diff --git a/autoloader.php b/autoloader.php index dbfecc3..da6f80f 100755 --- a/autoloader.php +++ b/autoloader.php @@ -4,18 +4,27 @@ * fonction d'autoloading : prend en paramètre le nom de la classe et s'occupe d'inclure les fichiers correspondant aux classes */ +//pour l'inclusion dans le dossier src +$GLOBALS['managers_dir'] = dirname(__FILE__).DIRECTORY_SEPARATOR.'src'; + function autoloader($class) { - //si on charge le StaticRepo - if(strpos($class, 'StaticRepo') !== FALSE){ - require_once dirname(__FILE__).DIRECTORY_SEPARATOR.'repositories'.DIRECTORY_SEPARATOR.$class . '.php'; - } - //si on charge un Repo - elseif(strpos($class, 'Repo') !== FALSE){ - require_once dirname(__FILE__).DIRECTORY_SEPARATOR.'repositories'.DIRECTORY_SEPARATOR.'repos'.DIRECTORY_SEPARATOR.$class . '.php'; + //si on charge le StaticRepo + if(strpos($class, 'StaticRepo') !== FALSE){ + require_once dirname(__FILE__).DIRECTORY_SEPARATOR.'repositories'.DIRECTORY_SEPARATOR.$class . '.php'; + } + //si on charge un Repo + elseif(strpos($class, 'Repo') !== FALSE){ + require_once dirname(__FILE__).DIRECTORY_SEPARATOR.'repositories'.DIRECTORY_SEPARATOR.'repos'.DIRECTORY_SEPARATOR.$class . '.php'; - //cas particuliers pas identifiable par nom de classe - } + //cas particuliers pas identifiable par nom de classe + }else{ + //si on charge un manager + if(is_file(dirname(__FILE__).DIRECTORY_SEPARATOR.'src'.DIRECTORY_SEPARATOR.$class . '.php')){ + require_once dirname(__FILE__).DIRECTORY_SEPARATOR.'src'.DIRECTORY_SEPARATOR.$class . '.php'; + + } + } } //enregistrememnt de la fonction tout en bas de la pile pour ne pas casser l'autoloader de phpUnit diff --git a/css/global.css b/css/global.css new file mode 100755 index 0000000..03ba01c --- /dev/null +++ b/css/global.css @@ -0,0 +1,185 @@ +*{ margin: 0; padding: 0; } + +a{ + /* remove-a default properties */ + text-decoration: none; + color: inherit; +} + + + +body{ + /* position */ + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + /* background */ + background-color: #e8e8e8; + + /* foreground */ + font-size: 14px; + font-family: 'Open Sans', 'sans serif'; + color: #000; +} + + + +/* WRAPPER DE LA PAGE */ +#WRAPPER{ + /* position */ + display: block; + position: absolute; + top: 1em; + left: calc( 50% - 512px - 1em ); + width: 1024px; + min-height: 50%; + margin: 2em; + + /* border */ + border: 1px solid #e2e2e2; + box-shadow: 0 0 5px #e2e2e2; + + /* background */ + background-color: #fff; +} + + +/* MENU DE LA PAGE */ +#WRAPPER > #MENU{ + /* position */ + display: block; + position: absolute; + top: 0; + left: 0; + width: 225px; + height: 100%; + + /* border */ + border-right: 1px solid #e2e2e2; + border-bottom: 1px solid #e2e2e2; + + /* background */ + background-color: #fafafa; +} + + +/* LOGO DANS LE MENU */ +#WRAPPER > #MENU > #ICON{ + /* position */ + position: relative; + width: 100%; + height: 100px; + + /* background */ + background-color: red; +} + +/* LES LIENS DU MENU */ +#WRAPPER > #MENU > a{ + /* position */ + display: inline-block; + position: relative; + width: calc( 100% - 3em ); + padding-left: 3em; + padding-bottom: 1px; + + /* background */ + background-color: transparent; + + /* foreground */ + line-height: 2.5em; + + /* extra */ + cursor: pointer; +} + +/* :hover / .active */ +#WRAPPER > #MENU > a:hover, +#WRAPPER > #MENU > a.active{ + background-color: #efefef !important; + color: #818181; +} + + +/* @first/@last */ +#WRAPPER > #MENU > a:nth-child(2){ margin-top: 1em; } +#WRAPPER > #MENU > a:last-child{ margin-bottom: 1em; } + + +/* icones associées aux liens */ +#WRAPPER > #MENU > a#consultations{ + background: url(../src/consultation.svg) left 1em center no-repeat; + background-size: auto 1.5em; +} + +#WRAPPER > #MENU > a#doctor{ + background: url(../src/doctor.svg) left 1em center no-repeat; + background-size: auto 1.5em; +} + +#WRAPPER > #MENU > a#dashboard{ + background: url(../src/dashboard.svg) left 1em center no-repeat; + background-size: auto 1.5em; +} + + + + +/*************/ +/* CONTAINER */ +/*************/ +#WRAPPER > #CONTAINER{ + /* position */ + display: block; + position: absolute; + top: 0; + left: 225px; + width: calc( 100% - 225px ); + height: 100%; +} + + +/* FIL D'ARIANE */ +#WRAPPER > #CONTAINER > #BREADCRUMB{ + /* position */ + display: inline-block; + position: relative; + width: calc( 100% - 2*2em - 2*.6em ); + margin: 2em; + padding: .6em; + + /* background */ + background-color: #eee; + + /* foreground */ + color: #333; +} + +#WRAPPER > #CONTAINER > #BREADCRUMB a:hover{ + text-decoration: underline; +} + +/* chevrons */ +#WRAPPER > #CONTAINER > #BREADCRUMB a:before{ + content: ''; + /* position */ + display: inline-block; + position: relative; + width: 1em; + height: .7em; + + background: url(../src/right-arrow.svg) center center no-repeat; + background-size: 40% auto; +} + +/* premier: maison au lien de chevron */ +#WRAPPER > #CONTAINER > #BREADCRUMB a:first-child:before{ + margin-right: 1em; + height: 1em; + background-image: url(../src/home.svg); + background-size: auto 100%; +} diff --git a/css/login.css b/css/login.css new file mode 100755 index 0000000..55400fe --- /dev/null +++ b/css/login.css @@ -0,0 +1,128 @@ +/*******************************/ +/*** RECTIFICATIONS GLOBALES ***/ +/*******************************/ +*{ margin: 0; padding: 0; } + +body{ + /* position */ + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + /* background */ + background-color: #233342; + + /* foreground */ + font: 16px 'Open Sans'; + color: #fff; +} + + + +/*******************/ +/*** FORMULAIRES ***/ +/*******************/ +form{ + /* position */ + display: block; + position: absolute; + left: calc( 50% - 20em/2 ); + width: 20em; + height: auto; + margin: 2em; + padding: 2em; + + /* border */ + border-radius: 5px; + box-shadow: inset 0 0 10px #0a0f14; + + /* background */ + background-color: #151f28; +} + + + + +/************************/ +/*** CHAMPS DE SAISIE ***/ +/************************/ +input{ + /* position */ + display: inline-block; + padding: 1em 2em; + margin: 1em; + width: calc( 100% - 2*2em - 5px - 2*1em ); + + /* border */ + border-radius: 3px 0 0 3px; + border: 0; + border-right: 5px solid #aaa; + + /* background */ + background-color: #ddd; + + /* animation */ + transition: all .2s ease-in-out; + -moz-transition: all .2s ease-in-out; + -webkit-transition: all .2s ease-in-out; + -ms-transition: all .2s ease-in-out; + -o-transition: all .2s ease-in-out; +} + +/* @hover */ +input:focus { background-color: #fff; } +input:nth-child(4n+0):focus{ border-right-color: #f45b2a; } +input:nth-child(4n+1):focus{ border-right-color: #1b84db; } +input:nth-child(4n+2):focus{ border-right-color: #f4b92a; } +input:nth-child(4n+3):focus{ border-right-color: #b62af4; } + + +/*****************************/ +/*** BOUTONS DE VALIDATION ***/ +/*****************************/ +input[type=submit], +input[type=button]{ + /* position */ + width: auto; + float: right; + + /* foreground */ + font-weight: bold; +} + +input[type=submit]:hover, +input[type=button]:hover{ + + /* border */ + border-right-color: #1b84db; + + /* background */ + background-color: #fff; + + /* foreground */ + color: #1b84db; + + /* extra */ + cursor: pointer; +} + + +/****************/ +/*** MESSAGES ***/ +/****************/ +.error{ + font-size: .8em; + padding: 1em 1em; + color: #f14973; + text-shadow: 1px 1px 0 #000; +} + +.success{ + font-size: .8em; + padding: 1em 1em; + color: #49f16b; + text-shadow: 1px 1px 0 #000; +} \ No newline at end of file diff --git a/index.php b/index.php new file mode 100755 index 0000000..c07c582 --- /dev/null +++ b/index.php @@ -0,0 +1,65 @@ + + +authentification($_POST['username'],$_POST['password']); +} + +if(Authentification::checkUser(0)){ + header("Location: http://".$_SERVER['HTTP_HOST']."/Dashboard.php"); + die(); +}; +?> + + + + Tests php + + + + + + + + + + + + "; + + /* AFFICHAGE D'ERREURS */ + if( $postVariablesAreSet ){ // si formulaire soumis + if( !$postVariablesNEmpty ) + echo 'Certains champs requis sont vides.'; + elseif( !$usernameCheck ) + echo 'Nom d\'utilisateur incorrect. (3 car. min)'; + elseif( !$mailCheck ) + echo 'Adresse mail incorrecte.'; + elseif( !$passwordCheck ) + echo 'Mot de passe incorrect. (8 car. min)'; + elseif( connected($user) ) + echo 'Vous êtes connectés.'; + } + + echo ""; + echo ""; + echo ""; + echo ""; + + ?> + + + + \ No newline at end of file diff --git a/src/Authentification.php b/src/Authentification.php new file mode 100755 index 0000000..a81fd19 --- /dev/null +++ b/src/Authentification.php @@ -0,0 +1,87 @@ +users = json_decode(file_get_contents($GLOBALS['managers_dir'].DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'users.json'),true); + } + + /** + * méthode d'authentification, utilise param['identifiant'] et param['mdp'] et les comparent à + * nos utilisateurs enregistrés puis créer une session securisée par token + * @param array $param contiens les infomations de connection + * @return json json contenant le résultat de l'authentification (true si authentification correcte, sinon non) + */ + public function authentification($user,$mdp){ + foreach($this->users as $utilisateur=>$infos){ + if($utilisateur == $user and $infos['password'] == $mdp){ + $this->createSecureSession($user,$infos['role']); + return true; + } + } + return false; + } + + /** + * déconnecte l'utilisateur en détruisant la session et le cookie + * @return json renvoie true, il n'y aucune raison que ça foire + */ + public function deconnection(){ + $this->destroySecureSession(); + Response::quickResponse(200,json_encode(['result' => true])); + } + + /** + * créer une session sécurisé , protégé du vol de session par identification de l'utilisateur par navigateur/ip/cookie + * @param String $user nom d'utilisateur + * @param String $role role de l'utilisateur (0=administrateur, 1= prof, 2=scolarité,3=élève) + * @return void + */ + private function createSecureSession($user,$role){ + $id = uniqid(); + $_SESSION['id'] = $id; + $_SESSION['token'] = sha1($_SERVER['HTTP_USER_AGENT'].$_SERVER['REMOTE_ADDR'].$id); + setcookie('UserId',$id,time()+10*60,'/'); + + $_SESSION['user'] = $user; + $_SESSION['role'] = $role; + + } + + /** + * Détruit une session + * @return void + */ + private function destroySecureSession(){ + session_destroy(); + setcookie('token',time()-1); + } + + /** + * Vérifie qu'un utilisateur donné a les droits demandés (passés en paramètres) + * @param int $role role minimum + * @param boolean $strict si strict vaut true, seul les utilisateurs avec le role précis seront acceptés, sinon tout les utilisateurs + * avec un role superieur le seront + * @return boolean + */ + public static function checkUser($role, $strict=false){ + if(isset($_SESSION['token'])){ + foreach($_SESSION['role'] as $roleUser){ + if(($strict and $roleUser == $role) or (!$strict and $roleUser<= $role)){ + if($_SESSION['token'] == sha1($_SERVER['HTTP_USER_AGENT'].$_SERVER['REMOTE_ADDR'].$_SESSION['id'])){ + setcookie('UserId',$_COOKIE['UserId'],time()+10*60,'/'); + return true; + }; + } + } + } + return false; + } + + public static function getCurrentUser(){ + return $_SESSION['user']; + } +} +?> diff --git a/src/Response.php b/src/Response.php new file mode 100644 index 0000000..23e3971 --- /dev/null +++ b/src/Response.php @@ -0,0 +1,202 @@ + '100 Continue', + 101 => '101 Switching Protocols', + //Successful 2xx + 200 => '200 OK', + 201 => '201 Created', + 202 => '202 Accepted', + 203 => '203 Non-Authoritative Information', + 204 => '204 No Content', + 205 => '205 Reset Content', + 206 => '206 Partial Content', + 226 => '226 IM Used', + //Redirection 3xx + 300 => '300 Multiple Choices', + 301 => '301 Moved Permanently', + 302 => '302 Found', + 303 => '303 See Other', + 304 => '304 Not Modified', + 305 => '305 Use Proxy', + 306 => '306 (Unused)', + 307 => '307 Temporary Redirect', + //Client Error 4xx + 400 => '400 Bad Request', + 401 => '401 Unauthorized', + 402 => '402 Payment Required', + 403 => '403 Forbidden', + 404 => '404 Not Found', + 405 => '405 Method Not Allowed', + 406 => '406 Not Acceptable', + 407 => '407 Proxy Authentication Required', + 408 => '408 Request Timeout', + 409 => '409 Conflict', + 410 => '410 Gone', + 411 => '411 Length Required', + 412 => '412 Precondition Failed', + 413 => '413 Request Entity Too Large', + 414 => '414 Request-URI Too Long', + 415 => '415 Unsupported Media Type', + 416 => '416 Requested Range Not Satisfiable', + 417 => '417 Expectation Failed', + 418 => '418 I\'m a teapot', + 422 => '422 Unprocessable Entity', + 423 => '423 Locked', + 426 => '426 Upgrade Required', + 428 => '428 Precondition Required', + 429 => '429 Too Many Requests', + 431 => '431 Request Header Fields Too Large', + //Server Error 5xx + 500 => '500 Internal Server Error', + 501 => '501 Not Implemented', + 502 => '502 Bad Gateway', + 503 => '503 Service Unavailable', + 504 => '504 Gateway Timeout', + 505 => '505 HTTP Version Not Supported', + 506 => '506 Variant Also Negotiates', + 510 => '510 Not Extended', + 511 => '511 Network Authentication Required' + ); + + /** + * Constructeur de la Response + * @param int $status status HTTP de la réponse (404,200,500, etc) + * @param bool|false $stream Si la réponse est un stream (avtive/désactive les méthodes send/stream() + * @param string $type type HTTP des données de retour + * @param bool|true $clearBuffer si activé, vide le buffer avant chaque envoi de donnée (a pour effet de ne pas afficher les echo/printf) + */ + public function __construct($status = 200,$stream = false,$type = 'application/json', $clearBuffer = false) + { + $this->status = $status; + array_push($this->headers,['Content-Type',$type]); + + $this->config['clearBuffer'] = $clearBuffer; + $this->config['stream'] = $stream; + } + + /** Ajoute du contenu a la réponse qui sera envoyé (par stream() ou par send() ) + * @param $content contenu a ajouter a la réponse + */ + public function write($content){ + $this->response .= $content; + } + + /** Envoie une partie de réponse au client (doit être récupéré en ajax, aucun intéret sinon), chaque bloc de donéne envoyé est séparé par + * un délimiteur ("//Block//" par défaut).ATTENTION: stream() vide la réponse (si on write() puis stream(), la réponse qu'il restera dans l'objet sera vide) + * @param string $content contenu a envoyer (optionnel car on peut utiliser la méthode write pour le faire) + * @throws Exception si la réponse n'est pas un stream + */ + public function stream($content="",$delimiter = "//Block//"){ + //vérification que la réponse est un stream + if(!$this->config['stream']){ + throw new Exception("Stream d'une réponse synchrone"); + } + //si les headers ne sont pas encore envoyés, on le fait + if(!headers_sent()){ + $this->sendHeader(); + } + //si demandé, on clear le buffer avant d'envoyer + if($this->config['clearBuffer']){ + ob_end_clean(); + if($GLOBALS['compression']){ + ob_start("ob_gzhandler"); + }else{ + ob_start(); + } + } + //on envoi le contenu de response et la variable content + if($this->response!=""){ + echo $delimiter.$this->response; + }if($content != ""){ + echo $delimiter.$content; + } + ob_flush();flush(); + $this->response = ''; + } + + /** + * Envoi les headers de la réponse (status et ceux potentiellement défnini par l'utilisateur) + */ + public function sendHeader(){ + //envoie le status de la requete (petit trick suivant l'architecture de PHP) + if (strpos(PHP_SAPI, 'cgi') === 0) { + header(sprintf('Status: %s', $this->Messages[$this->status])); + } else { + header(sprintf('HTTP/1.1 %s', $this->Messages[$this->status])); + } + //les autres headers + foreach($this->headers as $header){ + header(sprintf('%s: %s',$header[0],$header[1])); + } + } + + /** + * Défini un header qui sera envoyé + * @param $header Nom du header + * @param $value Valeur du header + */ + public function setHeader($header,$value){ + array_push($this->headers,[$header,$value]); + } + + /** Envoi la réponse et ferme la communication + * @throws Exception si la réponse est un stream + */ + public function send(){ + //vérification que la réponse n'est pas un stream + if($this->config['stream']){ + throw new Exception("Envoi synchrone d'une réponse stream"); + } + //si les headers ne sont pas encore envoyés, on le fait + if(!headers_sent()){ + $this->sendHeader(); + } + //si demandé, on clear le buffer avant d'envoyer + if($this->config['clearBuffer']){ + ob_end_clean(); + if($GLOBALS['compression']){ + ob_start("ob_gzhandler"); + }else{ + ob_start(); + } + } + //envoi de la réponse + echo $this->response; + //fermeture de la communication + header('Connection: close'); + header('Content-Length: '.ob_get_length()); + ob_end_flush(); + ob_flush(); + flush(); + //permet au reste du script de s'executer même si la réponse a été envoyé et que l'utilisateur interromp le script (changement de page, etc...) + ignore_user_abort(true); + } + + /** + * @param int $status status HTTP de la réponse (404,200,500, etc) + * @param $content + * @param string $type + */ + public static function quickResponse($status,$content,$type = 'application/json'){ + $response = new Response($status,false,$type); + $response->write($content); + $response->send(); + } +} diff --git a/src/config/users.json b/src/config/users.json new file mode 100755 index 0000000..310a83a --- /dev/null +++ b/src/config/users.json @@ -0,0 +1,6 @@ +{ + "secretaire": { + "password":"thecakeisalie", + "role":[0] + } +} diff --git a/src/consultation.svg b/src/consultation.svg new file mode 100755 index 0000000..a45e261 --- /dev/null +++ b/src/consultation.svg @@ -0,0 +1,84 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/src/dashboard.svg b/src/dashboard.svg new file mode 100755 index 0000000..9b750e3 --- /dev/null +++ b/src/dashboard.svg @@ -0,0 +1,146 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/home.svg b/src/home.svg new file mode 100755 index 0000000..fa67b05 --- /dev/null +++ b/src/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/right-arrow.svg b/src/right-arrow.svg new file mode 100755 index 0000000..04e77dd --- /dev/null +++ b/src/right-arrow.svg @@ -0,0 +1,71 @@ + + + + + + + + + + image/svg+xml + + + + + + + > + +