1
0
Fork 0

Init simple Go React blog with Goxygen

This commit is contained in:
Aroy-Art 2024-07-19 22:13:45 +02:00
parent 634a24fe19
commit 1ada5abed1
Signed by: Aroy
GPG key ID: 583642324A1D2070
31 changed files with 873 additions and 0 deletions

View file

@ -0,0 +1,2 @@
webapp/node_modules/
webapp/build/

14
simple-blog/Dockerfile Normal file
View file

@ -0,0 +1,14 @@
FROM node:16.20-alpine3.18 AS JS_BUILD
COPY webapp /webapp
WORKDIR /webapp
RUN npm install && npm run build
FROM golang:1.22.1-alpine3.18 AS GO_BUILD
COPY server /server
WORKDIR /server
RUN go build -o /go/bin/server
FROM alpine:3.18.6
COPY --from=JS_BUILD /webapp/build* ./webapp/
COPY --from=GO_BUILD /go/bin/server ./
CMD ./server

58
simple-blog/README.md Normal file
View file

@ -0,0 +1,58 @@
# Simple-blog
## Environment setup
You need to have [Go](https://golang.org/),
[Node.js](https://nodejs.org/),
[Docker](https://www.docker.com/), and
[Docker Compose](https://docs.docker.com/compose/)
(comes pre-installed with Docker on Mac and Windows)
installed on your computer.
Verify the tools by running the following commands:
```sh
go version
npm --version
docker --version
docker-compose --version
```
## Start in development mode
In the project directory run the command (you might
need to prepend it with `sudo` depending on your setup):
```sh
docker-compose -f docker-compose-dev.yml up
```
This starts a local PostgreSQL database on `localhost:5432`.
The database will be populated with test records from the
[init-db.sql](init-db.sql) file.
Navigate to the `server` folder and start the back end:
```sh
cd server
go run server.go
```
The back end will serve on http://localhost:8080.
Navigate to the `webapp` folder, install dependencies,
and start the front end development server by running:
```sh
cd webapp
npm install
npm start
```
The application will be available on http://localhost:3000.
## Start in production mode
Perform:
```sh
docker-compose up
```
This will build the application and start it together with
its database. Access the application on http://localhost:8080.

View file

@ -0,0 +1,12 @@
version: "3.8"
services:
dev_db:
image: postgres:15.4-alpine3.18
environment:
POSTGRES_PASSWORD: pass
POSTGRES_USER: goxygen
POSTGRES_DB: goxygen
ports:
- 5432:5432
volumes:
- ./init-db.sql:/docker-entrypoint-initdb.d/init.sql

View file

@ -0,0 +1,21 @@
version: "3.8"
services:
app:
build: .
container_name: app
ports:
- 8080:8080
depends_on:
- db
environment:
profile: prod
db_pass: pass
db:
image: postgres:15.4-alpine3.18
environment:
POSTGRES_PASSWORD: pass
POSTGRES_USER: goxygen
POSTGRES_DB: goxygen
volumes:
- ./init-db.sql:/docker-entrypoint-initdb.d/init.sql
- ./pgdata:/var/lib/postgresql/data

13
simple-blog/init-db.sql Normal file
View file

@ -0,0 +1,13 @@
CREATE TABLE technologies (
name VARCHAR(255),
details VARCHAR(255)
);
insert into technologies values (
'Go', 'An open source programming language that makes it easy to build simple and efficient software.'
);
insert into technologies values (
'JavaScript', 'A lightweight, interpreted, or just-in-time compiled programming language with first-class functions.'
);
insert into technologies values (
'PostgreSQL', 'A powerful, open source object-relational database system'
);

View file

@ -0,0 +1,36 @@
package db
import (
"database/sql"
"simple-blog/model"
)
type DB interface {
GetTechnologies() ([]*model.Technology, error)
}
type PostgresDB struct {
db *sql.DB
}
func NewDB(db *sql.DB) DB {
return PostgresDB{db: db}
}
func (d PostgresDB) GetTechnologies() ([]*model.Technology, error) {
rows, err := d.db.Query("select name, details from technologies")
if err != nil {
return nil, err
}
defer rows.Close()
var tech []*model.Technology
for rows.Next() {
t := new(model.Technology)
err = rows.Scan(&t.Name, &t.Details)
if err != nil {
return nil, err
}
tech = append(tech, t)
}
return tech, nil
}

View file

@ -0,0 +1,5 @@
module simple-blog
go 1.22
require github.com/lib/pq v1.10.9

View file

@ -0,0 +1,2 @@
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=

View file

@ -0,0 +1,6 @@
package model
type Technology struct {
Name string `json:"name"`
Details string `json:"details"`
}

View file

@ -0,0 +1,35 @@
package main
import (
"database/sql"
"log"
"os"
"simple-blog/db"
"simple-blog/web"
_ "github.com/lib/pq"
)
func main() {
d, err := sql.Open("postgres", dataSource())
if err != nil {
log.Fatal(err)
}
defer d.Close()
// CORS is enabled only in prod profile
cors := os.Getenv("profile") == "prod"
app := web.NewApp(db.NewDB(d), cors)
err = app.Serve()
log.Println("Error", err)
}
func dataSource() string {
host := "localhost"
pass := "pass"
if os.Getenv("profile") == "prod" {
host = "db"
pass = os.Getenv("db_pass")
}
return "postgresql://" + host + ":5432/goxygen" +
"?user=goxygen&sslmode=disable&password=" + pass
}

View file

@ -0,0 +1,63 @@
package web
import (
"encoding/json"
"log"
"net/http"
"simple-blog/db"
)
type App struct {
d db.DB
handlers map[string]http.HandlerFunc
}
func NewApp(d db.DB, cors bool) App {
app := App{
d: d,
handlers: make(map[string]http.HandlerFunc),
}
techHandler := app.GetTechnologies
if !cors {
techHandler = disableCors(techHandler)
}
app.handlers["/api/technologies"] = techHandler
app.handlers["/"] = http.FileServer(http.Dir("/webapp")).ServeHTTP
return app
}
func (a *App) Serve() error {
for path, handler := range a.handlers {
http.Handle(path, handler)
}
log.Println("Web server is available on port 8080")
return http.ListenAndServe(":8080", nil)
}
func (a *App) GetTechnologies(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
technologies, err := a.d.GetTechnologies()
if err != nil {
sendErr(w, http.StatusInternalServerError, err.Error())
return
}
err = json.NewEncoder(w).Encode(technologies)
if err != nil {
sendErr(w, http.StatusInternalServerError, err.Error())
}
}
func sendErr(w http.ResponseWriter, code int, message string) {
resp, _ := json.Marshal(map[string]string{"error": message})
http.Error(w, string(resp), code)
}
// Needed in order to disable CORS for local development
func disableCors(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
h(w, r)
}
}

View file

@ -0,0 +1,57 @@
package web
import (
"errors"
"net/http"
"net/http/httptest"
"simple-blog/model"
"testing"
)
type MockDb struct {
tech []*model.Technology
err error
}
func (m *MockDb) GetTechnologies() ([]*model.Technology, error) {
return m.tech, m.err
}
func TestApp_GetTechnologies(t *testing.T) {
app := App{d: &MockDb{
tech: []*model.Technology{
{"Tech1", "Details1"},
{"Tech2", "Details2"},
},
}}
r, _ := http.NewRequest("GET", "/api/technologies", nil)
w := httptest.NewRecorder()
app.GetTechnologies(w, r)
if w.Code != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", w.Code, http.StatusOK)
}
want := `[{"name":"Tech1","details":"Details1"},{"name":"Tech2","details":"Details2"}]` + "\n"
if got := w.Body.String(); got != want {
t.Errorf("handler returned unexpected body: got %v want %v", got, want)
}
}
func TestApp_GetTechnologies_WithDBError(t *testing.T) {
app := App{d: &MockDb{
tech: nil,
err: errors.New("unknown error"),
}}
r, _ := http.NewRequest("GET", "/api/technologies", nil)
w := httptest.NewRecorder()
app.GetTechnologies(w, r)
if w.Code != http.StatusInternalServerError {
t.Errorf("handler returned wrong status code: got %v want %v", w.Code, http.StatusOK)
}
}

View file

@ -0,0 +1 @@
REACT_APP_API_URL=http://localhost:8080

View file

@ -0,0 +1 @@
REACT_APP_API_URL=

View file

@ -0,0 +1,37 @@
{
"name": "simple-blog",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "~1.2.1",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"react-scripts": "~5.0.1"
},
"devDependencies": {
"@testing-library/jest-dom": "~5.16.5",
"@testing-library/react": "~13.4.0",
"@testing-library/user-event": "~14.4.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using goxygen"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>simple-blog</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start`.
To create a production bundle, use `npm run build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View file

@ -0,0 +1,25 @@
{
"short_name": "simple-blog",
"name": "simple-blog",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View file

@ -0,0 +1,34 @@
body {
margin-top: 5%;
padding-right: 5%;
padding-left: 5%;
font-size: larger;
}
@media screen and (min-width: 800px) {
body {
padding-right: 15%;
padding-left: 15%;
}
}
@media screen and (min-width: 1600px) {
body {
padding-right: 30%;
padding-left: 30%;
}
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
background-color: #b3e6ff;
}
.title {
text-align: center;
}
.logo {
text-align: center;
}

View file

@ -0,0 +1,23 @@
import React from 'react';
import './App.css';
import Logo from './Logo';
import {Tech} from "./tech/Tech";
export function App() {
return (
<div className="app">
<h2 className="title">simple-blog</h2>
<div className="logo"><Logo/></div>
<div>
This project is generated with <b><a
href="https://github.com/shpota/goxygen">goxygen</a></b>.
<p/>The following list of technologies comes from
a REST API call to the Go-based back end. Find
and change the corresponding code
in <code>webapp/src/tech/Tech.js
</code> and <code>server/web/app.go</code>.
<Tech/>
</div>
</div>
);
}

View file

@ -0,0 +1,9 @@
import React from 'react';
import {render} from '@testing-library/react';
import {App} from './App';
test('renders learn react link', () => {
const {getByText} = render(<App/>);
const linkElement = getByText(/goxygen/i);
expect(linkElement).toBeInTheDocument();
});

View file

@ -0,0 +1,322 @@
import React from "react";
const Logo = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
height="150px"
version="1.1"
viewBox="0 0 464.745 338.814"
>
<defs>
<linearGradient id="linearGradient4289">
<stop offset="0" stopColor="#00dce2" stopOpacity="1"></stop>
<stop offset="1" stopColor="#97f0ff" stopOpacity="1"></stop>
</linearGradient>
<linearGradient
id="linearGradient4295"
x1="431.334"
x2="400.666"
y1="531.393"
y2="386.128"
gradientTransform="translate(129.453 193.74)"
gradientUnits="userSpaceOnUse"
xlinkHref="#linearGradient4289"
></linearGradient>
</defs>
<g display="inline" transform="translate(-111.737 -176.023)">
<path
fill="url(#linearGradient4295)"
fillOpacity="1"
fillRule="evenodd"
stroke="none"
strokeLinecap="butt"
strokeLinejoin="miter"
strokeOpacity="1"
strokeWidth="1"
d="M132.042 433.463c23.84-32.316 103.542-15.624 130.056.606 89.595 44.02 169.113-19.05 186.402-33.707 42.52-37.92 60.763-37.33 75.655-30.124 4.45 2.154 10.795 11.465 14.408 12.5 98.104 111.17-186.007 131.629-317.798 102.342-60.452-13.433-102.376-33.11-88.723-51.617z"
></path>
</g>
<g display="none" transform="translate(-111.737 -176.023)">
<path
fill="#f3df49"
fillOpacity="1"
fillRule="evenodd"
stroke="none"
strokeLinecap="butt"
strokeLinejoin="miter"
strokeOpacity="1"
strokeWidth="1"
d="M132.042 433.463c23.84-32.316 103.542-15.624 130.056.606 89.595 44.02 169.113-19.05 186.402-33.707 42.52-37.92 60.763-37.33 75.655-30.124 4.45 2.154 10.795 11.465 14.408 12.5 98.104 111.17-186.007 131.629-317.798 102.342-60.452-13.433-102.376-33.11-88.723-51.617z"
></path>
</g>
<g transform="translate(17.717 17.717)">
<path
fill="#2e2e2c"
fillOpacity="1"
d="M397.824 203.03c-2.159.004-4.045.321-5.6.972-1.554.651-2.778 1.63-3.645 2.857-.866 1.227-1.375 2.699-1.529 4.306-.153 1.607.049 3.349.582 5.126.579 1.93 1.307 3.55 2.192 4.91a13.088 13.088 0 003.107 3.348c1.182.895 2.503 1.59 3.932 2.165 1.428.575 2.962 1.03 4.566 1.444l.553.146.548.15.544.152.54.155c.982.297 1.843.588 2.584.913.74.325 1.364.677 1.87 1.086.506.41.895.875 1.153 1.453.258.577.386 1.265.358 2.152a5.951 5.951 0 01-.47 2.131 6.491 6.491 0 01-1.208 1.864 7.522 7.522 0 01-1.858 1.459 8.952 8.952 0 01-2.42.918c-1.06.24-2.007.293-2.854.183a5.78 5.78 0 01-2.27-.79c-.672-.405-1.267-.943-1.811-1.577-.545-.634-1.037-1.364-1.507-2.154l-1.985 1.807-2.038 1.863-2.09 1.915-2.142 1.96c.653 1.394 1.473 2.66 2.469 3.74a11.564 11.564 0 003.52 2.618c1.353.647 2.886 1.049 4.598 1.16 1.712.11 3.601-.072 5.668-.572 2.113-.512 4.089-1.296 5.865-2.332 1.777-1.035 3.355-2.321 4.663-3.833a16.57 16.57 0 003.055-5.187 17.519 17.519 0 001.046-6.319c-.046-2.03-.392-3.575-.957-4.93-.565-1.354-1.35-2.504-2.325-3.564-.976-1.06-2.123-1.878-3.477-2.66a23.428 23.428 0 00-4.756-2.066l-.546-.168-.554-.16-.56-.157-.57-.14a34.934 34.934 0 01-2.614-.735c-.76-.255-1.408-.529-1.952-.849-.544-.32-.983-.686-1.331-1.128-.348-.441-.605-.957-.792-1.578a3.249 3.249 0 01-.124-1.428c.067-.442.244-.843.525-1.183.281-.34.667-.617 1.15-.817.484-.2 1.065-.32 1.733-.354a8.835 8.835 0 011.843.09 7.096 7.096 0 011.677.485 7.75 7.75 0 011.552.897c.503.37 1 .815 1.513 1.346l1.164-1.306 1.08-1.395 1.126-1.449 1.302-1.465c-.991-1.037-1.863-1.841-2.77-2.497-.906-.656-1.868-1.17-3.04-1.614a20.581 20.581 0 00-3.912-1.033c-1.382-.225-2.43-.278-4.371-.33 0 0 1.894-.003 0 0zm-35.9 4.18l2.956 8.923 2.485 8.916 1.614 9.051.648 9.272a9.355 9.355 0 01-.439 3.521c-.309.977-.768 1.768-1.352 2.408-.584.64-1.294 1.13-2.115 1.507-.82.376-1.752.64-2.78.827-1.073.196-1.992.196-2.801.044a5.873 5.873 0 01-2.145-.869c-.636-.412-1.207-.934-1.756-1.523-.549-.59-1.077-1.246-1.625-1.93l-2.483 1.763-2.427 1.737-2.357 1.71-2.276 1.68c-1.22.901 1.792 2.467 2.93 3.515a15.408 15.408 0 003.934 2.65c1.492.697 3.171 1.185 5.068 1.395 1.897.21 4.013.141 6.381-.281 2.622-.468 5.04-1.253 7.208-2.352 2.168-1.098 4.086-2.511 5.705-4.232a19.5 19.5 0 003.883-6.069c.94-2.32 1.683-4.968 1.511-7.8l-.473-9.292-1.59-8.98-2.622-8.769-3.26-8.728-2.891.48-2.94.48-2.98.476z"
display="inline"
></path>
</g>
<g
strokeDasharray="none"
strokeLinejoin="round"
strokeMiterlimit="4"
strokeOpacity="1"
strokeWidth="2"
transform="translate(-111.737 -176.023)"
>
<path
fill="#388e3c"
fillOpacity="1"
fillRule="evenodd"
stroke="#000"
strokeLinecap="butt"
d="M198.79 380.429c.258-2.631 4.098-13.37 31.374-.952 87.587 39.873 172.374 28.761 238.086 1.513-35.446 31.74-105.09 70.96-151.5 82.622-33.173-.25-69.22-8.332-91.922-33.79-10.377-11.637-27.083-38.739-26.038-49.393z"
display="inline"
></path>
<ellipse
cx="280.876"
cy="474.384"
fill="#1b5e2f"
fillOpacity="0.941"
stroke="none"
strokeDashoffset="0"
strokeLinecap="round"
opacity="1"
rx="83.288"
ry="22.62"
transform="matrix(.9852 -.1714 .13103 .99138 0 0)"
></ellipse>
</g>
<g
stroke="#000"
strokeOpacity="1"
transform="translate(-111.737 -176.023)"
>
<path
fill="#8ed4fe"
fillOpacity="1"
fillRule="evenodd"
strokeDasharray="none"
strokeLinecap="round"
strokeLinejoin="miter"
strokeMiterlimit="4"
strokeWidth="2.367"
d="M369.51 223.971c8.7-7.655 16.286-10.658 23.079-7.257 4.975 2.49 3.88 9.206.186 14.35-4.404 6.131-8.49 9.32-8.744 9.31"
display="inline"
opacity="1"
></path>
<path
fill="#000"
fillOpacity="1"
fillRule="evenodd"
strokeLinecap="butt"
strokeLinejoin="miter"
strokeWidth="0.395"
d="M377.176 235.834c.04.824 4.25 3.422 5.457 3.096 1.767-.477 8.642-9.057 8.6-10.83-.114-4.972-14.082 7.2-14.057 7.734z"
display="inline"
opacity="1"
></path>
<path
fill="#8ed4fd"
fillOpacity="1"
strokeDasharray="none"
strokeDashoffset="0"
strokeLinecap="round"
strokeLinejoin="bevel"
strokeMiterlimit="4"
strokeWidth="2"
d="M388.092 315.25c1.496-1.405 4.867-1.942 7.212-3.04 4.645-2.175 7.611-5.532 6.625-7.496-.985-1.965-5.55-1.794-10.195.382-2.244 1.052-4.386 1.93-6.409 2.138"
display="inline"
opacity="1"
></path>
<path
fill="#8ed4fd"
fillOpacity="1"
fillRule="evenodd"
strokeDasharray="none"
strokeLinecap="butt"
strokeLinejoin="miter"
strokeMiterlimit="4"
strokeWidth="4"
d="M298.548 433.973c-13.22-2.143-19.679-12.58-26.607-23.445-39.8-62.416-31.281-150.252-11.383-173.744 28.95-34.182 95.759-33.51 125.692 5.328 23.506 26.833-1.187 59.973-.604 88.64 42.218 17.603 36.354 74.36 36.354 74.36s-29.164-3.515-46.414.485c-13.469 3.124-29.732 10.114-48.81 19.564-9.17 4.542-16.54 10.707-28.228 8.812z"
></path>
<path
fill="#8ed4fd"
fillOpacity="1"
strokeDasharray="none"
strokeDashoffset="0"
strokeLinecap="round"
strokeLinejoin="bevel"
strokeMiterlimit="4"
strokeWidth="4"
d="M250.55 329.788c-2.015 1.902-6.565 2.64-9.73 4.129-6.264 2.955-10.258 7.499-8.92 10.148 1.339 2.65 7.503 2.403 13.769-.553 3.025-1.428 5.915-2.621 8.646-2.91"
display="inline"
opacity="1"
></path>
<path
fill="none"
fillRule="evenodd"
strokeLinecap="butt"
strokeLinejoin="miter"
strokeWidth="1"
d="M391.384 334.295c-9.823-5.145-22.274-2.475-22.274-2.475"
></path>
<path
fill="#8ed4fd"
fillOpacity="1"
strokeDasharray="none"
strokeDashoffset="0"
strokeLinecap="round"
strokeLinejoin="bevel"
strokeMiterlimit="4"
strokeWidth="4"
d="M286.83 422.587c6.996 10.92 1.841 23.67 8.24 25.193 5.237 1.246 10.83-11.015 16.597-20.235"
display="inline"
opacity="1"
></path>
<circle
cx="312.057"
cy="259.305"
r="33.415"
fill="#fff"
fillOpacity="1"
strokeDasharray="none"
strokeDashoffset="0"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="4"
strokeWidth="3"
opacity="1"
></circle>
<ellipse
cx="383.257"
cy="275.775"
fill="#fff"
fillOpacity="1"
strokeDasharray="none"
strokeDashoffset="0"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="4"
strokeWidth="2.806"
opacity="1"
rx="23.077"
ry="24.17"
></ellipse>
<g
fillOpacity="1"
strokeDasharray="none"
strokeMiterlimit="4"
transform="translate(4.328 .457)"
>
<path
fill="#fff"
fillRule="evenodd"
strokeLinecap="butt"
strokeLinejoin="miter"
strokeWidth="2.564"
d="M335.728 298.82c-1.108 3.632-.698 12.213-.698 12.213l10.475 3.152s4.853-5.714 5.444-7.781c.592-2.068.338-3.207.338-3.207z"
display="inline"
opacity="1"
></path>
<path
fill="#d3b78d"
fillRule="evenodd"
strokeLinecap="butt"
strokeLinejoin="miter"
strokeWidth="2.564"
d="M327.716 292.849c-7.616 7.896 4.591 8.084 14.163 10.196 7.577 1.67 16.83 10.159 19.098 2.508 3.92-13.223-23.494-22.831-33.261-12.704z"
display="inline"
opacity="1"
></path>
<ellipse
cx="420.17"
cy="-167.764"
fill="#000"
strokeDashoffset="0"
strokeLinecap="round"
strokeLinejoin="bevel"
strokeWidth="1.923"
display="inline"
opacity="1"
rx="11.351"
ry="6.498"
transform="scale(1 -1) rotate(-18.249)"
></ellipse>
</g>
<circle
cx="324.338"
cy="267.016"
r="8.853"
fill="#000"
fillOpacity="1"
strokeDasharray="none"
strokeDashoffset="0"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="4"
strokeWidth="2.285"
opacity="1"
></circle>
<circle
cx="392.334"
cy="283.759"
r="6.781"
fill="#000"
fillOpacity="1"
strokeDasharray="none"
strokeDashoffset="0"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="4"
strokeWidth="1.75"
opacity="1"
></circle>
<path
fill="#8ed4fe"
fillOpacity="1"
fillRule="evenodd"
strokeDasharray="none"
strokeLinecap="round"
strokeLinejoin="miter"
strokeMiterlimit="4"
strokeWidth="2.913"
d="M278.198 228.795c-3.902-10.61-2.26-20.229 2.508-28.163 3.346-5.568 7.803-6.188 11.596-4.822 6.332 2.281 9.116 9.465 12.68 20.664"
display="inline"
opacity="1"
></path>
<path
fill="#000"
fillOpacity="1"
fillRule="evenodd"
strokeLinecap="butt"
strokeLinejoin="miter"
strokeWidth="0.486"
d="M288.665 223.718c.788.64 5.95-1.759 7.315-2.47 2.72-1.417.131-13.619-2.11-15.238-4.96-3.583-5.716 17.294-5.205 17.708z"
display="inline"
opacity="1"
></path>
</g>
<g display="inline" transform="translate(-111.737 -176.023)">
<path
fill="#4caf50"
fillOpacity="1"
fillRule="evenodd"
stroke="#000"
strokeDasharray="none"
strokeLinecap="butt"
strokeLinejoin="round"
strokeMiterlimit="4"
strokeOpacity="1"
strokeWidth="2"
d="M199.505 378.716c-3.003 5.856-4.34 9.43-3.462 28.282 21.667 47.77 122.45 97.485 235.386 8.578 16.785-13.214 43.013-41.156 70.714-46.428 20.44-3.89 35.552 12.637 35.552 12.637l-9.17-19.116c-10.613-21.217-55.799-16.392-82.11 2.943-35.088 25.784-70.3 86.105-142.844 83.536-79.713-2.823-108.82-63.392-104.066-70.432z"
></path>
</g>
</svg>
);
};
export default Logo;

View file

@ -0,0 +1,8 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View file

@ -0,0 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import {App} from './App';
ReactDOM.render(<App/>, document.getElementById('root'));

View file

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';

View file

@ -0,0 +1,3 @@
.technologies {
margin-top: 5px;
}

View file

@ -0,0 +1,29 @@
import React, {Component} from "react";
import axios from "axios";
import "./Tech.css"
export class Tech extends Component {
state = {
technologies: []
};
componentDidMount() {
axios.get(`${process.env.REACT_APP_API_URL}/api/technologies`)
.then(resp => this.setState({
technologies: resp.data
}));
}
render() {
const technologies = this.state.technologies.map((technology, i) =>
<li key={i}>
<b>{technology.name}</b>: {technology.details}
</li>
);
return (
<ul className="technologies">
{technologies}
</ul>
);
}
}