Initial homepage and feed up and running
This commit is contained in:
parent
0c01712341
commit
2a8571d6dd
19
.idea/dataSources.xml
Normal file
19
.idea/dataSources.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="www_1@db03.millslan.net" uuid="1002a9b7-c04c-43d0-bbbf-bd2c4fc7b6a9">
|
||||||
|
<driver-ref>postgresql</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:postgresql://db03.millslan.net:5432/www_1</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
<data-source source="LOCAL" name="@db03.millslan.net" uuid="afe39e7a-503d-4718-abe7-8bfb0428771b">
|
||||||
|
<driver-ref>mongo</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>com.dbschema.MongoJdbcDriver</jdbc-driver>
|
||||||
|
<jdbc-url>mongodb://db03.millslan.net:27017</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
@ -1,7 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module type="WEB_MODULE" version="4">
|
<module type="WEB_MODULE" version="4">
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$" />
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/exbuild" />
|
||||||
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
<orderEntry type="module" module-name="di" />
|
<orderEntry type="module" module-name="di" />
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
"lib": "lib"
|
"lib": "lib"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@extollo/lib": "^0.9.0",
|
"@extollo/lib": "^0.9.1",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"ts-expose-internals": "^4.5.4",
|
"ts-expose-internals": "^4.5.4",
|
||||||
|
@ -2,7 +2,7 @@ lockfileVersion: 5.3
|
|||||||
|
|
||||||
specifiers:
|
specifiers:
|
||||||
'@extollo/cc': ^0.6.0
|
'@extollo/cc': ^0.6.0
|
||||||
'@extollo/lib': ^0.9.0
|
'@extollo/lib': ^0.9.1
|
||||||
copyfiles: ^2.4.1
|
copyfiles: ^2.4.1
|
||||||
rimraf: ^3.0.2
|
rimraf: ^3.0.2
|
||||||
ts-expose-internals: ^4.5.4
|
ts-expose-internals: ^4.5.4
|
||||||
@ -12,7 +12,7 @@ specifiers:
|
|||||||
zod: ^3.11.6
|
zod: ^3.11.6
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@extollo/lib': 0.9.0
|
'@extollo/lib': 0.9.1
|
||||||
copyfiles: 2.4.1
|
copyfiles: 2.4.1
|
||||||
rimraf: 3.0.2
|
rimraf: 3.0.2
|
||||||
ts-expose-internals: 4.5.4
|
ts-expose-internals: 4.5.4
|
||||||
@ -110,8 +110,8 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@extollo/lib/0.9.0:
|
/@extollo/lib/0.9.1:
|
||||||
resolution: {integrity: sha512-Gu9qwjwjDWPfXrp1ThyjFIROWXD8jXqwz4c7JxjWoWbGnY0dLGAZHv6MOF1tqNGU9zTW399jpGS/7lyazApMpQ==}
|
resolution: {integrity: sha512-HPYwlKO0SHZsTQBQh5zTA8oErIEncdFWKr3LGs3LmL7Dq8rWN7dQ1qK2qk809Fc7FBqWeiZKuOsRA8yLkhr6Kg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atao60/fse-cli': 0.1.7
|
'@atao60/fse-cli': 0.1.7
|
||||||
'@extollo/ui': 0.1.0_@types+node@14.18.12
|
'@extollo/ui': 0.1.0_@types+node@14.18.12
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { env } from '@extollo/lib'
|
import { env } from '@extollo/lib'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: env('APP_NAME', 'Extollo'),
|
name: env('APP_NAME', 'Garrett Mills'),
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { env } from "@extollo/lib";
|
|||||||
export default {
|
export default {
|
||||||
connections: {
|
connections: {
|
||||||
default: {
|
default: {
|
||||||
user: env('DATABASE_USERNAME', 'extollo'),
|
user: env('DATABASE_USERNAME', 'www'),
|
||||||
password: env('DATABASE_PASSWORD'),
|
password: env('DATABASE_PASSWORD'),
|
||||||
host: env('DATABASE_HOST', 'localhost'),
|
host: env('DATABASE_HOST', 'localhost'),
|
||||||
port: env('DATABASE_PORT', 5432),
|
port: env('DATABASE_PORT', 5432),
|
||||||
|
59
src/app/http/controllers/Home.controller.ts
Normal file
59
src/app/http/controllers/Home.controller.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import {Controller, view, Injectable, SecurityContext, Inject, Collection} from '@extollo/lib'
|
||||||
|
import {WorkItem} from '../../models/WorkItem.model'
|
||||||
|
import {FeedPost} from '../../models/FeedPost.model'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class Home extends Controller {
|
||||||
|
@Inject()
|
||||||
|
protected readonly security!: SecurityContext
|
||||||
|
|
||||||
|
public async welcome() {
|
||||||
|
const workItems = await this.getWorkItems()
|
||||||
|
const feedPosts = await this.getFeedPosts()
|
||||||
|
|
||||||
|
return view('welcome', {
|
||||||
|
feedPosts: feedPosts.toArray(),
|
||||||
|
workItemGroups: workItems.groupBy(item => item.startDate.getFullYear()),
|
||||||
|
workItemYears: workItems.map(item => item.startDate.getFullYear())
|
||||||
|
.unique()
|
||||||
|
.toArray(),
|
||||||
|
workItemHiddenYears: workItems.filter(item => item.endDate)
|
||||||
|
.map(item => item.startDate.getFullYear())
|
||||||
|
.unique()
|
||||||
|
.toArray()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async feed() {
|
||||||
|
const feedPosts = await this.getFeedPosts(true)
|
||||||
|
return view('feed', {
|
||||||
|
feedPosts: feedPosts.toArray(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getFeedPosts(all = false): Promise<Collection<FeedPost>> {
|
||||||
|
const query = FeedPost.query<FeedPost>()
|
||||||
|
.orderByDescending('posted_at')
|
||||||
|
|
||||||
|
if ( !all ) {
|
||||||
|
query.limit(6)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !this.security.hasUser() ) {
|
||||||
|
query.where('visible', '=', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.get().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getWorkItems(): Promise<Collection<WorkItem>> {
|
||||||
|
const query = WorkItem.query<WorkItem>()
|
||||||
|
.orderByDescending('start_date')
|
||||||
|
|
||||||
|
if ( !this.security.hasUser() ) {
|
||||||
|
query.where('visible', '=', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.get().collect()
|
||||||
|
}
|
||||||
|
}
|
@ -1,26 +0,0 @@
|
|||||||
import {Controller, view, Session, Inject, Injectable, Locale, Validator} from '@extollo/lib'
|
|
||||||
import {UserLogin} from "../../../types/UserLogin";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class Home extends Controller {
|
|
||||||
@Inject()
|
|
||||||
protected readonly session!: Session;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
protected readonly locale!: Locale;
|
|
||||||
|
|
||||||
public welcome() {
|
|
||||||
this.session.set('app_visits', this.session.get('app_visits', 0) + 1)
|
|
||||||
|
|
||||||
const valid = new Promise<UserLogin>(() => {})
|
|
||||||
|
|
||||||
return view('@extollo:welcome', {
|
|
||||||
app_visits: this.session.get('app_visits'),
|
|
||||||
locale: this.locale.helper(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public test() {
|
|
||||||
return new Validator<UserLogin>()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +1,17 @@
|
|||||||
import {Route, SessionAuthMiddleware, AuthRequiredMiddleware} from '@extollo/lib'
|
import {Route, SessionAuthMiddleware} from '@extollo/lib'
|
||||||
import {Home} from '../controllers/main/Home.controller'
|
import {Home} from '../controllers/Home.controller'
|
||||||
|
|
||||||
Route.group('/', () => {
|
Route.group('/', () => {
|
||||||
Route.get('/')
|
Route.get('/')
|
||||||
.calls<Home>(Home, home => home.welcome)
|
.calls<Home>(Home, home => home.welcome)
|
||||||
})
|
|
||||||
|
|
||||||
Route.group('', () => {
|
Route.get('/feed')
|
||||||
Route.get('/dash')
|
.calls<Home>(Home, home => home.feed)
|
||||||
.pre(AuthRequiredMiddleware)
|
.alias('feed')
|
||||||
.calls<Home>(Home, home => home.welcome)
|
|
||||||
}).pre(SessionAuthMiddleware)
|
}).pre(SessionAuthMiddleware)
|
||||||
|
|
||||||
|
// Route.group('', () => {
|
||||||
|
// Route.get('/dash')
|
||||||
|
// .pre(AuthRequiredMiddleware)
|
||||||
|
// .calls<Home>(Home, home => home.welcome)
|
||||||
|
// }).pre(SessionAuthMiddleware)
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
import {DatabaseService, FieldType, Inject, Injectable, Migration} from '@extollo/lib'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreateWorkItemsTableMigration
|
||||||
|
* ----------------------------------
|
||||||
|
* Create the work_items table to track project history.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export default class CreateWorkItemsTableMigration extends Migration {
|
||||||
|
@Inject()
|
||||||
|
protected readonly db!: DatabaseService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the migration.
|
||||||
|
*/
|
||||||
|
async up(): Promise<void> {
|
||||||
|
const schema = this.db.get().schema()
|
||||||
|
const table = await schema.table('work_items')
|
||||||
|
|
||||||
|
table.primaryKey('work_item_id').required()
|
||||||
|
|
||||||
|
table.column('visible')
|
||||||
|
.type(FieldType.bool)
|
||||||
|
.required()
|
||||||
|
.default(false)
|
||||||
|
|
||||||
|
table.column('name')
|
||||||
|
.type(FieldType.varchar)
|
||||||
|
.required()
|
||||||
|
|
||||||
|
table.column('description')
|
||||||
|
.type(FieldType.text)
|
||||||
|
.required()
|
||||||
|
.default('')
|
||||||
|
|
||||||
|
table.column('start_date')
|
||||||
|
.type(FieldType.timestamp)
|
||||||
|
.required()
|
||||||
|
|
||||||
|
table.column('end_date')
|
||||||
|
.type(FieldType.timestamp)
|
||||||
|
.nullable()
|
||||||
|
|
||||||
|
await schema.commit(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undo the migration.
|
||||||
|
*/
|
||||||
|
async down(): Promise<void> {
|
||||||
|
const schema = this.db.get().schema()
|
||||||
|
const table = await schema.table('work_items')
|
||||||
|
|
||||||
|
table.dropIfExists()
|
||||||
|
|
||||||
|
await schema.commit(table)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
import {DatabaseService, FieldType, Inject, Injectable, Migration, raw} from '@extollo/lib'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreateFeedPostsTableMigration
|
||||||
|
* ----------------------------------
|
||||||
|
* Create the table to hold posts to the "updates" feed.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export default class CreateFeedPostsTableMigration extends Migration {
|
||||||
|
@Inject()
|
||||||
|
protected readonly db!: DatabaseService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the migration.
|
||||||
|
*/
|
||||||
|
async up(): Promise<void> {
|
||||||
|
const schema = this.db.get().schema()
|
||||||
|
const table = await schema.table('feed_posts')
|
||||||
|
|
||||||
|
table.primaryKey('feed_post_id', FieldType.varchar)
|
||||||
|
.required()
|
||||||
|
|
||||||
|
// date, tag, text, visible
|
||||||
|
|
||||||
|
table.column('posted_at')
|
||||||
|
.type(FieldType.timestamp)
|
||||||
|
.default(raw('NOW()'))
|
||||||
|
.required()
|
||||||
|
|
||||||
|
table.column('tag')
|
||||||
|
.type(FieldType.varchar)
|
||||||
|
.required()
|
||||||
|
|
||||||
|
table.column('visible')
|
||||||
|
.type(FieldType.bool)
|
||||||
|
.default(false)
|
||||||
|
.required()
|
||||||
|
|
||||||
|
table.column('body')
|
||||||
|
.type(FieldType.text)
|
||||||
|
.default('')
|
||||||
|
.required()
|
||||||
|
|
||||||
|
await schema.commit(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undo the migration.
|
||||||
|
*/
|
||||||
|
async down(): Promise<void> {
|
||||||
|
const schema = this.db.get().schema()
|
||||||
|
const table = await schema.table('feed_posts')
|
||||||
|
|
||||||
|
table.dropIfExists()
|
||||||
|
|
||||||
|
await schema.commit(table)
|
||||||
|
}
|
||||||
|
}
|
21
src/app/models/FeedPost.model.ts
Normal file
21
src/app/models/FeedPost.model.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {Field, FieldType, Model} from '@extollo/lib'
|
||||||
|
|
||||||
|
export class FeedPost extends Model<FeedPost> {
|
||||||
|
protected static table = 'feed_posts'
|
||||||
|
protected static key = 'feed_post_id'
|
||||||
|
|
||||||
|
@Field(FieldType.varchar, 'feed_post_id')
|
||||||
|
public readonly feedPostId?: string
|
||||||
|
|
||||||
|
@Field(FieldType.timestamp, 'posted_at')
|
||||||
|
public postedAt: Date = new Date()
|
||||||
|
|
||||||
|
@Field(FieldType.varchar)
|
||||||
|
public tag!: string
|
||||||
|
|
||||||
|
@Field(FieldType.bool)
|
||||||
|
public visible = false
|
||||||
|
|
||||||
|
@Field(FieldType.text)
|
||||||
|
public body = ''
|
||||||
|
}
|
44
src/app/models/WorkItem.model.ts
Normal file
44
src/app/models/WorkItem.model.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import {Field, FieldType, Model} from '@extollo/lib'
|
||||||
|
|
||||||
|
export class WorkItem extends Model<WorkItem> {
|
||||||
|
protected static table = 'work_items'
|
||||||
|
protected static key = 'work_item_id'
|
||||||
|
|
||||||
|
@Field(FieldType.serial, 'work_item_id')
|
||||||
|
public readonly workItemId!: number
|
||||||
|
|
||||||
|
@Field(FieldType.bool)
|
||||||
|
public visible: boolean = false
|
||||||
|
|
||||||
|
@Field(FieldType.varchar)
|
||||||
|
public name!: string
|
||||||
|
|
||||||
|
@Field(FieldType.text)
|
||||||
|
public description: string = ''
|
||||||
|
|
||||||
|
@Field(FieldType.timestamp, 'start_date')
|
||||||
|
public startDate!: Date
|
||||||
|
|
||||||
|
@Field(FieldType.timestamp, 'end_date')
|
||||||
|
public endDate?: Date
|
||||||
|
|
||||||
|
public startDisplay(): string {
|
||||||
|
return `${this.getMonth(this.startDate.getMonth())} ${this.startDate.getFullYear()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
public endDisplay(): string {
|
||||||
|
if ( !this.endDate ) {
|
||||||
|
return 'Present'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${this.getMonth(this.endDate.getMonth())} ${this.endDate.getFullYear()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
public rangeDisplay(): string {
|
||||||
|
return `${this.startDisplay()} - ${this.endDisplay()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getMonth(index: number): string {
|
||||||
|
return ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][index]
|
||||||
|
}
|
||||||
|
}
|
BIN
src/app/resources/assets/ffox.png
Normal file
BIN
src/app/resources/assets/ffox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
src/app/resources/assets/font/fira/FiraCode-Bold.ttf
Normal file
BIN
src/app/resources/assets/font/fira/FiraCode-Bold.ttf
Normal file
Binary file not shown.
BIN
src/app/resources/assets/font/fira/FiraCode-Light.ttf
Normal file
BIN
src/app/resources/assets/font/fira/FiraCode-Light.ttf
Normal file
Binary file not shown.
BIN
src/app/resources/assets/font/fira/FiraCode-Medium.ttf
Normal file
BIN
src/app/resources/assets/font/fira/FiraCode-Medium.ttf
Normal file
Binary file not shown.
BIN
src/app/resources/assets/font/fira/FiraCode-Regular.ttf
Normal file
BIN
src/app/resources/assets/font/fira/FiraCode-Regular.ttf
Normal file
Binary file not shown.
BIN
src/app/resources/assets/font/fira/FiraCode-SemiBold.ttf
Normal file
BIN
src/app/resources/assets/font/fira/FiraCode-SemiBold.ttf
Normal file
Binary file not shown.
BIN
src/app/resources/assets/font/playfair/PlayfairDisplay-Black.ttf
Normal file
BIN
src/app/resources/assets/font/playfair/PlayfairDisplay-Black.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/app/resources/assets/font/playfair/PlayfairDisplay-Bold.ttf
Normal file
BIN
src/app/resources/assets/font/playfair/PlayfairDisplay-Bold.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/app/resources/assets/font/rajdhani/Rajdhani-Bold.ttf
Normal file
BIN
src/app/resources/assets/font/rajdhani/Rajdhani-Bold.ttf
Normal file
Binary file not shown.
BIN
src/app/resources/assets/font/rajdhani/Rajdhani-Light.ttf
Normal file
BIN
src/app/resources/assets/font/rajdhani/Rajdhani-Light.ttf
Normal file
Binary file not shown.
BIN
src/app/resources/assets/font/rajdhani/Rajdhani-Medium.ttf
Normal file
BIN
src/app/resources/assets/font/rajdhani/Rajdhani-Medium.ttf
Normal file
Binary file not shown.
BIN
src/app/resources/assets/font/rajdhani/Rajdhani-Regular.ttf
Normal file
BIN
src/app/resources/assets/font/rajdhani/Rajdhani-Regular.ttf
Normal file
Binary file not shown.
BIN
src/app/resources/assets/font/rajdhani/Rajdhani-SemiBold.ttf
Normal file
BIN
src/app/resources/assets/font/rajdhani/Rajdhani-SemiBold.ttf
Normal file
Binary file not shown.
BIN
src/app/resources/assets/img/profile.jpg
Normal file
BIN
src/app/resources/assets/img/profile.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
BIN
src/app/resources/assets/img/profile_wide.jpg
Normal file
BIN
src/app/resources/assets/img/profile_wide.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
src/app/resources/assets/img/profile_wide.jpg~
Normal file
BIN
src/app/resources/assets/img/profile_wide.jpg~
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
374
src/app/resources/assets/main.css
Normal file
374
src/app/resources/assets/main.css
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "Raj";
|
||||||
|
src: url("/assets/font/rajdhani/Rajdhani-Regular.ttf");
|
||||||
|
font-weight: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Raj";
|
||||||
|
src: url("/assets/font/rajdhani/Rajdhani-Bold.ttf");
|
||||||
|
font-weight: bold;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Raj";
|
||||||
|
src: url("/assets/font/rajdhani/Rajdhani-Light.ttf");
|
||||||
|
font-weight: 200;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Raj";
|
||||||
|
src: url("/assets/font/rajdhani/Rajdhani-Medium.ttf");
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Raj";
|
||||||
|
src: url("/assets/font/rajdhani/Rajdhani-SemiBold.ttf");
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: #5e6572;
|
||||||
|
color: #c9d4e2;
|
||||||
|
font-family: "Raj", sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 64pt;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 36pt;
|
||||||
|
padding-top: 70px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 24pt;
|
||||||
|
text-transform: lowercase;
|
||||||
|
margin: 0;
|
||||||
|
padding: 30px 0 0;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: 18pt;
|
||||||
|
margin: 0;
|
||||||
|
padding: 40px 0 0;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 18pt;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
font-size: 16pt;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-size: 12pt;
|
||||||
|
background: #3e4552;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.inner {
|
||||||
|
padding-bottom: 10%;
|
||||||
|
max-width: min(70%, 1000px);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.theme-hide {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
button,
|
||||||
|
a.button,
|
||||||
|
a.button-small {
|
||||||
|
border: 1px solid #3e4552;
|
||||||
|
font-family: "Raj", sans-serif;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 16pt;
|
||||||
|
padding: 5px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: background-color 0.1s linear;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #c9d4e2;
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
button:hover,
|
||||||
|
a.button:hover,
|
||||||
|
a.button-small:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background: #3e4552;
|
||||||
|
}
|
||||||
|
button:active,
|
||||||
|
a.button:active,
|
||||||
|
a.button-small:active {
|
||||||
|
background: #323742;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #c9d4e2;
|
||||||
|
}
|
||||||
|
a.button-small,
|
||||||
|
button.small {
|
||||||
|
padding: 1px 10px;
|
||||||
|
background: #c9d4e2;
|
||||||
|
border: none;
|
||||||
|
color: #3e4552;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
a.button-small:hover,
|
||||||
|
button.small:hover {
|
||||||
|
color: #c9d4e2;
|
||||||
|
background: #3e4552;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
font-size: 14pt;
|
||||||
|
font-weight: 300;
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
.form-group input,
|
||||||
|
.form-group textarea,
|
||||||
|
.form-group select {
|
||||||
|
border: 1px solid #3e4552;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 7px;
|
||||||
|
font-size: 14pt;
|
||||||
|
min-width: 400px;
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
color: #c9d4e2;
|
||||||
|
}
|
||||||
|
.form-group input::placeholder,
|
||||||
|
.form-group textarea::placeholder,
|
||||||
|
.form-group select::placeholder {
|
||||||
|
color: #c9d4e2;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
background: #3e4552;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
}
|
||||||
|
footer ul {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 50px 0 0;
|
||||||
|
}
|
||||||
|
footer ul li {
|
||||||
|
display: inline;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
footer ul li a {
|
||||||
|
color: #a9b4c2;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 16pt;
|
||||||
|
font-weight: 300;
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
footer ul li a:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
footer .col {
|
||||||
|
margin: 0 30px;
|
||||||
|
}
|
||||||
|
footer .by-line {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 36pt;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: right;
|
||||||
|
padding-top: 30px;
|
||||||
|
}
|
||||||
|
footer .copy {
|
||||||
|
font-size: 14pt;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
footer ul li {
|
||||||
|
display: block;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
footer #tagline:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
footer .auth-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
footer .auth-container .profile {
|
||||||
|
margin-right: 15px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.button-links {
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
.button-links a {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
justify-content: left;
|
||||||
|
padding: 20px;
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
.inner {
|
||||||
|
width: 100%;
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
.form-group input,
|
||||||
|
.form-group textarea,
|
||||||
|
.form-group select {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
footer .by-line {
|
||||||
|
text-align: left;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
footer .links {
|
||||||
|
padding: 20px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hero {
|
||||||
|
margin-top: 100px;
|
||||||
|
}
|
||||||
|
.hero h1 {
|
||||||
|
font-weight: 300;
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
.hero h2 {
|
||||||
|
font-size: 36pt;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: 300;
|
||||||
|
margin: 10px 0 0;
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
.hero p {
|
||||||
|
font-size: 20pt;
|
||||||
|
}
|
||||||
|
section#about .about-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
section#about .img {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
section#about .img img {
|
||||||
|
max-width: 300px;
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-right: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
section#work .timeline-group .timeline-year {
|
||||||
|
font-size: 18pt;
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
section#work .timeline-group .timeline-content {
|
||||||
|
background: #3e4552;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
section#work .timeline-group .timeline-content h3 {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
section#work .timeline-group .timeline-content p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16pt;
|
||||||
|
}
|
||||||
|
section#contact .contact-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
section#contact .contact-container .message {
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
section#recent .feed-item {
|
||||||
|
background: #3e4552;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
section#recent .feed-item .feed-category {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
section#recent .feed-item .feed-category .tag {
|
||||||
|
background: #5e6572;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 14pt;
|
||||||
|
}
|
||||||
|
section#recent .feed-item .bottom {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
section#recent .feed-item .bottom .stamp {
|
||||||
|
margin-left: 20px;
|
||||||
|
font-size: 14pt;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: right;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
section#recent a.button {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
section#auth {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
section#auth h3 {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.hero h1 {
|
||||||
|
font-size: 48pt;
|
||||||
|
}
|
||||||
|
section#about .about-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
section#work .timeline-content {
|
||||||
|
margin: 20px 0 !important;
|
||||||
|
}
|
||||||
|
section#contact .contact-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
section#contact .contact-container .message {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
section#recent .feed-item {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
section#recent a.button {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
section#recent .bottom {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
section#recent .bottom .stamp {
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
}
|
37
src/app/resources/assets/welcome.js
Normal file
37
src/app/resources/assets/welcome.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
window.glmdev = window.glmdev || {}
|
||||||
|
|
||||||
|
window.glmdev.taglines = [
|
||||||
|
'...is proud that this site is Google-free',
|
||||||
|
'...is a supporter of FLOSS',
|
||||||
|
'...prefers JavaScript, but Python will do',
|
||||||
|
'...occasionally rants about dependency injection',
|
||||||
|
'...loves GNU/Linux',
|
||||||
|
'(or as I\'ve recently taken to calling it, GNU+Linux)',
|
||||||
|
'...uses spaces, not tabs',
|
||||||
|
'...self-hosts all the things',
|
||||||
|
'...occasionally does contract work',
|
||||||
|
'...reads the terms and conditions',
|
||||||
|
'...enjoys r/sysadmin',
|
||||||
|
'...just lost the game',
|
||||||
|
'...is running out of tag-line ideas',
|
||||||
|
`copyright © ${(new Date()).getFullYear()} garrett mills`,
|
||||||
|
]
|
||||||
|
|
||||||
|
document.querySelector('#tagline')
|
||||||
|
.addEventListener('click', event => {
|
||||||
|
if ( typeof glmdev.tagline_index === 'undefined' ) glmdev.tagline_index = 0
|
||||||
|
else if ( glmdev.tagline_index === glmdev.taglines.length - 1 ) glmdev.tagline_index = 0
|
||||||
|
else glmdev.tagline_index += 1
|
||||||
|
|
||||||
|
document.querySelector('#tagline').innerHTML = glmdev.taglines[glmdev.tagline_index]
|
||||||
|
})
|
||||||
|
|
||||||
|
document.querySelector('#timeline-view-all')
|
||||||
|
.addEventListener('click', event => {
|
||||||
|
const hidden = document.querySelectorAll('.work-container.theme-hide')
|
||||||
|
for ( const item of hidden ) {
|
||||||
|
item.classList.remove('theme-hide')
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('#timeline-view-all').classList.add('theme-hide')
|
||||||
|
}, false)
|
23
src/app/resources/views/feed.pug
Normal file
23
src/app/resources/views/feed.pug
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
extends template_raj
|
||||||
|
|
||||||
|
block content
|
||||||
|
.container#top
|
||||||
|
.inner
|
||||||
|
.hero
|
||||||
|
h1 garrettmills
|
||||||
|
section#recent
|
||||||
|
h2 posts & updates
|
||||||
|
.button-links
|
||||||
|
a.button-small(href='/feed/rss.xml') rss
|
||||||
|
a.button-small(href='/feed/atom.xml') atom
|
||||||
|
a.button-small(href='/feed/json.json') json
|
||||||
|
each item in feedPosts
|
||||||
|
.feed-item
|
||||||
|
.feed-category(id=item.feedPostId)
|
||||||
|
.tag #{item.tag}
|
||||||
|
span
|
||||||
|
if !item.visible
|
||||||
|
p.text (draft)
|
||||||
|
p.text !{item.body}
|
||||||
|
.bottom
|
||||||
|
.stamp <a href="#{named('feed')}##{item.feedPostId}" class="feed-edit-button">permalink</a> | #{item.postedAt.toLocaleString()}
|
65
src/app/resources/views/template_raj.pug
Normal file
65
src/app/resources/views/template_raj.pug
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
doctype html
|
||||||
|
head
|
||||||
|
block meta
|
||||||
|
meta(charset='utf-8')
|
||||||
|
meta(name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no')
|
||||||
|
meta(http-equiv='x-ua-compatible' content='ie=edge')
|
||||||
|
meta(name='description' content='Hi, there! My name is Garrett. I am a self-taught software developer and speaker.')
|
||||||
|
meta(name='keywords' content='garrett mills glmdev developer speaker flitter extollo student')
|
||||||
|
meta(name='author' content=config('app.name', 'Garrett Mills'))
|
||||||
|
meta(name='robots' content='index, follow')
|
||||||
|
|
||||||
|
block title
|
||||||
|
title #{config('app.name', 'Garrett Mills')} | Developer, Speaker, Designer
|
||||||
|
|
||||||
|
block style
|
||||||
|
link(rel='stylesheet' href=asset('main.css'))
|
||||||
|
|
||||||
|
link(rel='author' href='humans.txt')
|
||||||
|
link(rel="alternate" href="/feed/atom.xml" title="Garrett Mills - Posts & Updates" type="application/atom+xml")
|
||||||
|
link(rel="alternate" href="/feed/rss.xml" title="Garrett Mills - Posts & Updates" type="application/rss+xml")
|
||||||
|
|
||||||
|
body
|
||||||
|
block content
|
||||||
|
|
||||||
|
footer
|
||||||
|
.ff-tag(style="margin-right: 80px")
|
||||||
|
a(href="https://www.mozilla.org/en-US/firefox/browsers/" target="_blank")
|
||||||
|
img(src="/assets/ffox.png" width="75" style="margin-top: -1px")
|
||||||
|
.by-line garrettmills
|
||||||
|
.copy#tagline(title="my, aren't you curious...") copyright © #{(new Date()).getFullYear()} garrett mills
|
||||||
|
if false
|
||||||
|
div.auth-container
|
||||||
|
div.profile
|
||||||
|
img(src=user.profile_url width=50 style="border-radius: 50%")
|
||||||
|
p Welcome, #{user.first_name} #{user.last_name} | <a href="/auth/logout">Log out</a>
|
||||||
|
.col
|
||||||
|
ul.links
|
||||||
|
li
|
||||||
|
a(href='/#home') home
|
||||||
|
li
|
||||||
|
a(href='/#about') about me
|
||||||
|
li
|
||||||
|
a(href='/#work') what I'm working on
|
||||||
|
.col
|
||||||
|
ul.links
|
||||||
|
li
|
||||||
|
a(href='/#contact') get in touch
|
||||||
|
li
|
||||||
|
a(href='/#recent') latest updates
|
||||||
|
li
|
||||||
|
a(href='https://static.garrettmills.dev/Resume.pdf' target='_blank') résumé
|
||||||
|
.col
|
||||||
|
ul.links
|
||||||
|
li
|
||||||
|
a(href='/blog') blog
|
||||||
|
li
|
||||||
|
a(href='https://code.garrettmills.dev/garrettmills' target='_blank') my code
|
||||||
|
li
|
||||||
|
a(href='/technical') technical info
|
||||||
|
if false
|
||||||
|
li
|
||||||
|
a(href='/dash/main') dashboard
|
||||||
|
|
||||||
|
block script
|
||||||
|
script(src=asset('welcome.js'))
|
@ -1,6 +1,85 @@
|
|||||||
html
|
extends template_raj
|
||||||
head
|
block content
|
||||||
title !{locale('app_name')}
|
.container#home
|
||||||
body
|
.inner
|
||||||
h1 !{locale('welcome_to_extollo')}
|
.hero
|
||||||
h2 !{locale('viewed_page_num_times', { interp: { num: app_visits } })}
|
h1 garrettmills
|
||||||
|
p Hi, there. My name is Garrett, and I'm a self-taught software-developer and speaker.
|
||||||
|
section#about
|
||||||
|
h2 about me
|
||||||
|
.about-container
|
||||||
|
.img
|
||||||
|
img(src="/assets/img/profile.jpg" width=300 height=300)
|
||||||
|
.about
|
||||||
|
p Hi! My name is Garrett. Welcome to my corner of the internet. I'm a self-taught developer and maker. I like to build software to improve the developer/user experience. I created the <a href="https://extollo.garrettmills.dev" target="_blank">Extollo</a> framework, <a href="https://code.garrettmills.dev/Noded" target="_blank">Noded</a>, <a href="https://code.garrettmills.dev/starship/coreid" target="_blank">CoreID</a> authentication server, and a couple other projects. I love to communicate my work, and help others pursue their projects. I write blog posts, create video tutorials, hold talks, and publish code from my projects in the hope that others will find it useful.
|
||||||
|
p A bit more background: I grew up in the rural mid-west, and I got started by teaching myself everything I know. I'm a big fan of learning to code this way. I'm currently studying computer science at the University of Kansas.
|
||||||
|
|
||||||
|
if workItemYears && workItemYears.length
|
||||||
|
section#work
|
||||||
|
h2 what I'm working on
|
||||||
|
.timeline-group
|
||||||
|
each year in workItemYears
|
||||||
|
.work-container(class=(workItemHiddenYears.includes(year) ? 'timeline-item theme-hide' : 'timeline-item'))
|
||||||
|
.timeline-container
|
||||||
|
.timeline-year #{year}
|
||||||
|
|
||||||
|
each item in workItemGroups[year]
|
||||||
|
.work-container(class=(item.endDate ? 'timeline-item theme-hide' : 'timeline-item'))
|
||||||
|
.timeline-container
|
||||||
|
.timeline-content
|
||||||
|
.range-small #{item.rangeDisplay()}
|
||||||
|
h3 !{item.name}
|
||||||
|
p !{item.description}
|
||||||
|
.work-container
|
||||||
|
button#timeline-view-all show past work
|
||||||
|
|
||||||
|
section#contact
|
||||||
|
h2 get in touch
|
||||||
|
.contact-container
|
||||||
|
.message
|
||||||
|
p I'd love to hear from you if you have questions or inquiries related to me or my projects. You can get in touch by text, e-mail, or using this form. I also occasionally share thoughts on my <a href="/blog">blog</a>.
|
||||||
|
p <b>E-mail:</b> <a href="mailto:shout@garrettmills.dev">shout@garrettmills.dev</a>
|
||||||
|
.form
|
||||||
|
form#contact-form
|
||||||
|
.form-group
|
||||||
|
input#contactEmail.form-control(type='email' name='email' placeholder='E-Mail Address' required)
|
||||||
|
.form-group
|
||||||
|
input#contactFirst.form-control(name='first' placeholder='First Name' required)
|
||||||
|
.form-group
|
||||||
|
input#contactLast.form-control(name='last' placeholder='Last Name' required)
|
||||||
|
.form-group
|
||||||
|
textarea.form-control#contactMessage(name='message' placeholder='Message' required rows=6)
|
||||||
|
.form-group
|
||||||
|
button Send
|
||||||
|
|
||||||
|
section#recent
|
||||||
|
h2 latest updates
|
||||||
|
each item in feedPosts
|
||||||
|
.feed-item
|
||||||
|
.feed-category(id='feedPostTag_' + item.feedPostId)
|
||||||
|
.tag #{item.tag}
|
||||||
|
span
|
||||||
|
if !item.visible
|
||||||
|
p.text (draft)
|
||||||
|
p.text !{item.body}
|
||||||
|
.bottom
|
||||||
|
.stamp <a href="#{named('feed')}##{item.feedPostId}" class="feed-edit-button">permalink</a> | #{item.postedAt.toLocaleString()}
|
||||||
|
// each item in feed_items
|
||||||
|
// .feed-item
|
||||||
|
// div.feed-category(id='feedPostTag_' + item.id)
|
||||||
|
// .tag #{item.tag}
|
||||||
|
// span
|
||||||
|
// if item.draft
|
||||||
|
// p.text (draft)
|
||||||
|
// p.text !{item.text}
|
||||||
|
// .bottom
|
||||||
|
// if user && can_access.feed_delete && can_access.feed_edit
|
||||||
|
// .stamp <a href="/dash/c/form/FeedPost?id=#{item.id}" class="feed-edit-button" target="_blank">edit</a> | <a href="#recent" class="feed-delete-button" postid="#{item.id}">delete</a>
|
||||||
|
// .stamp <a href="#{app.url}feed##{item.id}" class="feed-edit-button">permalink</a> | #{item.date.toLocaleString()}
|
||||||
|
if feed_overflow
|
||||||
|
.row.mt-4
|
||||||
|
.col-12.text-center
|
||||||
|
a.button(href="/feed") view more
|
||||||
|
|
||||||
|
block append script
|
||||||
|
script(src="/assets/js/raj/welcome.js")
|
||||||
|
Loading…
Reference in New Issue
Block a user