✨ Hello world
This commit is contained in:
7
resources/js/app.js
vendored
Normal file
7
resources/js/app.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
require('./bootstrap');
|
||||
|
||||
import Alpine from 'alpinejs';
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.start();
|
15
resources/js/app.ts
Normal file
15
resources/js/app.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* First we will load all of this project's JavaScript dependencies which
|
||||
* includes React and other helpers. It's a great starting point while
|
||||
* building robust, powerful web applications using React + Laravel.
|
||||
*/
|
||||
|
||||
require('./bootstrap');
|
||||
|
||||
/**
|
||||
* Next, we will create a fresh React component instance and attach it to
|
||||
* the page. Then, you may begin adding components to this application
|
||||
* or customize the JavaScript scaffolding to fit your unique needs.
|
||||
*/
|
||||
|
||||
require('./components/pages/App.tsx');
|
32
resources/js/bootstrap.js
vendored
Normal file
32
resources/js/bootstrap.js
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
window._ = require('lodash');
|
||||
|
||||
try {
|
||||
require('bootstrap');
|
||||
} catch (e) {}
|
||||
|
||||
/**
|
||||
* We'll load the axios HTTP library which allows us to easily issue requests
|
||||
* to our Laravel back-end. This library automatically handles sending the
|
||||
* CSRF token as a header based on the value of the "XSRF" token cookie.
|
||||
*/
|
||||
|
||||
window.axios = require('axios');
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
||||
/**
|
||||
* Echo exposes an expressive API for subscribing to channels and listening
|
||||
* for events that are broadcast by Laravel. Echo and event broadcasting
|
||||
* allows your team to easily build robust real-time web applications.
|
||||
*/
|
||||
|
||||
// import Echo from 'laravel-echo';
|
||||
|
||||
// window.Pusher = require('pusher-js');
|
||||
|
||||
// window.Echo = new Echo({
|
||||
// broadcaster: 'pusher',
|
||||
// key: process.env.MIX_PUSHER_APP_KEY,
|
||||
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
|
||||
// forceTLS: true
|
||||
// });
|
59
resources/js/components/pages/App.tsx
Normal file
59
resources/js/components/pages/App.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import ReactDOM from 'react-dom';
|
||||
import PageForm from "./Form";
|
||||
import Pages from "./List";
|
||||
import Prompt from "./Prompt";
|
||||
import {useState} from "react";
|
||||
import * as React from 'react';
|
||||
import {Divider, Paper} from "@mui/material";
|
||||
|
||||
interface List {
|
||||
id: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
const app = document.getElementById('app');
|
||||
|
||||
let sessionPassphrase = sessionStorage.getItem("key");
|
||||
let pages: List[] = [];
|
||||
let getPageContentUrl = "";
|
||||
let postUrl = "";
|
||||
let removeUrl = "";
|
||||
let csrf = "";
|
||||
if (app) {
|
||||
getPageContentUrl = "" + app.getAttribute('data-url');
|
||||
pages = JSON.parse("" + app.getAttribute('data-list'));
|
||||
postUrl = "" + app.getAttribute('data-post');
|
||||
removeUrl = "" + app.getAttribute('data-remove');
|
||||
csrf = "" + app.getAttribute('data-csrf');
|
||||
ReactDOM.render(<App/>, app);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [listPages, setListPages] = useState(pages);
|
||||
const [passphrase, setPassphrase] = useState(sessionPassphrase);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-md-8">
|
||||
{/*<div className="card">*/}
|
||||
{/*<Paper elevation={3}>*/}
|
||||
<Prompt open={passphrase === null} setOpen={setPassphrase}/>
|
||||
<Pages
|
||||
pages={listPages}
|
||||
url={getPageContentUrl}
|
||||
passphrase={passphrase}
|
||||
setPassphrase={setPassphrase}
|
||||
csrf={csrf}
|
||||
removeUrl={removeUrl}/>
|
||||
<Divider/>
|
||||
<PageForm setListPages={setListPages} csrf={csrf} url={postUrl} passphrase={passphrase}/>
|
||||
{/*</Paper>*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
110
resources/js/components/pages/Form.tsx
Normal file
110
resources/js/components/pages/Form.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import * as React from 'react';
|
||||
import {EncryptStorage} from 'storage-encryption';
|
||||
import {Button, Stack, TextField} from "@mui/material";
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
let encryptStorage = new EncryptStorage('test'); // TODO la clef doit venir de l'utilisateur
|
||||
|
||||
export default function PageForm({setListPages, csrf, url, passphrase}) {
|
||||
const isPassphraseSet = passphrase !== null;
|
||||
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
encryptStorage = new EncryptStorage(passphrase);
|
||||
let HTMLForm : HTMLFormElement = event.currentTarget;
|
||||
let decryptedFormData = new FormData(HTMLForm);
|
||||
let encryptedFormData = new FormData();
|
||||
for (let [key, value] of decryptedFormData.entries()) {
|
||||
encryptStorage.encrypt('uuid'+key, value);
|
||||
let newEncryptedString = localStorage.getItem('uuid'+key);
|
||||
if (newEncryptedString) {
|
||||
encryptedFormData.append(key, newEncryptedString);
|
||||
}
|
||||
}
|
||||
|
||||
encryptedFormData.append('_token', csrf);
|
||||
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: encryptedFormData
|
||||
});
|
||||
|
||||
const json = await response.json();
|
||||
const uuid = json.uuid;
|
||||
for (let key of decryptedFormData.keys()) {
|
||||
let newEncryptedString = localStorage.getItem('uuid'+key);
|
||||
localStorage.setItem(uuid+key, ""+newEncryptedString);
|
||||
localStorage.removeItem("uuid"+key);
|
||||
}
|
||||
|
||||
if (json.success) {
|
||||
HTMLForm.reset();
|
||||
setListPages(previousList => [
|
||||
...previousList,
|
||||
{
|
||||
id: uuid,
|
||||
date: json.date,
|
||||
title: decryptedFormData.get("title"),
|
||||
content: decryptedFormData.get("text"),
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
if (isPassphraseSet) {
|
||||
return (
|
||||
/* <Stack
|
||||
component="form"
|
||||
sx={{
|
||||
width: '25ch',
|
||||
}}
|
||||
spacing={2}
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
>
|
||||
<TextField
|
||||
label="Titre"
|
||||
id="title"
|
||||
name="title"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
<MDEditor
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
/>
|
||||
<MDEditor.Markdown source={value} />
|
||||
<TextField
|
||||
label="Texte"
|
||||
name="content"
|
||||
id="filled-hidden-label-normal"
|
||||
defaultValue="Normal"
|
||||
variant="outlined"
|
||||
multiline
|
||||
maxRows={15}
|
||||
/>
|
||||
<Button variant="contained" component="span" onClick={onSubmit}>
|
||||
Enregistrer
|
||||
</Button>
|
||||
</Stack>*/
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-md-8">
|
||||
<div className="card">
|
||||
<form action={url} id="postPage" method="post" onSubmit={onSubmit}>
|
||||
<label htmlFor="title">Titre:</label>
|
||||
<input id="title" name="title"/>
|
||||
<hr/>
|
||||
<label htmlFor="text">Texte:</label>
|
||||
<textarea id="text" name="text"/>
|
||||
<hr/>
|
||||
<input type="submit" value="Enregistrer"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (<div></div>);
|
||||
}
|
||||
|
||||
|
29
resources/js/components/pages/List.tsx
Normal file
29
resources/js/components/pages/List.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import {Grid, List, Pagination} from '@mui/material';
|
||||
import * as React from 'react';
|
||||
import Page from "./Page";
|
||||
|
||||
interface List {
|
||||
id: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export default function Pages({pages, url, removeUrl, csrf, passphrase, setPassphrase}) {
|
||||
const isPassphraseSet = passphrase !== null;
|
||||
|
||||
let listPages = pages.map(page =>
|
||||
<Page page={page} url={url} setPassphrase={setPassphrase} passphrase={passphrase} csrf={csrf} removeUrl={removeUrl} />
|
||||
)
|
||||
|
||||
if (isPassphraseSet) {
|
||||
return (
|
||||
<div>
|
||||
<Grid container rowSpacing={1} columnSpacing={1}>
|
||||
{listPages}
|
||||
</Grid>
|
||||
<Pagination count={10} color="primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (<div></div>);
|
||||
}
|
123
resources/js/components/pages/Page.tsx
Normal file
123
resources/js/components/pages/Page.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Card, CardActions,
|
||||
CardContent,
|
||||
CardHeader, Collapse,
|
||||
Grid, IconButton, IconButtonProps,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import * as React from 'react';
|
||||
import {EncryptStorage} from 'storage-encryption';
|
||||
import {Delete} from "@mui/icons-material";
|
||||
import {unmountComponentAtNode} from "react-dom";
|
||||
|
||||
interface Page {
|
||||
date: string;
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface ExpandMoreProps extends IconButtonProps {
|
||||
expand: boolean;
|
||||
}
|
||||
|
||||
const ExpandMore = styled((props: ExpandMoreProps) => {
|
||||
const { expand, ...other } = props;
|
||||
return <IconButton {...other} />;
|
||||
})(({ theme, expand }) => ({
|
||||
transform: !expand ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||
marginLeft: 'auto',
|
||||
transition: theme.transitions.create('transform', {
|
||||
duration: theme.transitions.duration.shortest,
|
||||
}),
|
||||
}));
|
||||
|
||||
export default function Page({page, url, removeUrl, csrf, passphrase, setPassphrase}) {
|
||||
const [more, setMore] = React.useState(false);
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
const handleMoreClick = () => { setMore(!more); };
|
||||
const handleExpandClick = () => { onLoad().then(r => setExpanded(!expanded)); };
|
||||
const remove = async () => {
|
||||
const formData = new FormData();
|
||||
formData.set('_token', csrf);
|
||||
formData.set('_method', 'DELETE');
|
||||
let response = await fetch(removeUrl.replace("replace_me", page.id), {
|
||||
method: 'POST', body: formData
|
||||
});
|
||||
|
||||
const json = await response.json();
|
||||
unmountComponentAtNode(document.getElementById(page.id));
|
||||
};
|
||||
|
||||
let encryptStorage = new EncryptStorage(passphrase);
|
||||
let title, content = "";
|
||||
let alert_popup:JSX.Element|null = null;
|
||||
let setTitle, setContent: React.Dispatch<any>|null = null;
|
||||
let onLoad = async () => {};
|
||||
try {
|
||||
[content, setContent] = React.useState(encryptStorage.decrypt(page.id + "text"));
|
||||
[title, setTitle] = React.useState(encryptStorage.decrypt(page.id + "title"));
|
||||
onLoad = async () => {
|
||||
if (localStorage.getItem(page.id + "text") === null) {
|
||||
let response = await fetch(url.replace("replace_me", page.id));
|
||||
|
||||
let json = await response.json();
|
||||
localStorage.setItem(page.id + "title", json.metadata.title);
|
||||
localStorage.setItem(page.id + "text", json.content);
|
||||
}
|
||||
setTitle(encryptStorage.decrypt(page.id + "title"));
|
||||
// @ts-ignore
|
||||
setContent(encryptStorage.decrypt(page.id + "text"));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setPassphrase(null);
|
||||
alert_popup = <Alert severity="error">
|
||||
<AlertTitle>Erreur</AlertTitle>
|
||||
Vos pages ne peuvent pas être décodées — <strong>Réindiquez votre clef!</strong>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid item xs={12} sm={6} md={6} id={page.id}>
|
||||
{alert_popup}
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<IconButton aria-label="settings">
|
||||
<MoreVertIcon onClick={handleMoreClick} />
|
||||
</IconButton>
|
||||
}
|
||||
title={title}
|
||||
subheader={page.date}
|
||||
/>
|
||||
<Collapse in={more} timeout="auto" unmountOnExit>
|
||||
<IconButton aria-label="remove">
|
||||
<Delete onClick={remove} />
|
||||
</IconButton>
|
||||
</Collapse>
|
||||
<CardActions disableSpacing>
|
||||
<ExpandMore
|
||||
expand={expanded}
|
||||
onClick={handleExpandClick}
|
||||
aria-expanded={expanded}
|
||||
aria-label="show more"
|
||||
>
|
||||
<ExpandMoreIcon />
|
||||
</ExpandMore>
|
||||
</CardActions>
|
||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||
<CardContent>
|
||||
<Typography paragraph>
|
||||
{content}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Collapse>
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
}
|
43
resources/js/components/pages/Prompt.tsx
Normal file
43
resources/js/components/pages/Prompt.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import {Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField} from "@mui/material";
|
||||
import * as React from 'react';
|
||||
|
||||
export default function Prompt({open, setOpen}) {
|
||||
|
||||
const handleEnterClose = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleClose();
|
||||
}
|
||||
}
|
||||
const handleClose = () => {
|
||||
// @ts-ignore
|
||||
const passphrase = ""+document.getElementById("passphrase").value;
|
||||
sessionStorage.setItem("key", passphrase);
|
||||
setOpen(passphrase);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogTitle>Hey ! Si c'est bien toi, donne moi la clef !</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Avant de pouvoir lire ou écrire, il faut ouvrir ton carnet avec la clef.
|
||||
</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="passphrase"
|
||||
label="Passphrase"
|
||||
type="password"
|
||||
fullWidth
|
||||
variant="standard"
|
||||
onKeyPress={handleEnterClose}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>Tourner la clef</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user