-
Notifications
You must be signed in to change notification settings - Fork 2
Ben
Learning goals
- Learning and using Svelte to build applications
- Learning and using Typescript to build applications
- How to progressively enhance an application with a framework
- Learn about Supabase
- Collaborating in a development team
- Create applications that provide a pleasurable experience for the user
- Test effectively with the users (testing, setting up the test,)
- Having clear and effective communication with a client
In week 1 we started off the the week with a kickoff with Tessa. Tessa is an alumni from Communication and & Multimediadesign and the concept for Wat Zegt Deze Brief was her graduation project. During the kickoff she explained what the concept, we discussed what we were expecting from each other and she provided us with the designs for the application. The concept of Wat Zegt Deze Brief was especially interesting to me because my mothers fits in the target group of the application and I've experienced how frustrating it can be for her when she receives letters but hardly understands what is written in it because she is low-literate in Dutch. Me and my sister will most of the time explain letters for her and I think the concept of Wat Zegt Deze Brief really could help these people.
After the kickoff I wrote the problem statement, design challenge, client description, description of the assignment and stakeholders definition for the debriefing.
For this concept we are going to make a Web Application with Svelte-kit, Typescript and Supabase. I've never worked with this tech before so in the first week I felt it was important for myself to learn the basics of these tools. I started out with following the tutorials that are in the documentation of Svelte and Typescript. These gave me a basic idea of the core concepts in Svelte and Typescript. Since we are going to making components with the Atomic Design methodology, we started of the week by making reusable components. This was a perfect chance for me to apply the knowledge I've learned in the Svelte tutorial and get some hands-on experience. These are some of the components I made during the first week. A NavItem component and a Icon component
// Icon component
<script>
import Icon from './Icon.svelte'
export let route: string = '#'
</script>
<a href={route}>
<Icon>
<slot />
</Icon>
</a>
// NavItem component
<script>
export let color: string = ''
</script>
<style lang="scss">
div {
display: block;
width: 100%;
height: 100%;
position: relative;
:global(svg) {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
fill: var(--color);
}
}
</style>
<div style="--color: {color}">
<slot />
</div>
During the first week I started to get the basic concepts of Svelte, but I still felt I needed to learn a lot more so I could contribute more to the team. What really helped me understand the basics of Svelte was dabbling with it in my free time. I would follow the tutorials of The Net Ninja and the Svelte Official tutorial and try to understand each concept/technique of Svelte before I moved on to the next thing. During the workdays this came in really handy, because I was applying the knowledge I learned in my free time and was also getting more experience in Svelte.
These are some basic things I made in my free time following official Svelte Tutorial and the Net Ninja tutorial:
<script>
let name;
let beltColor;
let age;
let skills = [];
function handleSubmit() {
console.log(name, beltColor, age, skills)
}
</script>
<form on:submit|preventDefault={handleSubmit}>
<input type="text" placeholder="name" bind:value={name}>
<input type="text" placeholder="belt color" bind:value={beltColor}>
<input type="number" placeholder="age" bind:value={age}>
<label>Skills:</label>
<input type="checkbox" bind:group={skills} value="fighting">fighting<br>
<input type="checkbox" bind:group={skills} value="sneaking">sneaking<br>
<input type="checkbox" bind:group={skills} value="running">running<br>
<button>Add person</button>
</form>
At the beginning of the week we had a clear view of what we had to do this week. Due to creating multiple issues for several features and diving these between the teammates we all knew what we had to do on Monday. This week the first thing I would be doing was help setting up the process of submitting a single page for a user. When this feature was done we would improve this feature by making it possible to upload multiple pages. The first thing I did was create an image preview for the users to see the page that they are uploading.
<script>
import type { InputType } from '$types'
import { getContext, setContext } from 'svelte'
import type { Writable } from 'svelte/store'
export let value = ''
export let type: InputType = 'text'
export let name = ''
const pageStore = getContext<Writable<string | ArrayBuffer>>('previews')
const onFileSelected = e => {
let image = e.target.files[0]
let reader = new FileReader()
reader.readAsDataURL(image)
reader.onload = e => {
let page = e.target.result
pageStore.set(page)
}
}
</script>
<style>
</style>
<input {value} {name} {type} {...$$restProps} />
{#if type === 'file'}
<input on:change={e => onFileSelected(e)} {value} {name} {type} {...$$restProps} />
{:else}
<input {value} {name} {type} {...$$restProps} />
{/if}
Using Svelte context I managed to 'store' the file of the input field and use context to pass this file to another component. Initially I had the idea to use Svelte Store for this but after asking some advice from Jonah he advised me to use context instead.
When the feature was done I had to enhance it to show multiple previews of the pages that were uploaded. To do this I decided to create a Carousel component. I never made a carousel so I was very excited to try it out.
<script>
import { Icon } from '$atoms'
import { Image } from '$atoms'
import { getContext } from 'svelte'
import type { Writable } from 'svelte/store'
const pageStore = getContext<Writable<any>>('previews')
let index: number = 0
const next = () => {
index = (index + 1) % $pageStore.length
}
</script>
{#if $pageStore}
<section>
<button on:click={next} id="previous"><Icon>></Icon></button>
<div>
{#each [$pageStore[index]] as page (index)}
<Image src={page} alt="Page preview" />
{/each}
</div>
<button on:click={next} id="previous"><Icon><</Icon></button>
</section>
{/if}
At the end of the week I was pretty satisfied with what I had made, considering I don't have any experience with Svelte. Although it felt challenging I felt like I wasn't getting challenged enough. On the Friday retrospective session I indicated that I would like to pick up some issues that were more challenging and it was well received. We decided as a team that I would be developing the chat feature in the coming weeks. Even though it is quite a challenging issue I was very excited about getting to develop the chat feature in the coming weeks.
Week 3 started and it was time for me to start working on quite a big feature. I'm going to develop the chat feature. Users can chat with each other through voice messages so a volunteer can explain the letter. The first challenge for me was to create the feature of sending voice messages. The first thing I did was do some research on voice communication and looking what technology would be the best for my use case. During my research I found the MediaStream Recording API which makes it possible to record media and store it in a file. To get a basic idea of the functionality of this API I first started dabbling around with it and reading up the documentation on it. After I had a good grasp on how this API works I started developing the feature to be able to record voice messages.
// VoiceRecorder component
<script>
import { createEventDispatcher } from 'svelte'
import { IconButton } from '$atoms'
import { RecordIcon, StopRecordingIcon } from '$icons'
export let recorder: MediaRecorder
let chunks: any[] = []
let clicked = false
const dispatch = createEventDispatcher()
function recordMedia() {
clicked = !clicked
recorder.start()
recorder.ondataavailable = (e: BlobEvent) => {
chunks.push(e.data)
}
}
function stopMedia() {
clicked = !clicked
recorder.stop()
recorder.onstop = () => {
let blob = new Blob(chunks, { type: 'audio/ogg; codecs=opus' })
chunks = []
const file = new File([blob], 'message.ogg', { type: 'audio/ogg; codecs=opus' })
dispatch('message', file)
}
}
</script>
{#if clicked}
<IconButton on:click={stopMedia}><StopRecordingIcon /></IconButton>
{:else}
<IconButton on:click={recordMedia}><RecordIcon /></IconButton>
{/if}
// Uploading the message to Supabase
async function messageHandler(e: any) {
const message: File = e.detail
if (!message) return
const id = uuid()
await client.storage.from('messages').upload(`${id}`, message)
}
Honestly I had a lot of fun doing this feature and I learned a lot about MediStream API and how to get it to work with Svelte. Even though I had a lot of fun doing this feature I was still a bit dissatisfied at the end of the week because it was the only feature I was able to finish during this sprint. My teammates however said that I shouldn't be because it was quite a big feature where I had to learn a lot of new things. I think I was a bit too ambitious at the beginning of the week thinking that I could finish the whole chat feature in 5 days. In hindsight it also made a lot of sense because I have to learn all these new things to create the feature I made during this week.
In the week prior to this week I finished the feature of recording voice messages and storing these in the Supabase. Now it was time to actually create the chat feature so users can communicate with each other. To make this feature work I had to do a lot with Supabase to make the chat real-time.
The overall flow of making the chat work is to fetch the messages that are bound to a letter from Supabase and pass these voice messages to the Chat component as a prop so the voice messages can be played in the audio elements.
<script context="module">
export const load: Load = async ({ page }) => {
const messages = await listMessages(page.params.id)
if (!messages) return {}
const { body: isUserRole } = await client.rpc<boolean>('is_role', {
user_id: client.auth.session().user.id,
u_role: 'user',
})
return {
props: {
messages,
letterId: page.params.id,
userRole: (isUserRole as unknown as boolean) ? 'user' : 'volunteer',
},
}
}
</script>
<script>
import { Chat } from '$templates'
import { listMessages, downloadAudioMessage, fetchMessage } from '$db/messages'
import type { Load } from '@sveltejs/kit'
import type { ChatMessage, definitions } from '$types'
import { client } from '$config/supabase'
import { onMount } from 'svelte'
export let userRole: string
export let messages: ChatMessage[]
export let letterId: string
onMount(async () => {
messages = (await Promise.all(
messages.map(message =>
message.type === 'audio'
? downloadAudioMessage(letterId, message.sender.id, message.content).then(file => ({
...message,
file,
}))
: new Promise(resolve => resolve(message))
)
)) as ChatMessage[]
client
.from<definitions['letters']>(`letters:id=eq.${letterId}`)
.on('UPDATE', async payload => {
const messageID = payload.new.messages[payload.new.messages.length - 1]
const message = await fetchMessage(messageID)
if (message.type !== 'audio') {
messages.push(message)
messages = messages
return
}
const file = await downloadAudioMessage(letterId, message.sender.id, message.content)
messages.push({
...message,
file,
})
messages = messages
})
.subscribe()
})
</script>
<Chat {messages} {userRole} />
When the messages are fetched from Supabase and passed as a prop the to Chat component these messages can now be rendered by looping over each message in the array and assigning the data url as a src in the audio element. To make the chat real-time I used the .on('UPDATE')
subscribe function from Supabase to pass the message as a prop each time a new message is sent to the user.
<script>
import Header from './Header.svelte'
import type { ChatMessage } from '$types'
import { SpokenText, Help, Back, MessageCloud } from '$atoms'
import { RecordAudio } from '$molecules'
import { browser } from '$app/env'
import { messageHandler } from '$db/messageHandler'
import { client } from '$config/supabase'
export let messages: ChatMessage[]
export let userRole: string
const userId = client.auth.session().user.id
let recorder: MediaRecorder
if (browser) {
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
recorder = new MediaRecorder(stream)
})
}
</script>
<style>
footer {
display: flex;
flex-direction: column;
align-items: center;
box-shadow: var(--bs-up);
padding-top: var(--space-m);
width: 100%;
overflow-x: scroll;
}
main {
display: flex;
flex-direction: column;
overflow: auto;
}
audio {
margin: 1rem 0 1rem 0;
}
.you {
align-self: flex-end;
}
</style>
<Header>
<Back slot="left" href="/dashboard" />
<SpokenText --align="center" slot="middle" text="Gesproken bericht" />
<Help slot="right" />
</Header>
<main>
{#each messages as message (message.id)}
{#if message.sender.id === userId}
<audio controls src={message.file} type="audio/ogg" class="you" />
{:else}
<audio controls src={message.file} type="audio/ogg" />
{/if}
{/each}
</main>
<footer>
{#if userRole === 'user'}
<SpokenText
--align="center"
text="Klik op de microfoon en stel nog een vraag of bedank de vrijwilliger"
/>
{/if}
<RecordAudio {recorder} on:message={messageHandler} />
</footer>
This week was at the same time the most frustrating and the most fun out of all the weeks for me. I got stuck for hours at sometimes and I really had to think a lot about the flow of creating a creating a chat feature. The first mistake I made was jumping right in and start coding. Luckily I got stuck a lot and it made me realize that it is smarter to first think about the flow and processes of a feature, start writing pseudo-code and then actually start coding. It won't alway work but atleast I'm tackling the problem with a blueprint of how to do things in my head.
Looking back at where I was as a developer at the beginning of the project and where I'm now, I think I made really big improvements as a developer. During this project I learned the most out of all the projects I had ever done and I also had the most fun time doing it. This project was also particularly interesting to me because the concept solves a problem I see my mother have problems with on a daily basis. It really gave me the feeling of developing an application that actually helps people and with that in mind it made the process all the more fun, knowing how this platform can really help people.
Taking a look at my learning goals I feel that on the technical side I've accomplished them all. At the start of the project I wasn't familiar with Svelte, Typescript and Supabase at all. By doing research on these technologies in my free time and applying the knowledge I learned in the project I feel I really made steps as a developer. During the process of this project I also realized how important reading documentation is and how to use the documentation as a guideline to solve the problems for my usecase.
When I look back at all the knowledge I've gained during the various courses of the minor, I feel like I have utilised them all. We wanted to make the application Progressively Enhanced and applied the knowledge I learned during Browser Technologies to do this. To see if the current application was actually helping the user we tested with users that belong to the target group and iterated on the results of the tests (Human Centered Design). To make the chat real-time we worked with real-time data and a real-time database (Real-time Web). During the development of this application I particularly learned a lot about using real-time databases and how you can use it to create real-time features. During the course of this project we also applied all of the knowledge we've learned in CSS to the Rescue by styling the application according to the design of the client. Finally during Web App From Scratch and Progressive Web Apps we learned how to render pages client-side and server side. The knowledge I gained during this course really helped me with understanding the concept of server-side rendering and how we applied that to our project by using a framework like Svelte-kit.
In conclusion I'm really proud of the product we've made and it also gave me extra motivation to start working on some side projects in the summer to apply the knowledge I've gained in this project and build out some new cool projects I can be proud of. Looking back at my level at the start of the minor and where I am now it feels like a world of difference. This minor course was the most fun I had during my time as a student and I'm happy with all the new things I've learned during this course.