mirror of https://github.com/Budibase/budibase.git
6 changed files with 651 additions and 228 deletions
@ -0,0 +1,124 @@ |
|||
<script> |
|||
export let backgroundColour |
|||
export let imageSrc |
|||
export let name |
|||
export let icon |
|||
export let overlayEnabled = true |
|||
|
|||
let imageError = false |
|||
let imageLoaded = false |
|||
|
|||
const imageRenderError = () => { |
|||
imageError = true |
|||
} |
|||
|
|||
const imageLoadSuccess = () => { |
|||
imageLoaded = true |
|||
} |
|||
</script> |
|||
|
|||
<div class="template-card" style="background-color:{backgroundColour};"> |
|||
<div class="template-thumbnail card-body"> |
|||
<img |
|||
alt={name} |
|||
src={imageSrc} |
|||
on:error={imageRenderError} |
|||
on:load={imageLoadSuccess} |
|||
class={`${imageLoaded ? "loaded" : ""}`} |
|||
/> |
|||
<div style={`display:${imageError ? "block" : "none"}`}> |
|||
<svg |
|||
width="26px" |
|||
height="26px" |
|||
class="spectrum-Icon" |
|||
style="color: white" |
|||
focusable="false" |
|||
> |
|||
<use xlink:href="#spectrum-icon-18-{icon}" /> |
|||
</svg> |
|||
</div> |
|||
<div class={overlayEnabled ? "template-thumbnail-action-overlay" : ""}> |
|||
<slot /> |
|||
</div> |
|||
</div> |
|||
<div class="template-thumbnail-text"> |
|||
<div>{name}</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
.template-thumbnail { |
|||
position: relative; |
|||
} |
|||
|
|||
.template-card:hover .template-thumbnail-action-overlay { |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.template-thumbnail-action-overlay { |
|||
position: absolute; |
|||
top: 0px; |
|||
left: 0px; |
|||
width: 100%; |
|||
height: 70%; |
|||
display: none; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background-color: rgba(0, 0, 0, 0.7); |
|||
border-top-right-radius: inherit; |
|||
border-top-left-radius: inherit; |
|||
} |
|||
|
|||
.template-thumbnail-text { |
|||
position: absolute; |
|||
bottom: 0px; |
|||
display: flex; |
|||
align-items: center; |
|||
height: 30%; |
|||
width: 100%; |
|||
color: var( |
|||
--spectrum-heading-xs-text-color, |
|||
var(--spectrum-alias-heading-text-color) |
|||
); |
|||
background-color: var(--spectrum-global-color-gray-50); |
|||
} |
|||
|
|||
.template-thumbnail-text > div { |
|||
padding-left: 1rem; |
|||
padding-right: 1rem; |
|||
} |
|||
|
|||
.template-card { |
|||
position: relative; |
|||
display: flex; |
|||
border-radius: var(--border-radius-s); |
|||
border: 1px solid var(--spectrum-global-color-gray-300); |
|||
overflow: hidden; |
|||
min-height: 200px; |
|||
} |
|||
|
|||
.template-card > *{ |
|||
width : 100% |
|||
} |
|||
|
|||
.template-card img.loaded { |
|||
display: block; |
|||
} |
|||
|
|||
.template-card img { |
|||
display: none; |
|||
max-width: 100%; |
|||
border-radius: var(--border-radius-s) 0px var(--border-radius-s) 0px; |
|||
} |
|||
|
|||
.template-card:hover { |
|||
background: var(--spectrum-alias-background-color-tertiary); |
|||
} |
|||
|
|||
.card-body { |
|||
padding-left: 1rem; |
|||
padding-top: 1rem; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,147 @@ |
|||
<script> |
|||
import { Layout, Detail, Heading, Button, Modal } from "@budibase/bbui" |
|||
import TemplateCard from "components/common/TemplateCard.svelte" |
|||
import CreateAppModal from "components/start/CreateAppModal.svelte" |
|||
|
|||
export let templates |
|||
|
|||
let selectedTemplateCategory |
|||
let creationModal |
|||
let template |
|||
|
|||
const groupTemplatesByCategory = (templates, categoryFilter) => { |
|||
let grouped = templates.reduce((acc, template) => { |
|||
if ( |
|||
typeof categoryFilter === "string" && |
|||
[categoryFilter].indexOf(template.category) < 0 |
|||
) { |
|||
return acc |
|||
} |
|||
|
|||
acc[template.category] = !acc[template.category] |
|||
? [] |
|||
: acc[template.category] |
|||
acc[template.category].push(template) |
|||
|
|||
return acc |
|||
}, {}) |
|||
return grouped |
|||
} |
|||
|
|||
$: filteredTemplates = groupTemplatesByCategory( |
|||
templates, |
|||
selectedTemplateCategory |
|||
) |
|||
|
|||
$: filteredTemplateCategories = filteredTemplates |
|||
? Object.keys(filteredTemplates).sort() |
|||
: [] |
|||
|
|||
$: templateCategories = templates |
|||
? Object.keys(groupTemplatesByCategory(templates)).sort() |
|||
: [] |
|||
|
|||
const stopAppCreation = () => { |
|||
template = null |
|||
} |
|||
</script> |
|||
|
|||
<div class="template-category-filters"> |
|||
<Layout noPadding gap="S"> |
|||
<Heading size="S">Templates</Heading> |
|||
|
|||
<div class="template-category-filters spectrum-ActionGroup"> |
|||
<button |
|||
on:click={() => { |
|||
selectedTemplateCategory = null |
|||
}} |
|||
class="template-category-filter-all template-category-filter spectrum-ActionButton spectrum-ActionButton--sizeM |
|||
spectrum-ActionGroup-item {!selectedTemplateCategory || |
|||
'is-selected'}" |
|||
> |
|||
<span class="spectrum-ActionButton-label">All</span> |
|||
</button> |
|||
|
|||
{#each templateCategories as templateCategoryKey} |
|||
<button |
|||
on:click={() => { |
|||
selectedTemplateCategory = templateCategoryKey |
|||
}} |
|||
class="template-category-filter spectrum-ActionButton spectrum-ActionButton--sizeM |
|||
spectrum-ActionGroup-item {templateCategoryKey == |
|||
selectedTemplateCategory || 'is-selected'}" |
|||
> |
|||
<span class="spectrum-ActionButton-label">{templateCategoryKey}</span> |
|||
</button> |
|||
{/each} |
|||
</div> |
|||
</Layout> |
|||
</div> |
|||
|
|||
<div class="template-categories"> |
|||
<Layout gap="XL" noPadding> |
|||
{#each filteredTemplateCategories as templateCategoryKey} |
|||
<div class="template-category"> |
|||
<Detail size="M">{templateCategoryKey}</Detail> |
|||
<div class="template-grid"> |
|||
{#each filteredTemplates[templateCategoryKey] as templateEntry} |
|||
<TemplateCard |
|||
name={templateEntry.name} |
|||
imageSrc={templateEntry.image} |
|||
backgroundColour={templateEntry.background} |
|||
icon={templateEntry.icon} |
|||
> |
|||
<Button |
|||
cta |
|||
on:click={() => { |
|||
template = templateEntry |
|||
creationModal.show() |
|||
}} |
|||
> |
|||
Use template |
|||
</Button> |
|||
<a |
|||
href={templateEntry.url} |
|||
target="_blank" |
|||
class="overlay-preview-link spectrum-Button spectrum-Button--sizeM spectrum-Button--secondary" |
|||
on:click|stopPropagation |
|||
> |
|||
Details |
|||
</a> |
|||
</TemplateCard> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
{/each} |
|||
</Layout> |
|||
</div> |
|||
|
|||
<Modal |
|||
bind:this={creationModal} |
|||
padding={false} |
|||
width="600px" |
|||
on:hide={stopAppCreation} |
|||
> |
|||
<CreateAppModal {template} /> |
|||
</Modal> |
|||
|
|||
<style> |
|||
.template-grid { |
|||
padding-top: 10px; |
|||
display: grid; |
|||
grid-gap: var(--spacing-xl); |
|||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); |
|||
} |
|||
|
|||
a:hover.spectrum-Button.spectrum-Button--secondary.overlay-preview-link { |
|||
background-color: #c8c8c8; |
|||
border-color: #c8c8c8; |
|||
color: #505050; |
|||
} |
|||
|
|||
a.spectrum-Button--secondary.overlay-preview-link { |
|||
margin-top: 20px; |
|||
border-color: #c8c8c8; |
|||
color: #c8c8c8; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,141 @@ |
|||
<script> |
|||
import { goto } from "@roxi/routify" |
|||
import { |
|||
Layout, |
|||
Page, |
|||
notifications, |
|||
Button, |
|||
Heading, |
|||
Body, |
|||
Modal, |
|||
Divider, |
|||
} from "@budibase/bbui" |
|||
import CreateAppModal from "components/start/CreateAppModal.svelte" |
|||
import TemplateDisplay from "components/common/TemplateDisplay.svelte" |
|||
import { onMount } from "svelte" |
|||
import { templates } from "stores/portal" |
|||
|
|||
let loaded = false |
|||
let template |
|||
let creationModal = false |
|||
let creatingApp = false |
|||
|
|||
const welcomeBody = |
|||
"Start from scratch or get a head start with one of our templates" |
|||
const createAppTitle = "Create new app" |
|||
const createAppButtonText = "Start from scratch" |
|||
|
|||
onMount(async () => { |
|||
try { |
|||
await templates.load() |
|||
if ($templates?.length === 0) { |
|||
notifications.error( |
|||
"There was a problem loading quick start templates." |
|||
) |
|||
} |
|||
} catch (error) { |
|||
notifications.error("Error loading apps and templates") |
|||
} |
|||
loaded = true |
|||
}) |
|||
|
|||
const initiateAppCreation = () => { |
|||
template = null |
|||
creationModal.show() |
|||
creatingApp = true |
|||
} |
|||
|
|||
const stopAppCreation = () => { |
|||
template = null |
|||
creatingApp = false |
|||
} |
|||
|
|||
const initiateAppImport = () => { |
|||
template = { fromFile: true } |
|||
creationModal.show() |
|||
creatingApp = true |
|||
} |
|||
</script> |
|||
|
|||
<Page wide> |
|||
<Layout noPadding gap="XL"> |
|||
<span> |
|||
<Button |
|||
primary |
|||
on:click={() => { |
|||
$goto("../") |
|||
}} |
|||
> |
|||
Back |
|||
</Button> |
|||
</span> |
|||
|
|||
<div class="title"> |
|||
<div class="welcome"> |
|||
<Layout noPadding gap="XS"> |
|||
<Heading size="M">{createAppTitle}</Heading> |
|||
<Body size="S"> |
|||
{welcomeBody} |
|||
</Body> |
|||
</Layout> |
|||
|
|||
<div class="buttons"> |
|||
<Button size="L" icon="Add" cta on:click={initiateAppCreation}> |
|||
{createAppButtonText} |
|||
</Button> |
|||
<Button |
|||
icon="Import" |
|||
size="L" |
|||
quiet |
|||
secondary |
|||
on:click={initiateAppImport} |
|||
> |
|||
Import app |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
<Divider size="S" /> |
|||
</div> |
|||
|
|||
{#if loaded && $templates?.length} |
|||
<TemplateDisplay templates={$templates} /> |
|||
{/if} |
|||
</Layout> |
|||
</Page> |
|||
|
|||
<Modal |
|||
bind:this={creationModal} |
|||
padding={false} |
|||
width="600px" |
|||
on:hide={stopAppCreation} |
|||
> |
|||
<CreateAppModal {template} /> |
|||
</Modal> |
|||
|
|||
<style> |
|||
.title .welcome > .buttons { |
|||
padding-top: 30px; |
|||
} |
|||
.title { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
gap: var(--spacing-xl); |
|||
flex-wrap: wrap; |
|||
} |
|||
.buttons { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
gap: var(--spacing-xl); |
|||
flex-wrap: wrap; |
|||
} |
|||
@media (max-width: 640px) { |
|||
.buttons { |
|||
flex-direction: row-reverse; |
|||
justify-content: flex-end; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,41 @@ |
|||
<script> |
|||
import { goto } from "@roxi/routify" |
|||
import { Layout, Page, notifications, Button } from "@budibase/bbui" |
|||
import TemplateDisplay from "components/common/TemplateDisplay.svelte" |
|||
import { onMount } from "svelte" |
|||
import { templates } from "stores/portal" |
|||
|
|||
let loaded = false |
|||
|
|||
onMount(async () => { |
|||
try { |
|||
await templates.load() |
|||
if ($templates?.length === 0) { |
|||
notifications.error( |
|||
"There was a problem loading quick start templates." |
|||
) |
|||
} |
|||
} catch (error) { |
|||
notifications.error("Error loading apps and templates") |
|||
} |
|||
loaded = true |
|||
}) |
|||
</script> |
|||
|
|||
<Page wide> |
|||
<Layout noPadding gap="XL"> |
|||
<span> |
|||
<Button |
|||
primary |
|||
on:click={() => { |
|||
$goto("../") |
|||
}} |
|||
> |
|||
Back |
|||
</Button> |
|||
</span> |
|||
{#if loaded && $templates?.length} |
|||
<TemplateDisplay templates={$templates} /> |
|||
{/if} |
|||
</Layout> |
|||
</Page> |
|||
Loading…
Reference in new issue