admin ui featured blogs
This commit is contained in:
parent
8f5069e207
commit
1e75744b7b
@ -3,6 +3,7 @@ layout: blog
|
|||||||
title: Transition to Colemak keyboard layout
|
title: Transition to Colemak keyboard layout
|
||||||
segments:
|
segments:
|
||||||
- blog
|
- blog
|
||||||
|
- featured
|
||||||
published: true
|
published: true
|
||||||
date: 2020-05-11T05:38:18.797Z
|
date: 2020-05-11T05:38:18.797Z
|
||||||
tags:
|
tags:
|
||||||
|
@ -3,13 +3,13 @@ layout: blog
|
|||||||
title: Error handling with Either<Type>
|
title: Error handling with Either<Type>
|
||||||
segments:
|
segments:
|
||||||
- blog
|
- blog
|
||||||
|
- featured
|
||||||
published: true
|
published: true
|
||||||
date: 2022-02-28T11:30:54.195Z
|
date: 2022-02-28T11:30:54.195Z
|
||||||
tags:
|
tags:
|
||||||
- Development
|
- Development
|
||||||
- Guide
|
- Guide
|
||||||
---
|
---
|
||||||
|
|
||||||
We have started a new small internal project for automating a few workflows around counting worked hours and time offs.
|
We have started a new small internal project for automating a few workflows around counting worked hours and time offs.
|
||||||
|
|
||||||
## Application architecture
|
## Application architecture
|
||||||
@ -22,7 +22,7 @@ It will run on a server with a possibility of migrating into serverless when we
|
|||||||
|
|
||||||
As it is not a classic web server application I had to come up with slightly different error handling as we are used to. I've been trying to find a semi-functional API with all the good practices described in my [guide on error handling](/blog/2020-12-09-guide-on-error-handling). The main goal is to not let users be presented with internal information about errors. We want to show user-friendly messages instead.
|
As it is not a classic web server application I had to come up with slightly different error handling as we are used to. I've been trying to find a semi-functional API with all the good practices described in my [guide on error handling](/blog/2020-12-09-guide-on-error-handling). The main goal is to not let users be presented with internal information about errors. We want to show user-friendly messages instead.
|
||||||
I call this API semi-functional as **I didn't want to use monads** and go 100% functional. We use simple asynchronous functions to handle interactions.
|
I call this API semi-functional as **I didn't want to use monads** and go 100% functional. We use simple asynchronous functions to handle interactions.
|
||||||
The goal is to handle errors that are expected. Unexpected errors should still be thrown and caught by an _"Error boundary"_ around the whole app that will handle and log the error.
|
The goal is to handle errors that are expected. Unexpected errors should still be thrown and caught by an *"Error boundary"* around the whole app that will handle and log the error.
|
||||||
|
|
||||||
## Error types
|
## Error types
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ layout: blog
|
|||||||
title: "Our attempt at Rusty game jam - Weekly #25-2022"
|
title: "Our attempt at Rusty game jam - Weekly #25-2022"
|
||||||
segments:
|
segments:
|
||||||
- blog
|
- blog
|
||||||
|
- featured
|
||||||
published: true
|
published: true
|
||||||
date: 2022-06-26T20:02:47.419Z
|
date: 2022-06-26T20:02:47.419Z
|
||||||
tags:
|
tags:
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
---
|
---
|
||||||
layout: blog
|
layout: blog
|
||||||
title: "Keyboards & ergonomics of 21st century"
|
title: Keyboards & ergonomics of 21st century
|
||||||
segments:
|
segments:
|
||||||
- broadcasts
|
- broadcasts
|
||||||
|
- featured
|
||||||
published: true
|
published: true
|
||||||
date: 2023-04-27T21:22:21.191Z
|
date: 2023-04-27T21:22:21.191Z
|
||||||
tags:
|
tags:
|
||||||
|
@ -3,6 +3,7 @@ layout: blog
|
|||||||
title: I built my 3rd custom keyboard
|
title: I built my 3rd custom keyboard
|
||||||
segments:
|
segments:
|
||||||
- blog
|
- blog
|
||||||
|
- featured
|
||||||
published: true
|
published: true
|
||||||
date: 2023-08-29T19:34:17.071Z
|
date: 2023-08-29T19:34:17.071Z
|
||||||
tags:
|
tags:
|
||||||
|
@ -13,6 +13,11 @@ svgstore:
|
|||||||
server_dev:
|
server_dev:
|
||||||
cargo watch -x run
|
cargo watch -x run
|
||||||
|
|
||||||
|
# CMS server for local dev
|
||||||
|
# TODO #directory-swap
|
||||||
|
decap_server:
|
||||||
|
cd .. && npx decap-server
|
||||||
|
|
||||||
# Run dev server in watch mode
|
# Run dev server in watch mode
|
||||||
dev:
|
dev:
|
||||||
(just server_dev; just tailwind) | parallel
|
(just server_dev; just tailwind) | parallel
|
||||||
|
13
axum_server/src/featured_posts.rs
Normal file
13
axum_server/src/featured_posts.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
use crate::{pages::post::PostMetadata, post_list::get_post_list, post_parser::ParseResult};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
|
||||||
|
pub async fn get_featured_posts() -> Result<Vec<ParseResult<PostMetadata>>, StatusCode> {
|
||||||
|
let post_list = get_post_list::<PostMetadata>().await?;
|
||||||
|
|
||||||
|
let featured_posts = post_list
|
||||||
|
.into_iter()
|
||||||
|
.filter(|post| post.metadata.segments.contains(&"featured".to_string()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(featured_posts)
|
||||||
|
}
|
@ -1,7 +1,20 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
// This filter does not have extra arguments
|
// This filter does not have extra arguments
|
||||||
pub fn pretty_date(date_time: &DateTime<Utc>) -> ::askama::Result<String> {
|
pub fn pretty_date(date_time: &DateTime<Utc>) -> ::askama::Result<String> {
|
||||||
let formatted = format!("{}", date_time.format("%e %B %Y"));
|
let formatted = format!("{}", date_time.format("%e %B %Y"));
|
||||||
Ok(formatted)
|
Ok(formatted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This filter does not have extra arguments
|
||||||
|
pub fn description_filter(body: &String) -> ::askama::Result<String> {
|
||||||
|
let description = body
|
||||||
|
.lines()
|
||||||
|
.filter(|line| line.starts_with("<p"))
|
||||||
|
.take(3)
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join("\n");
|
||||||
|
debug!(description);
|
||||||
|
Ok(description)
|
||||||
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
use axum;
|
use axum::{self};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tower_livereload::LiveReloadLayer;
|
use tower_livereload::LiveReloadLayer;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
mod components;
|
mod components;
|
||||||
|
mod featured_posts;
|
||||||
mod feed;
|
mod feed;
|
||||||
mod filters;
|
mod filters;
|
||||||
mod pages;
|
mod pages;
|
||||||
@ -30,7 +31,11 @@ async fn main() {
|
|||||||
let app = router::get_router()
|
let app = router::get_router()
|
||||||
.nest_service("/styles", ServeDir::new("styles"))
|
.nest_service("/styles", ServeDir::new("styles"))
|
||||||
.nest_service("/images", ServeDir::new("../static/images"))
|
.nest_service("/images", ServeDir::new("../static/images"))
|
||||||
.nest_service("/svg", ServeDir::new("../static/svg"));
|
.nest_service("/svg", ServeDir::new("../static/svg"))
|
||||||
|
.nest_service(
|
||||||
|
"/config.yml",
|
||||||
|
ServeDir::new("../static/resources/config.yml"),
|
||||||
|
);
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
let app = app.layer(LiveReloadLayer::new());
|
let app = app.layer(LiveReloadLayer::new());
|
||||||
|
9
axum_server/src/pages/admin.rs
Normal file
9
axum_server/src/pages/admin.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
use askama::Template;
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "admin.html")]
|
||||||
|
pub struct AdminPageTemplate {}
|
||||||
|
|
||||||
|
pub async fn render_admin() -> AdminPageTemplate {
|
||||||
|
AdminPageTemplate {}
|
||||||
|
}
|
@ -1,25 +1,32 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
|
|
||||||
|
use crate::filters;
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{
|
components::{
|
||||||
site_footer::{render_site_footer, SiteFooter},
|
site_footer::{render_site_footer, SiteFooter},
|
||||||
site_header::HeaderProps,
|
site_header::HeaderProps,
|
||||||
},
|
},
|
||||||
|
featured_posts::get_featured_posts,
|
||||||
|
post_parser::ParseResult,
|
||||||
tag_list::get_popular_blog_tags,
|
tag_list::get_popular_blog_tags,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::post::PostMetadata;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "index.html")]
|
#[template(path = "index.html")]
|
||||||
pub struct IndexTemplate {
|
pub struct IndexTemplate {
|
||||||
site_footer: SiteFooter,
|
site_footer: SiteFooter,
|
||||||
header_props: HeaderProps,
|
header_props: HeaderProps,
|
||||||
blog_tags: Vec<String>,
|
blog_tags: Vec<String>,
|
||||||
|
featured_posts: Vec<ParseResult<PostMetadata>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_index() -> Result<IndexTemplate, StatusCode> {
|
pub async fn render_index() -> Result<IndexTemplate, StatusCode> {
|
||||||
let site_footer = tokio::spawn(render_site_footer());
|
let site_footer = tokio::spawn(render_site_footer());
|
||||||
let blog_tags = tokio::spawn(get_popular_blog_tags());
|
let blog_tags = tokio::spawn(get_popular_blog_tags());
|
||||||
|
let featured_posts = tokio::spawn(get_featured_posts());
|
||||||
|
|
||||||
let blog_tags = blog_tags
|
let blog_tags = blog_tags
|
||||||
.await
|
.await
|
||||||
@ -29,9 +36,14 @@ pub async fn render_index() -> Result<IndexTemplate, StatusCode> {
|
|||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let featured_posts = featured_posts
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)??;
|
||||||
|
|
||||||
Ok(IndexTemplate {
|
Ok(IndexTemplate {
|
||||||
site_footer,
|
site_footer,
|
||||||
header_props: HeaderProps::default(),
|
header_props: HeaderProps::default(),
|
||||||
blog_tags,
|
blog_tags,
|
||||||
|
featured_posts,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
pub mod admin;
|
||||||
pub mod contact;
|
pub mod contact;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
feed::render_rss_feed,
|
feed::render_rss_feed,
|
||||||
pages::{
|
pages::{
|
||||||
contact::render_contact, index::render_index, post::render_post,
|
admin::render_admin, contact::render_contact, index::render_index, post::render_post,
|
||||||
post_list::render_post_list,
|
post_list::render_post_list,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -16,6 +16,7 @@ pub fn get_router() -> Router {
|
|||||||
.route("/blog/tags/:tag", get(render_post_list))
|
.route("/blog/tags/:tag", get(render_post_list))
|
||||||
.route("/blog/:post_id", get(render_post))
|
.route("/blog/:post_id", get(render_post))
|
||||||
.route("/contact", get(render_contact))
|
.route("/contact", get(render_contact))
|
||||||
|
.route("/admin", get(render_admin))
|
||||||
.route("/feed.xml", get(render_rss_feed))
|
.route("/feed.xml", get(render_rss_feed))
|
||||||
.layer(
|
.layer(
|
||||||
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
|
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
|
||||||
|
@ -554,6 +554,10 @@ video {
|
|||||||
--tw-contain-style: ;
|
--tw-contain-style: ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row-span-3 {
|
||||||
|
grid-row: span 3 / span 3;
|
||||||
|
}
|
||||||
|
|
||||||
.m-1 {
|
.m-1 {
|
||||||
margin: 0.25rem;
|
margin: 0.25rem;
|
||||||
}
|
}
|
||||||
@ -596,16 +600,6 @@ video {
|
|||||||
margin-right: 1.5rem;
|
margin-right: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.my-0 {
|
|
||||||
margin-top: 0px;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.my-0\.5 {
|
|
||||||
margin-top: 0.125rem;
|
|
||||||
margin-bottom: 0.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.my-12 {
|
.my-12 {
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
@ -653,6 +647,10 @@ video {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -685,6 +683,14 @@ video {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid-flow-col {
|
||||||
|
grid-auto-flow: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-rows-3 {
|
||||||
|
grid-template-rows: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.flex-row {
|
.flex-row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
@ -713,6 +719,10 @@ video {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gap-4 {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.rounded {
|
.rounded {
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
15
axum_server/templates/admin.html
Normal file
15
axum_server/templates/admin.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/m-logo.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/m-logo-192.png" />
|
||||||
|
<title>Content Manager</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Include the script that builds the page and powers Decap CMS -->
|
||||||
|
<script src="https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
15
axum_server/templates/components/post_preview.html
Normal file
15
axum_server/templates/components/post_preview.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<article class="grid grid-rows-3 grid-flow-col gap-4">
|
||||||
|
<aside class="row-span-3">
|
||||||
|
<svg aria-hidden="true" class="h-12 w-12 fill-blue-950">
|
||||||
|
<use xlink:href="/svg/icons-sprite.svg#mail" />
|
||||||
|
</svg>
|
||||||
|
</aside>
|
||||||
|
<header>
|
||||||
|
<h3 class="text-lg font-medium mb-1">{{post.metadata.title}}</h3>
|
||||||
|
</header>
|
||||||
|
<section class="text-sm leading-5 text-gray-800">{{post.body|description_filter|safe}}</section>
|
||||||
|
<footer>
|
||||||
|
Footrik
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
|
@ -51,7 +51,11 @@
|
|||||||
<hr class="border-blue-950 m-5">
|
<hr class="border-blue-950 m-5">
|
||||||
|
|
||||||
<ul class="mx-5">
|
<ul class="mx-5">
|
||||||
|
{% for post in featured_posts %}
|
||||||
|
<li>
|
||||||
|
{% include "components/post_preview.html" %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ collections:
|
|||||||
widget: 'select'
|
widget: 'select'
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
|
- { label: 'OLAOLA', value: 'featured' }
|
||||||
- { label: 'Blog', value: 'blog' }
|
- { label: 'Blog', value: 'blog' }
|
||||||
- { label: 'Broadcasts', value: 'broadcasts' }
|
- { label: 'Broadcasts', value: 'broadcasts' }
|
||||||
- { label: 'Cookbook', value: 'cookbook' }
|
- { label: 'Cookbook', value: 'cookbook' }
|
||||||
|
141
static/resources/config.yml
Normal file
141
static/resources/config.yml
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
backend:
|
||||||
|
name: git-gateway
|
||||||
|
repo: michalvankodev/michalvankodev
|
||||||
|
branch: master # Branch to update (optional; defaults to master)
|
||||||
|
site_domain: michalvanko.dev
|
||||||
|
|
||||||
|
# when using the default proxy server port
|
||||||
|
local_backend: true
|
||||||
|
|
||||||
|
media_folder: 'static/images/uploads' # Media files will be stored in the repo under images/uploads
|
||||||
|
public_folder: '/images/uploads' # The src attribute for uploaded media will begin with /images/uploads
|
||||||
|
|
||||||
|
collections:
|
||||||
|
- name: 'blog' # Used in routes, e.g., /admin/collections/blog
|
||||||
|
label: 'Blog' # Used in the UI
|
||||||
|
folder: '_posts/blog' # The path to the folder where the documents are stored
|
||||||
|
create: true # Allow users to create new documents in this collection
|
||||||
|
slug: '{{year}}-{{month}}-{{day}}-{{slug}}' # Filename template, e.g., YYYY-MM-DD-title.md
|
||||||
|
fields: # The fields for each document, usually in front matter
|
||||||
|
- { label: 'Layout', name: 'layout', widget: 'hidden', default: 'blog' }
|
||||||
|
- { label: 'Title', name: 'title', widget: 'string' }
|
||||||
|
- label: 'Segments'
|
||||||
|
name: 'segments'
|
||||||
|
widget: 'select'
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- { label: 'Featured', value: 'featured' }
|
||||||
|
- { label: 'Blog', value: 'blog' }
|
||||||
|
- { label: 'Broadcasts', value: 'broadcasts' }
|
||||||
|
- { label: 'Cookbook', value: 'cookbook' }
|
||||||
|
default: ['blog']
|
||||||
|
- {
|
||||||
|
label: 'Published',
|
||||||
|
name: 'published',
|
||||||
|
widget: 'boolean',
|
||||||
|
default: true,
|
||||||
|
}
|
||||||
|
- { label: 'Publish Date', name: 'date', widget: 'datetime' }
|
||||||
|
- {
|
||||||
|
label: 'Featured Image',
|
||||||
|
name: 'thumbnail',
|
||||||
|
widget: 'image',
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
label: 'Tags',
|
||||||
|
name: 'tags',
|
||||||
|
widget: 'list',
|
||||||
|
default: ['News'],
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
- { label: 'Body', name: 'body', widget: 'markdown' }
|
||||||
|
- {
|
||||||
|
label: 'Writers notes',
|
||||||
|
name: 'notes',
|
||||||
|
widget: 'markdown',
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
- name: 'pages'
|
||||||
|
label: 'Pages'
|
||||||
|
files:
|
||||||
|
- label: 'Portfolio'
|
||||||
|
name: 'portfolio'
|
||||||
|
file: '_pages/portfolio.md'
|
||||||
|
fields:
|
||||||
|
- { label: Title, name: title, widget: string }
|
||||||
|
- { label: Body, name: body, widget: markdown }
|
||||||
|
- {
|
||||||
|
label: Work history prelude,
|
||||||
|
name: work_history_prelude,
|
||||||
|
widget: markdown,
|
||||||
|
}
|
||||||
|
- label: Work history
|
||||||
|
name: work_history
|
||||||
|
widget: list
|
||||||
|
fields:
|
||||||
|
- { label: Company name, name: name, widget: string }
|
||||||
|
- { label: Description, name: description, widget: markdown }
|
||||||
|
- label: Address
|
||||||
|
widget: object
|
||||||
|
name: address
|
||||||
|
fields:
|
||||||
|
- { label: Business name, name: name, widget: string, required: false }
|
||||||
|
- { label: Address, name: location, widget: string, required: false }
|
||||||
|
- { label: Zipcode, name: zipcode, widget: string, required: false }
|
||||||
|
- { label: City, name: city, widget: string, required: false }
|
||||||
|
- { label: Country, name: country, widget: string, required: false }
|
||||||
|
- { label: Displayed, name: displayed, widget: boolean, default: true }
|
||||||
|
- label: Projects
|
||||||
|
name: projects
|
||||||
|
widget: list
|
||||||
|
fields:
|
||||||
|
- { label: Project name, name: name, widget: string }
|
||||||
|
- {
|
||||||
|
label: Displayed,
|
||||||
|
name: displayed,
|
||||||
|
widget: boolean,
|
||||||
|
default: true,
|
||||||
|
}
|
||||||
|
- { label: Description, name: description, widget: markdown }
|
||||||
|
- label: Image
|
||||||
|
name: image
|
||||||
|
widget: object
|
||||||
|
fields:
|
||||||
|
- {
|
||||||
|
label: Source,
|
||||||
|
name: source,
|
||||||
|
widget: image,
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
label: Image description,
|
||||||
|
name: image_description,
|
||||||
|
widget: string,
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
- label: Presentations
|
||||||
|
name: presentations
|
||||||
|
widget: list
|
||||||
|
fields:
|
||||||
|
- { label: Name, name: name, widget: string }
|
||||||
|
- {
|
||||||
|
label: Displayed,
|
||||||
|
name: displayed,
|
||||||
|
widget: boolean,
|
||||||
|
default: true,
|
||||||
|
}
|
||||||
|
- { label: Description, name: description, widget: markdown }
|
||||||
|
- { label: Link, name: link, widget: string }
|
||||||
|
- label: Education
|
||||||
|
name: education
|
||||||
|
widget: list
|
||||||
|
fields:
|
||||||
|
- { label: Institution, name: name, widget: string }
|
||||||
|
- {
|
||||||
|
label: Displayed,
|
||||||
|
name: displayed,
|
||||||
|
widget: boolean,
|
||||||
|
default: true,
|
||||||
|
}
|
||||||
|
- { label: Description, name: description, widget: markdown }
|
Loading…
Reference in New Issue
Block a user