Created Woodpecker CI/CD deployment
- Created Dockerfile for packing up API and Web projects as Docker image
This commit is contained in:
36
Web/src/components/auth0ProviderWithNavigate.jsx
Normal file
36
Web/src/components/auth0ProviderWithNavigate.jsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { Auth0Provider } from "@auth0/auth0-react";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const Auth0ProviderWithNavigate = ({ children }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const domain = process.env.REACT_APP_AUTH0_DOMAIN;
|
||||
const clientId = process.env.REACT_APP_AUTH0_CLIENT_ID;
|
||||
const redirectUri = process.env.REACT_APP_AUTH0_CALLBACK_URL;
|
||||
const audience = process.env.REACT_APP_AUTH0_AUDIENCE;
|
||||
|
||||
const onRedirectCallback = (appState) => {
|
||||
navigate(appState?.returnTo || window.location.pathname);
|
||||
};
|
||||
|
||||
if (!(domain && clientId && redirectUri)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Auth0Provider
|
||||
domain={domain}
|
||||
clientId={clientId}
|
||||
authorizationParams={{
|
||||
redirect_uri: redirectUri,
|
||||
audience: audience
|
||||
}}
|
||||
onRedirectCallback={onRedirectCallback}
|
||||
useRefreshTokens={true}
|
||||
cacheLocation="localstorage"
|
||||
>
|
||||
{children}
|
||||
</Auth0Provider>
|
||||
);
|
||||
};
|
||||
39
Web/src/components/bookmarks/bookmark.jsx
Normal file
39
Web/src/components/bookmarks/bookmark.jsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import { Row, Col, Form, Button } from "../external";
|
||||
import DateTimeHelper from "../../utils/dateTimeHelper";
|
||||
import { getHighlightedText } from "../../utils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Tag } from "../../components";
|
||||
|
||||
export function Bookmark(props) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const getTruncatedDescription = () => `${props.bookmark.description.substring(0, 97)}${props.bookmark.description.length >= 97 ? "..." : ""}`;
|
||||
const getHighlightedTitle = () => getHighlightedText(props.bookmark.title, props.searchString);
|
||||
const getHighlightedDescription = () => getHighlightedText(getTruncatedDescription(), props.searchString)
|
||||
|
||||
return <div className="mb-3">
|
||||
<Row>
|
||||
<Col xs={1} className="d-flex justify-content-center align-items-center">
|
||||
<Form.Check onChange={(e) => {props.onBookmarkSelected(e.target.checked)}} checked={props.bookmark.isSelected} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<a href={props.bookmark.url} target="_blank" style={{textDecoration: "none"}}>{ getHighlightedTitle() }</a>
|
||||
<div className="font-weight-normal">{ getHighlightedDescription()}</div>
|
||||
<div>
|
||||
{
|
||||
props.bookmark.tags.map((tag) => <Tag key={props.bookmark.id-tag.id} tag={tag} searchString={props.searchString}/>)
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<span>{DateTimeHelper.getFriendlyDate(props.bookmark.createdOn)} | </span>
|
||||
<span>
|
||||
<Button variant="link" className="p-0 me-2" style={{textDecoration: "none"}} onClick={() => navigate(`/bookmarks/${props.bookmark.id}`)}>Edit</Button>
|
||||
<Button variant="link" className="p-0 me-2" style={{textDecoration: "none"}} onClick={props.onHideClicked}>{props.bookmark.isHidden ? "Unhide" : "Hide"}</Button>
|
||||
<Button variant="link" className="text-danger p-0 me-2" style={{textDecoration: "none"}} onClick={props.onDeleteClicked}>Delete</Button>
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
}
|
||||
23
Web/src/components/external/index.js
vendored
Normal file
23
Web/src/components/external/index.js
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
import Alert from "react-bootstrap/Alert";
|
||||
import Col from "react-bootstrap/Col";
|
||||
import Container from "react-bootstrap/Container";
|
||||
import Row from "react-bootstrap/Row";
|
||||
import Form from 'react-bootstrap/Form';
|
||||
import Button from "react-bootstrap/Button";
|
||||
import Modal from 'react-bootstrap/Modal'
|
||||
import { Table } from "react-bootstrap";
|
||||
import Dropdown from 'react-bootstrap/Dropdown';
|
||||
import DropdownButton from 'react-bootstrap/DropdownButton';
|
||||
|
||||
export {
|
||||
Alert,
|
||||
Col,
|
||||
Container,
|
||||
Row,
|
||||
Form,
|
||||
Button,
|
||||
Modal,
|
||||
Table,
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
};
|
||||
13
Web/src/components/footer.jsx
Normal file
13
Web/src/components/footer.jsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import Container from 'react-bootstrap/Container';
|
||||
|
||||
export function Footer(props) {
|
||||
return (
|
||||
<footer className="py-4 bg-light mt-auto">
|
||||
<Container fluid>
|
||||
<div className="text-center align-middle">YABA: Yet Another Bookmark App</div>
|
||||
</Container>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
69
Web/src/components/header.jsx
Normal file
69
Web/src/components/header.jsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import Navbar from 'react-bootstrap/Navbar';
|
||||
import NavDropdown from 'react-bootstrap/NavDropdown';
|
||||
import Container from 'react-bootstrap/Container';
|
||||
import Nav from 'react-bootstrap/Nav';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { useAuth0 } from "@auth0/auth0-react";
|
||||
|
||||
export function Header(props) {
|
||||
const { isAuthenticated, loginWithRedirect, logout } = useAuth0();
|
||||
const handleLogin = async () => {
|
||||
await loginWithRedirect({
|
||||
appState: {
|
||||
returnTo: "/bookmarks",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSignUp = async () => {
|
||||
await loginWithRedirect({
|
||||
appState: {
|
||||
returnTo: "/bookmarks",
|
||||
},
|
||||
authorizationParams: {
|
||||
screen_hint: "signup",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout({
|
||||
logoutParams: {
|
||||
returnTo: window.location.origin,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Navbar bg="dark" variant="dark" expand="lg">
|
||||
<Container>
|
||||
<Navbar.Brand href={!isAuthenticated ? "/" : "/bookmarks"}>YABA: Yet Another Bookmark App</Navbar.Brand>
|
||||
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||
<Navbar.Collapse id="basic-navbar-nav" />
|
||||
<Nav className="ms-auto">
|
||||
{!isAuthenticated && (
|
||||
<>
|
||||
<Nav.Link onClick={handleLogin}>Login</Nav.Link>
|
||||
<Nav.Link onClick={handleSignUp}>Register</Nav.Link>
|
||||
</>
|
||||
)}
|
||||
{ isAuthenticated && (
|
||||
<>
|
||||
<NavDropdown title="Bookmarks">
|
||||
<NavDropdown.Item href="/bookmarks">All</NavDropdown.Item>
|
||||
<NavDropdown.Item href="/bookmarks/hidden">Hidden</NavDropdown.Item>
|
||||
<NavDropdown.Divider />
|
||||
<NavDropdown.Item href="/bookmarks/new">New</NavDropdown.Item>
|
||||
</NavDropdown>
|
||||
<Nav.Link href="/tags">Tags</Nav.Link>
|
||||
<Nav.Link onClick={handleLogout}>Logout</Nav.Link>
|
||||
</>
|
||||
)}
|
||||
</Nav>
|
||||
</Container>
|
||||
</Navbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
Web/src/components/index.js
Normal file
23
Web/src/components/index.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { Footer } from "./footer";
|
||||
import { Header } from "./header";
|
||||
import { Bookmark } from "./bookmarks/bookmark";
|
||||
import { Auth0ProviderWithNavigate } from "./auth0ProviderWithNavigate";
|
||||
import { ProtectedRoute } from "./protectedRoute";
|
||||
import { SplashScreen } from "./shared/splashScreen";
|
||||
import { SearchForm } from "./shared/searchForm";
|
||||
import { UpsertTagModal } from "./tags/upsertTagModal";
|
||||
import { InterceptModal } from "./shared/interceptModal";
|
||||
import { Tag } from "./tags/tag";
|
||||
|
||||
export {
|
||||
Footer,
|
||||
Header,
|
||||
Bookmark,
|
||||
Auth0ProviderWithNavigate,
|
||||
ProtectedRoute,
|
||||
SplashScreen,
|
||||
SearchForm,
|
||||
UpsertTagModal,
|
||||
InterceptModal,
|
||||
Tag,
|
||||
};
|
||||
19
Web/src/components/protectedRoute.jsx
Normal file
19
Web/src/components/protectedRoute.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { withAuthenticationRequired } from "@auth0/auth0-react";
|
||||
import React from "react";
|
||||
import { SplashScreen } from "./shared/splashScreen";
|
||||
|
||||
export const ProtectedRoute = ({ layout, header, footer, view, viewProps }) => {
|
||||
const ProtectedView = withAuthenticationRequired(layout, {
|
||||
// onRedirecting: () => (
|
||||
// <div className="page-layout">
|
||||
// <SplashScreen message="You are being redirected to the authentication page" />
|
||||
// </div>
|
||||
// ),
|
||||
});
|
||||
|
||||
const Header = () => React.createElement(header);
|
||||
const Footer = () => React.createElement(footer);
|
||||
const View = () => React.createElement(view, viewProps);
|
||||
|
||||
return <ProtectedView header={<Header/>} footer={<Footer />} content={<View />} />;
|
||||
};
|
||||
32
Web/src/components/shared/interceptModal.jsx
Normal file
32
Web/src/components/shared/interceptModal.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { Button, Form, Modal } from "../external";
|
||||
|
||||
export function InterceptModal(props) {
|
||||
return (
|
||||
<Modal
|
||||
show={props.show}
|
||||
onHide={props.onHide}
|
||||
keyboard={false}
|
||||
backdrop={props.backdrop}
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{props.title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>{props.message}</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant={props.secondaryAction.variant}
|
||||
onClick={props.secondaryAction.onClick}
|
||||
>
|
||||
{props.secondaryAction.text}
|
||||
</Button>
|
||||
<Button
|
||||
variant={props.primaryAction.variant}
|
||||
onClick={props.primaryAction.onClick}
|
||||
>
|
||||
{props.primaryAction.text}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
25
Web/src/components/shared/searchForm.jsx
Normal file
25
Web/src/components/shared/searchForm.jsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
import { Button, Form } from "../external";
|
||||
|
||||
export function SearchForm(props) {
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
onSubmit={props.onHandleSearch}
|
||||
className="d-flex">
|
||||
<Form.Control
|
||||
type="text"
|
||||
className="me-2"
|
||||
defaultValue={props.searchString}
|
||||
onChange={props.onSearchStringChange}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
{ props.searchButtonText ?? "Search" }
|
||||
</Button>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
49
Web/src/components/shared/splashScreen.jsx
Normal file
49
Web/src/components/shared/splashScreen.jsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import Spinner from 'react-bootstrap/Spinner';
|
||||
import Row from 'react-bootstrap/Row';
|
||||
import Col from 'react-bootstrap/Col';
|
||||
import Button from "react-bootstrap/Button";
|
||||
|
||||
export function SplashScreen(props) {
|
||||
return(
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
opacity: "0.6",
|
||||
backgroundColor: "grey"
|
||||
}}
|
||||
>
|
||||
<Row style={{
|
||||
width: "100%"
|
||||
}}>
|
||||
<Col xs="12" style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}>
|
||||
<Spinner animation="border" />
|
||||
</Col>
|
||||
<Col xs="12"style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}>
|
||||
<div>{props.message}</div>
|
||||
</Col>
|
||||
<Col xs="12"style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}>
|
||||
{props.showCloseSplashScreenButton && (
|
||||
<Button variant="link" onClick={props.onCloseSpashScreenClick}>Close this window</Button>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
16
Web/src/components/tags/tag.jsx
Normal file
16
Web/src/components/tags/tag.jsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { Button } from "../external";
|
||||
import { getHighlightedText } from "../../utils";
|
||||
|
||||
export function Tag(props) {
|
||||
const getHighlightedTagName = () => props.searchString ? getHighlightedText(props.tag.name, props.searchString) : props.tag.name
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="link"
|
||||
style={{textDecoration: "none"}} className={`ms-0 me-2 p-0 ${props.tag.isHidden ? "text-danger" : null}`}
|
||||
onClick={props.onClick}>
|
||||
#{getHighlightedTagName()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
73
Web/src/components/tags/upsertTagModal.jsx
Normal file
73
Web/src/components/tags/upsertTagModal.jsx
Normal file
@ -0,0 +1,73 @@
|
||||
import React from "react";
|
||||
import { Button, Form, Modal } from "../external";
|
||||
import { useFormik } from "formik";
|
||||
import * as Yup from "yup"
|
||||
|
||||
export function UpsertTagModal(props) {
|
||||
const formik = useFormik({
|
||||
initialValues: props.tag,
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().required("Name is required").lowercase()
|
||||
}),
|
||||
enableReinitialize: true,
|
||||
onSubmit: async(values) => props.onSave(values)
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={props.show}
|
||||
onHide={props.onHide}
|
||||
keyboard={false}
|
||||
backdrop="static"
|
||||
>
|
||||
<Form onSubmit={formik.handleSubmit}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{props.tag.id > 0 ? "Edit Tag" : "New Tag"}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Name:</Form.Label>
|
||||
<Form.Control
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Enter tag name"
|
||||
defaultValue={formik.values.name}
|
||||
onBlur={formik.handleChange}
|
||||
isInvalid={!!formik.errors.name}
|
||||
/>
|
||||
<Form.Control.Feedback
|
||||
type="invalid"
|
||||
className="d-flex justify-content-start"
|
||||
>
|
||||
{formik.errors.name}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="isHidden"
|
||||
type="switch"
|
||||
label="Mark as hidden"
|
||||
checked={formik.values.isHidden}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{ props.tag.id > 0 && <Button
|
||||
variant="danger"
|
||||
onClick={props.onDelete}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
}
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user