admin ui featured blogs
This commit is contained in:
		| @@ -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 } | ||||||
		Reference in New Issue
	
	Block a user