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